<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Langur Monkey - Toni Sagristà Sellés</title><link>https://tonisagrista.com/</link><description>Recent content on Langur Monkey - Toni Sagristà Sellés</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><managingEditor>me@tonisagrista.com (Toni Sagristà Sellés)</managingEditor><webMaster>me@tonisagrista.com (Toni Sagristà Sellés)</webMaster><lastBuildDate>Tue, 12 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tonisagrista.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Local agentic workflows are getting crazy good</title><link>https://tonisagrista.com/blog/2026/local-llm-qwen3.6/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2026/local-llm-qwen3.6/</guid><description>&lt;p&gt;Building on the previous post about &lt;a href="https://tonisagrista.com/blog/2026/qwensay"&gt;how good current open TTS models are&lt;/a&gt;, this post focuses on local &lt;abbr title="Large Language Model"&gt;LLM&lt;/abbr&gt;s, and their explosion in quality in the recent &lt;del&gt;years&lt;/del&gt; months. I have been using &lt;a href="https://openrouter.ai"&gt;OpenRouter&lt;/a&gt; with, for the most part, &lt;a href="https://openrouter.ai/deepseek/deepseek-v4-flash"&gt;DeepSeek v4 Flash&lt;/a&gt;. It is a very capable model, dirt-cheap (seems to be heavily subsidized, at $0.14/M tokens input and $0.28/M tokens output), and it works very well when it is free to browse the internet. I have also been tinkering with local models like the Qwen3.5 family, Gemma 3/4, Mistral, and more. They are good, I haven&amp;rsquo;t been able to use the versions that can run reasonably on consumer hardware for anything too productive. That is until now. The brand new Qwen3.6 models, particularly the 27B dense, and the 35B A3B &lt;abbr title="Mixture of Experts"&gt;MoE&lt;/abbr&gt;, have really impressed the community. They are trivial to run on common setups (e.g. 16GB RTX card, ~32-64GB RAM), and when boosted with agentic capabilities, they perform very well.&lt;/p&gt;
&lt;p&gt;My use case is quite modest. I typically use LLMs for research. I explain the problem and propose a couple of solutions, and ask for feedback. When I&amp;rsquo;m new to a topic, I like to get a bird&amp;rsquo;s eye view type of summary first, and then I ask for further explanation and deeper insight on certain areas that interest me. If possible, I add the sources that may help it gain more knowledge on the subject. Sometimes, I ask it to proofread my writings, but I tend to restrict it to fixing grammatical and spelling errors, with the occasional wording suggestion. I think writing is valuable and useful, as it helps with structuring my thoughts and organizing my ideas. That&amp;rsquo;s why I frown upon AI-generated blog posts. Additionally, I don&amp;rsquo;t like the overly verbose and structured texts it generates. I don&amp;rsquo;t do much AI-assisted coding. Sometimes, I may ask it to generate the odd Python script to solve a specific task that would otherwise get tedious.&lt;/p&gt;
&lt;p&gt;My setup is like this: For casual questions and general chatting, I use OpenRouter chat for remote models and LM Studio for local models. Usually, I run the models on a gaming laptop (RTX 5080 16GB, 32GB RAM) and serve them to my machine via LM Link. For researching and more complicated workflows, I use the &lt;a href="https://hermes-agent.nousresearch.com"&gt;Hermes Agent&lt;/a&gt;. It is open source (MIT), fully-featured, self-improving (learns between sessions!), and offers everything I need with minimal configuration. It works very, very well. I connect it to either DeepSeek V4 Flash via OpenRouter, or to my local LM Studio, which is tethered to the inference machine via LM Link.&lt;/p&gt;
&lt;p&gt;The Qwen3.6 27B and 35B A3B models, released in April 2026, have made strides in the community. They are extremely capable for their small size. They don&amp;rsquo;t have a ton of knowledge on all domains like the near-trillion-parameter frontier models, but when paired with the right toolset, they get very close. They don&amp;rsquo;t fit in most GPUs, but they can be partially offloaded. The MoE model in particular, with only 3B parameters active during inference, is very fast. I get ~35-40 tok/s with the 35B A3B MoE model, and about 4.5 tok/s with the 27B dense model. The dense model is borderline unusable, so I almost always rely on the MoE model. I offload 19 layers to the GPU (~12.5 GB out of a total of 26.63 GB), and use the maximum context size of ~262K tokens. This is plenty for agent sessions and works very well. The model is able to plan and execute actions very efficiently. It tends to overthink a lot, but this is an acceptable trade-off given the output quality. On OpenRouter, this exact same model is more expensive than DeepSeek V4 Flash, at $0.15/$1 per million tokens input/output. Being able to run such a model locally feels like a superpower.&lt;/p&gt;
&lt;p&gt;So, now the question is obvious. Will local LLM get good enough in the future to replace frontier models? I&amp;rsquo;d argue that it depends on the task. Frontier models have a vast amount of knowledge and are able to one-shot many complex problems successfully. They will continue to be more suited for bleeding-edge research for the years to come. But they are also very expensive, very taxing on the environment, and the primary reason for the sky-high current hardware prices. There is certainly a case to be made for local and more economic small models.For day-to-day agentic workflow, I think open models are very close already. I would bet that in the next 2 years they will get on pair for most everyday tasks. Is this prediction grounded on reality, or just wishful thinking? I&amp;rsquo;d like to think the former. In any case, I&amp;rsquo;ll be here, watching, tinkering.&lt;/p&gt;</description></item><item><title>Local TTS is getting very capable and accessible</title><link>https://tonisagrista.com/blog/2026/qwensay/</link><pubDate>Wed, 29 Apr 2026 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2026/qwensay/</guid><description>&lt;p&gt;Around 2007 I spent half a year in the &lt;a href="https://abdn.ac.uk"&gt;University of Aberdeen&lt;/a&gt; working on my &lt;a href="https://tonisagrista.com/projects/#computer-aided-catalan-learning-application"&gt;final year project&lt;/a&gt; involving &lt;abbr title="Natural Language Processing"&gt;NLP&lt;/abbr&gt;. The project consisted of a series of modules to build an graphical adventure-type game controlled by language input in the form of text. It also featured speech output generated by an early &lt;abbr title="Text-to-Speech"&gt;TTS&lt;/abbr&gt; system. To achieve this, we managed to partner with a group at &lt;a href="https://www.lasalle.edu/"&gt;La Salle University&lt;/a&gt; that were working on a TTS system for Catalan. It was a closed system that was accessible via a web API, but that option was far too slow for real time use. I ended up preprocessing the audio of all dialogs in the project into WAV files just to be able to play them in sync with mouth movements. At that time, I was amazed that a computer could so easily convert text to an &lt;em&gt;understandable&lt;/em&gt; audio file. The voice was very plain and robotic, with no emotion whatsoever. The results were hit or miss, but it &lt;em&gt;worked&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Fast forward to today, TTS systems are everywhere. Several groups have released low-parameter TTS models that run very well on consumer hardware. I have been using the lightweight &lt;a href="https://github.com/KittenML/KittenTTS"&gt;Kitten TTS&lt;/a&gt; for a while with fantastic results.&lt;/p&gt;
&lt;p&gt;The models are so lightweight that some websites are heavier than entire Kitten TTS models:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Download&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;kitten-tts-mini&lt;/td&gt;
&lt;td&gt;80M&lt;/td&gt;
&lt;td&gt;80 MB&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/KittenML/kitten-tts-mini-0.8"&gt;KittenML/kitten-tts-mini-0.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kitten-tts-micro&lt;/td&gt;
&lt;td&gt;40M&lt;/td&gt;
&lt;td&gt;41 MB&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/KittenML/kitten-tts-micro-0.8"&gt;KittenML/kitten-tts-micro-0.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kitten-tts-nano&lt;/td&gt;
&lt;td&gt;15M&lt;/td&gt;
&lt;td&gt;56 MB&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/KittenML/kitten-tts-nano-0.8-fp32"&gt;KittenML/kitten-tts-nano-0.8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;kitten-tts-nano (int8)&lt;/td&gt;
&lt;td&gt;15M&lt;/td&gt;
&lt;td&gt;25 MB&lt;/td&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/KittenML/kitten-tts-nano-0.8-int8"&gt;KittenML/kitten-tts-nano-0.8-int8&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Projects like &lt;a href="https://github.com/Mic92/puss-say"&gt;&lt;code&gt;puss-say&lt;/code&gt;&lt;/a&gt; streamline and trivialize Kitten TTS inference. I have a shell script in one of my &lt;code&gt;bin&lt;/code&gt; directories that does everything in a single command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#!/usr/bin/env bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;uvx --from &lt;span style="color:#a6be9d"&gt;&amp;#39;git+https://github.com/Mic92/puss-say&amp;#39;&lt;/span&gt; puss-say &lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;$@&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This clones the project, pulls dependencies and models, and plays the audio. It is quite fast, especially when using cached data. Kitten TTS produces acceptable results, though the output usually lacks emotion and nuance. For simple use cases (reading notifications, generating voiceovers for scripts) it&amp;rsquo;s more than sufficient.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/QwenLM/Qwen3-TTS"&gt;Qwen3-TTS&lt;/a&gt;, which I&amp;rsquo;ve been recently testing, represents a step-up in quality. It&amp;rsquo;s extremely good, and local inference is practical even on modest hardware given the model sizes. It offers three interesting variants:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;Hugging Face ID&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;VRAM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.7B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Free-form voice descriptions&lt;/td&gt;
&lt;td&gt;~6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.7B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Preset speakers + style instructions&lt;/td&gt;
&lt;td&gt;~6 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.6B&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Qwen/Qwen3-TTS-12Hz-0.6B-Base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Voice cloning from reference audio&lt;/td&gt;
&lt;td&gt;~2 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The voice design models are particularly clever: you describe the voice you want alongside the text to convert. Want a deep, gravelly voice with a Scottish accent? Or an excited teenager talking about a video game? Just describe it. It&amp;rsquo;s remarkable that you can run this locally so easily. However, as far as I know there&amp;rsquo;s no off-the-shelf CLI tool that handles dependencies, downloads the model, and runs inference out of the box.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s why I created &lt;a href="https://codeberg.org/langurmonkey/qwensay"&gt;QwenSay&lt;/a&gt;. With it, you can clone the repository and convert text to speech locally from your terminal without wrestling with dependencies or writing any code.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how it works. First, set it up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# Clone the repo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git clone ssh://git@codeberg.org/langurmonkey/qwensay.git
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;cd&lt;/span&gt; qwensay
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# Default dependencies&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;uv sync
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# For modern RTX cards, use flash attention&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;uv sync --extra gpu
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# For Pascal (GTX 10x0 family), you need a special CUDA version&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;uv sync --group torch-pascal --no-group torch-default
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, you are ready to convert your text to speech with Qwen3-TTS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;uv run qwensay.py &lt;span style="color:#a6be9d"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --text &lt;span style="color:#a6be9d"&gt;&amp;#34;Good morning! Today is going to be a great day.&amp;#34;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --instruct &lt;span style="color:#a6be9d"&gt;&amp;#34;A cheerful, energetic young woman with a clear American accent&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This uses the default 1.7B voice design model. You can also specify the model with &lt;code&gt;--model&lt;/code&gt;. There are many other CLI arguments that you can use to tune your output. Check out the &lt;a href="https://codeberg.org/langurmonkey/qwensay/src/branch/master/README.md"&gt;repository documentation&lt;/a&gt; for more details.&lt;/p&gt;
&lt;p&gt;Whether you&amp;rsquo;re building accessibility features, creating voiceovers for projects, or just experimenting, this is worth a try. I&amp;rsquo;ve made QwenSay my go-to TTS tool because it produces high-quality results and is genuinely fast.&lt;/p&gt;</description></item><item><title>Fine-tuning Qwen3.5 for Gaia Sky</title><link>https://tonisagrista.com/blog/2026/fine-tuning-qwen3.5-gaiasky/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><author>Toni Sagristà Sellés</author><guid>https://tonisagrista.com/blog/2026/fine-tuning-qwen3.5-gaiasky/</guid><description>&lt;p&gt;A little over a year ago I set up a &lt;a href="https://tonisagrista.com/blog/2025/local-llm-rag"&gt;local pipeline&lt;/a&gt; to use different &lt;abbr title="Large Language Model"&gt;LLM&lt;/abbr&gt;s to &lt;a href="https://tonisagrista.com/blog/2025/gaiasky-ai-assistant"&gt;respond to Gaia Sky questions&lt;/a&gt; using &lt;abbr title="Retrieval Augmented Generation"&gt;RAG&lt;/abbr&gt;. In that post, I built a dynamic scrapper that parsed the Gaia Sky website and documentation and ingested the content it into a vector database. Then, I built a minimal terminal chatbot interface that received the user prompt, queried the database for semantically similar data, and built up the context for each LLM call. The results were promising, and I found that they (obviously) strongly depended on the model used.&lt;/p&gt;
&lt;p&gt;Fast forward a few months, and the Qwen 3.5 models were released by Alibaba. The general consensus is that they are quite good for their size. I&amp;rsquo;ve been testing them for local inference with a similar impression. I thought that it would be interesting to repeat the exercise of creating a Gaia Sky AI assistant, but using a radically different approach: Instead of RAG, I would &lt;strong&gt;fine-tune the model&lt;/strong&gt; itself. In this post, I describe this fine-tuning project, from the creation and engineering of the training dataset to the fine-tuning and production of the final &lt;abbr title="GPT-Generated Unified Format"&gt;GGUF&lt;/abbr&gt; models.&lt;/p&gt;
&lt;p&gt;This project is composed by two, very distinct parts, which map to top-level chapters in this post:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#training-dataset-creation"&gt;Training dataset creation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#fine-tuning"&gt;Fine tuning&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At the end I quickly evaluate the results in the &lt;a href="#testing"&gt;testing&lt;/a&gt; section.&lt;/p&gt;
&lt;p&gt;The source code, dataset, and models discussed in this post are in the following repositories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dataset creation and fine-tuning - &lt;i class="fa fa-gitea" aria-hidden="true" title="Codeberg"&gt;&lt;/i&gt; &lt;a href="https://codeberg.org/gaiasky/gaiasky-finetune"&gt;gaiasky-finetune&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Gaia Sky training dataset repository - &lt;i class="fa fa-git-square" aria-hidden="true" title="Codeberg"&gt;&lt;/i&gt; &lt;a href="https://hf.co/Langurmonkey/gaiasky-training-dataset"&gt;gaiasky-training-dataset&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Qwen3.5 Gaia Sky fine-tuned models - &lt;i class="fa fa-git-square" aria-hidden="true" title="Codeberg"&gt;&lt;/i&gt; &lt;a href="https://hf.co/Langurmonkey/gaiasky-qwen-3.5-gguf"&gt;gaiasky-qwen-3.5-gguf&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="hardware"&gt;Hardware&lt;/h2&gt;
&lt;p&gt;Here is the hardware I have used to create the dataset and fine-tune the model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Desktop PC&lt;/strong&gt; - Arch Linux, Intel(R) Core(TM) i7-7700 (8) @ 4.20 GHz, 32 GB RAM, NVIDIA GeForce GTX 1070 8 GB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Laptop 1&lt;/strong&gt; - Windows 11, WSL2 (Arch Linux), Intel(R) Core(TM) Ultra 9 275HX (24) @ @ 3.07 GHz, 32 GB RAM, NVIDIA GeForce RTX 5080 Mobile 16 GB.
&lt;a href="#laptop2"&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Laptop 2&lt;/strong&gt; - Arch Linux, Intel(R) Core(TM) i7 8750H (12) @ 4.10 GHz, 16 GB RAM, NVIDIA GeForce GTX 1060 Mobile 6 GB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="training-dataset-creation"&gt;Training dataset creation&lt;/h2&gt;
&lt;p&gt;The creation of the training dataset is the most important piece of work in this project. It is composed of three parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#documentation-dataset"&gt;Documentation dataset&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#api-dataset"&gt;API dataset&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#identity"&gt;Identity&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="documentation-dataset"&gt;Documentation dataset&lt;/h3&gt;
&lt;p&gt;When I started this project, my first instinct was &amp;ldquo;more is better.&amp;rdquo; I thought that if I fed the model every single &lt;code&gt;.java&lt;/code&gt;, &lt;code&gt;.glsl&lt;/code&gt;, &lt;code&gt;.rst&lt;/code&gt;, and &lt;code&gt;.md&lt;/code&gt; file in the Gaia Sky repositories (project, documentation, etc.), it would emerge as an expert. Oh boy, was I wrong.&lt;/p&gt;
&lt;p&gt;A large codebase contains a lot of boilerplate noise. Getters, setters, license blocks, and infrastructure code that doesn&amp;rsquo;t actually help a model understand &lt;em&gt;how&lt;/em&gt; the engine works or &lt;em&gt;how&lt;/em&gt; to write scripts for it. I soon realized that the dataset is the single most important part of the project, and it needed a surgical approach.&lt;/p&gt;
&lt;p&gt;The plan was to automate the process of creating the dataset to a degree, and then use it to fine-tune the &lt;strong&gt;Qwen 3.5 4B&lt;/strong&gt; and &lt;strong&gt;8B&lt;/strong&gt; model variants.&lt;/p&gt;
&lt;p&gt;I wrote &lt;code&gt;generate-raw-dataset.py&lt;/code&gt; to act as a high-pass filter. Instead of a blind crawl, I implemented an &lt;strong&gt;allowlist&lt;/strong&gt; system. I would only let in the load-bearing files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; By far, the most important data, containing exhaustive human-written documentation pages. We convert the documentation &lt;abbr title="reStructuredText"&gt;RST&lt;/abbr&gt; files to Markdown with &lt;code&gt;pandoc&lt;/code&gt;, and then we add some additional key files, like the project&amp;rsquo;s &lt;code&gt;README.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Core Logic:&lt;/strong&gt; Selected Java files that are representative of the brain of the engine (main loop, scene, renderer, etc.).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visual Logic:&lt;/strong&gt; Selected shader files that define the look of the cosmos (stars, particles, PBR, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Almost every source file in Gaia Sky starts with a copyright header. This is &amp;ldquo;dead weight&amp;rdquo; for training. I added a regex-based stripper to ensure the model&amp;rsquo;s limited context window was filled with code logic, not license text:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;strip_java_copyright&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;content&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Matches the opening /* until the first */ at the start of the file&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;content&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;re&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;sub&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;r&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#39;^/\*.*?\*/&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;&amp;#39;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;content&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;count&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;flags&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;re&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;DOTALL&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;content&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;strip&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The output of this first phase was a &lt;code&gt;jsonl&lt;/code&gt; file where each line represented a single, cleaned-up file. It looked like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;instruction&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;core/src/gaiasky/util/coord/Coordinates.java&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;public class Coordinates {\n // Implementation of transformation matrices...\n}&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;source_file&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;core/src/gaiasky/util/coord/Coordinates.java&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This provided the &amp;ldquo;Context&amp;rdquo; for the next phase. However, a model trained directly on this would just learn to autocomplete files. To make it an &lt;em&gt;assistant&lt;/em&gt;, we had to turn these files into a conversation.&lt;/p&gt;
&lt;p&gt;Once I had a clean extraction of the most relevant information pieces, I faced a new problem. A raw dump of a &lt;code&gt;Coordinates.java&lt;/code&gt; file is great for a search engine, but it is not a conversation. To turn these files into training data, I used a &amp;ldquo;teacher&amp;rdquo; model, Qwen 3.5 27B, to look at each file and generate a specific number of Q&amp;amp;A pairs. I wrote &lt;code&gt;distill-dataset.py&lt;/code&gt; to handle this. The script calculates how many questions a file is worth based on its length and type. A long documentation file might get 25 Q&amp;amp;A pairs, while a short shader might only get 4. Below is an extract of the method that computes the number of target pairs for a file.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;calculate_target_pairs&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;file_path&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;content&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;&amp;#34;&amp;#34;Determine how many Q&amp;amp;A pairs to generate based on file metadata.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;filename&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;file_path&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;lower&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;content_len&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;len&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;content&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Skip files that are essentially empty&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;content_len&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;150&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Set a modest base count that depends on type&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Docs are high-value, so they start slightly higher than code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;filename&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;endswith&lt;/span&gt;((&lt;span style="color:#a6be9d"&gt;&amp;#39;.md&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;.rst&amp;#39;&lt;/span&gt;)):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;base_count&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;4&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# 1 pair per 750 characters for documentation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;chars_per_pair&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;750&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;base_count&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# 1 pair per 1500 characters for source code&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;chars_per_pair&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Calculate length-based bonus without a tiny cap&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# This scales better for massive files while keeping short files lean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;length_bonus&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;content_len&lt;/span&gt; &lt;span style="color:#ff636f"&gt;//&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;chars_per_pair&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Sum and apply a reasonable maximum&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;final_count&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;base_count&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;length_bonus&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Cap it to avoid overwhelming the distillation for single files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# but ensure even tiny files get at least the base_count&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;max&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;base_count&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;min&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;final_count&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;20&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Initially, I used the &lt;abbr title="Mixture of Experts"&gt;MoE&lt;/abbr&gt; Qwen 3.5 30B A3B, but it was consistently outputting the wrong format. Then I switched to the 27B dense model, and it performed a little better. Even so, I had to tell the model &lt;strong&gt;exactly&lt;/strong&gt; how to behave. Here are the key items I learned the hard way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Match the answer type:&lt;/strong&gt; If the question doesn&amp;rsquo;t ask for code, don&amp;rsquo;t provide it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grounding:&lt;/strong&gt; Every claim must be directly grounded on the source text.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diversity:&lt;/strong&gt; Every question must cover a different detail.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also found that, at these model sizes, it is better to batch Q&amp;amp;A pairs instead of asking the model to provide 20 of them in one go. I finally gravitated to 3 Q&amp;amp;A pairs per inference call. To prevent the model from repeating itself across batches, I tracked existing questions and fed them back into the prompt as exclusions.&lt;/p&gt;
&lt;p&gt;The prompt is constructed as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# A snippet from the distillation prompt&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;prompt&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;You are a Gaia Sky technical expert. &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;file_hint&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;Generate exactly &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;current_target&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt; Q&amp;amp;A pairs strictly based on the source text below. &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;Every fact and claim in your answers must be directly grounded in the source text. &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;Never invent API calls or function names not present in the source. &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;Match the answer type to the question: if no code is asked for, give no code. &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;HIGHLY DIVERSE Q&amp;amp;A pairs. Every question must cover a COMPLETELY DIFFERENT topic or detail &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;of the source text than the ones already provided. &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;avoid_context&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;Format the output as JSONL: one JSON object per line, with the question in the &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;instruction&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt; field and the answer in the &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;output&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt; field. &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;Output ONLY the JSONL, nothing else.&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;SOURCE FILE: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;file_path&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#39;=&amp;#39;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;60&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;file_content&lt;/span&gt;[:&lt;span style="color:#58a1dd"&gt;context_limit&lt;/span&gt;]&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#39;=&amp;#39;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;60&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It consists of the following parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Base text - This is composed by the raw strings in the &lt;code&gt;prompt&lt;/code&gt; variable.&lt;/li&gt;
&lt;li&gt;The file hint (&lt;code&gt;{file_hint}&lt;/code&gt;) - We add hints depending on the filetype. The following table displays the hint for each type.
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filetype&lt;/th&gt;
&lt;th&gt;Extensions&lt;/th&gt;
&lt;th&gt;Hint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This is Java source code. Focus on class responsibilities, method signatures, and architectural patterns. Do NOT generate Python scripting examples.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This is Python scripting code for the Gaia Sky API. Questions about usage and parameters are appropriate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shader&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.glsl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This is a GLSL shader. Focus on the rendering technique, uniforms, and mathematical operations. Do NOT generate Python scripting examples.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.md&lt;/code&gt; &lt;code&gt;.rst&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This is documentation. Focus on concepts, features, workflows, and user-facing features.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/li&gt;
&lt;li&gt;The pair count (&lt;code&gt;{current_target}&lt;/code&gt;) - Contains the number of Q&amp;amp;A pairs to generate.&lt;/li&gt;
&lt;li&gt;The previous Q&amp;amp;A pairs, if any (&lt;code&gt;{avoid_context}&lt;/code&gt;) - This is constructed by listing the existing pairs, as parsed by the program in the output, or accumulated in the current run.
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;instr_list&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; [&lt;span style="color:#58a1dd"&gt;p&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#39;instruction&amp;#39;&lt;/span&gt;] &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;p&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;existing_pairs&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;avoid_context&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;Do NOT repeat or rephrase any of these already-covered questions:&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;join&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;- &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;instr&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;instr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;instr_list&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;The filepath and content (&lt;code&gt;{file_path}&lt;/code&gt;, &lt;code&gt;{file_content}&lt;/code&gt;) - Contain the filename and the actual content, which is capped to fit within the context length.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, local LLMs tend to be chatty. Even with such strict instructions, larger models like the 27B sometimes also mess up. From time to time, this model would leak its own reasoning into the output. It would start its response with &lt;code&gt;&amp;quot;The user asked to provide 34 Q&amp;amp;A pairs given the source material...&amp;quot;&lt;/code&gt; or it would include meta-talk like &lt;code&gt;&amp;quot;This question is safe because it is derived from line 45.&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This created &lt;em&gt;dirty&lt;/em&gt; data that polluted the dataset and undermined the fine-tuning process. If I trained on this, the final model would start every answer by talking to itself.&lt;/p&gt;
&lt;p&gt;To fix this, I built &lt;code&gt;sanitize-jsonl.py&lt;/code&gt;. This script is a heavy-duty cleaner that uses regex to strip out training artifacts. It first tries to rescue bad rows, and if it fails, it deletes them. If the model accidentally put both the question and answer in the output field, the sanitizer attempts to detect the question mark and splits them back into the correct structure.&lt;/p&gt;
&lt;p&gt;Here is a look at what the data looked like before and after the sanitization process.&lt;/p&gt;
&lt;h6 id="raw-distilled-entry"&gt;Raw distilled entry&lt;/h6&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;instruction&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;I need to generate 3 pairs based on the Gaia Sky API. Here is pair 1:&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Question: How do I set the camera position? Answer: Use the gs.setPosition(pos)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; method. I ensured this doesn&amp;#39;t repeat previous pairs.&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;source_file&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;APIv1.md&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h6 id="sanitized-gold-entry"&gt;Sanitized &amp;ldquo;gold&amp;rdquo; entry&lt;/h6&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;instruction&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;How do I set the camera position?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Use the gs.setPosition(pos) method.&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;source_file&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;api.md&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The sanitizer also had to deal with Javadoc remnants. Since Gaia Sky is a Java project, class- and method-level comment blocks are full of HTML tags like &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; (Javadoc syntax). The script converts these into clean Markdown so the LLM learns a consistent documentation style.&lt;/p&gt;
&lt;p&gt;By the end of this process, I had &lt;code&gt;gaiasky-gold.jsonl&lt;/code&gt;. This contains a curated, clean list of questions and answers based on the whole project documentation.&lt;/p&gt;
&lt;h3 id="api-dataset"&gt;API dataset&lt;/h3&gt;
&lt;p&gt;Documentation is important, but I want the model to learn some Gaia Sky scripting as well. To do so, a new API/scripting dataset needed to be generated. To solve this, I built a synthetic data factory designed to teach the model both the &lt;em&gt;content&lt;/em&gt; of the API and the &lt;em&gt;context&lt;/em&gt; of how to use it.&lt;/p&gt;
&lt;p&gt;The first step was grounding the model. I wrote a script (&lt;code&gt;api-extract.py&lt;/code&gt;) that scans the Java source files and uses Regex to pair Javadoc comments with their corresponding method signatures. The &lt;code&gt;extract_from_file()&lt;/code&gt; method uses regular expressions and a little bit of logic to generate Q&amp;amp;A pairs based on method signatures and their Javadoc documentation:&lt;/p&gt;
&lt;details class="collapsedcode"&gt;
&lt;summary&gt;
&lt;span class="code"&gt;Code: &lt;/span&gt;&lt;span class="summary"&gt;Extract API Q&amp;amp;A pairs from a file&lt;/span&gt;
&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-Python" data-lang="Python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;extract_from_file&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;file_path&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;&amp;#34;&amp;#34;Extracts Javadoc blocks and method signatures from a single Java file.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#ff636f"&gt;not&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;os&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;path&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;exists&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;file_path&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;Skipping: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;file_path&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt; (not found)&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;with&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;open&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;file_path&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;encoding&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#39;utf-8&amp;#39;&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;f&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;content&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;f&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;read&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Regex: Matches Javadoc and the following method signature&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;pattern&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;re&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;compile&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;r&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#39;/\*\*(.*?)\*/\s+([^;{]+)[;{]&amp;#39;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;re&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;DOTALL&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;extracted&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pattern&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;finditer&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;content&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;doc&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;group&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;)&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;strip&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Clean Javadoc: remove leading stars and excess whitespace&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;doc&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;join&lt;/span&gt;([&lt;span style="color:#58a1dd"&gt;line&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;strip&lt;/span&gt;()&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;lstrip&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#39;*&amp;#39;&lt;/span&gt;)&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;strip&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;line&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;doc&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;split&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#39;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#39;&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;line&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;strip&lt;/span&gt;()])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;method_sig&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;group&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;2&lt;/span&gt;)&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;strip&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Clean method: remove annotations (like @Override) and newlines&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;method_sig&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;re&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;sub&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;r&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#39;@\w+(\(.*\))?&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;&amp;#39;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;method_sig&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;method_sig&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34; &amp;#34;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;join&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;method_sig&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;split&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;extracted&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;append&lt;/span&gt;({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;instruction&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;How do I use the &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;method_sig&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;split&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#39;(&amp;#39;&lt;/span&gt;)[&lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;]&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt; method in Gaia Sky?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;The method `&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;method_sig&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;` is used for: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;doc&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;source_file&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;API_Reference/&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;os&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;path&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;basename&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;file_path&lt;/span&gt;)&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;extracted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;This process produces the &lt;code&gt;gaiasky-api-raw.jsonl&lt;/code&gt;, which is used in the next step. It contains the API calls with their respective documentation.&lt;/p&gt;
&lt;p&gt;However, knowing a function exists isn&amp;rsquo;t enough. The model needs to know how to script with it. To address this, I developed &lt;code&gt;generate-scripting-dataset.py&lt;/code&gt; to transform those raw Java signatures into a diverse pedagogical dataset. As input, it gets all test and showcase scripts in the Gaia Sky repository, and the raw API &lt;abbr title="JSON Lines"&gt;JSONL&lt;/abbr&gt; file. It produces four types of output, termed A, B, C, and D:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Type A:&lt;/strong&gt; The API reference&lt;/p&gt;
&lt;p&gt;These are direct &amp;ldquo;How do I use X?&amp;rdquo; pairs. They include the parameters, the return types, and a basic Python example.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Type B:&lt;/strong&gt; The task synthesis&lt;/p&gt;
&lt;p&gt;This step is optional, and I ended up not including it in the final dataset. However, I think it is still worth mentioning. I used the larger teacher dense model (27B) to generate complex tasks (e.g., &amp;ldquo;Write a script that navigates to Mars, waits 5 seconds, and takes a screenshot&amp;rdquo;). The script provided the teacher model with a safe list of real functions extracted in Step 1 as a sort of guardrail. If the teacher tried to hallucinate a command, the script flagged and discarded it.&lt;/p&gt;
&lt;p&gt;The results of this section were kind of underwhelming, possibly because more parameters are needed for such open-ended tasks.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Type C:&lt;/strong&gt; Adversarial error correction&lt;/p&gt;
&lt;p&gt;This is my favorite part. I programmatically &lt;em&gt;broke&lt;/em&gt; the API calls to teach the model how to fix its own mistakes. The script would generate a &lt;em&gt;wrong&lt;/em&gt; script (e.g., using &lt;code&gt;snake_case&lt;/code&gt; instead of &lt;code&gt;camelCase&lt;/code&gt; or missing a required argument) and then provide the &lt;em&gt;correct&lt;/em&gt; version. The end goal was to prevent common LLM failures before they happen.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Type D:&lt;/strong&gt; The &amp;ldquo;gold standard&amp;rdquo; Library&lt;/p&gt;
&lt;p&gt;Finally, I indexed the actual test and showcase scripts from the Gaia Sky repository. These are human-written, battle-tested scripts that show the model how to handle complex logic, loops, and math.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="identity"&gt;Identity&lt;/h3&gt;
&lt;p&gt;Finally, I prepared a small &lt;code&gt;identity.jsonl&lt;/code&gt; file with essential project information that &lt;strong&gt;must&lt;/strong&gt; appear in the final integrated training dataset. It only contains 17 lines of Q&amp;amp;A, but it is rather important. Here is an excerpt of a few lines (formatted for readability):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;instruction&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Which institution is behind Gaia Sky?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Gaia Sky is developed at the Astronomisches Rechen-Institut (ARI),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; which is part of the Zentrum für Astronomie (ZAH) at Heidelberg University.&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;source_file&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Identity/Project_Meta&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;instruction&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Is Gaia Sky part of the ESA Gaia Mission?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Yes, Gaia Sky is developed within the framework of ESA&amp;#39;s Gaia mission,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; specifically as part of the German contribution to the Gaia Data Processing
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; and Analysis Consortium (DPAC).&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;source_file&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Identity/Project_Meta&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;instruction&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;What is the official repository for Gaia Sky?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;output&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;The official repository is hosted on Codeberg
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; at https://codeberg.org/gaiasky/gaiasky.&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;&amp;#34;source_file&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;Identity/Project_Meta&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The final dataset was composed by concatenating the three parts, documentation, API, and identity. It can be explored here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hf.co/datasets/Langurmonkey/gaiasky-training-dataset"&gt;gaiasky-training-dataset@HuggingFace&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once the dataset was ready, it was time for the actual fine-tuning.&lt;/p&gt;
&lt;h2 id="fine-tuning"&gt;Fine-tuning&lt;/h2&gt;
&lt;p&gt;With a dataset of 3,800+ specialized Gaia Sky pairs ready, it was time for the actual training. For this, I leaned on two heavy hitters in the open-source world: &lt;strong&gt;Unsloth&lt;/strong&gt; and &lt;strong&gt;Qwen 3.5&lt;/strong&gt;. I started by training the 4B model, and then realized that I could also fit the 9B one in my GPU. In the post I&amp;rsquo;ll focus on the larger version of the model. I went as high as my local hardware allowed. Otherwise, I would have tried the 27B model, or even the 122B-A10B.&lt;/p&gt;
&lt;p&gt;Training a model with 9 billion parameters typically requires a massive server cluster, but by using &lt;strong&gt;4-bit &lt;abbr title="Low-Rank Adaptation"&gt;LoRA&lt;/abbr&gt;&lt;/strong&gt;&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, I was able to squeeze the entire process onto a single &lt;strong&gt;RTX 5080 (16GB)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The RTX 5080 is a beast, but to get the most out of it, I enabled &lt;strong&gt;TensorFloat-32 (TF32)&lt;/strong&gt;. This allows the GPU to handle the heavy matrix multiplications of deep learning much faster than standard &lt;code&gt;float32&lt;/code&gt;, without the precision loss of &lt;code&gt;float16&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# blackwell-specific optimizations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;torch&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;backends&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;cuda&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;matmul&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;allow_tf32&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;torch&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;backends&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;cudnn&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;allow_tf32&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I used the following parameters for the fine-tuning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;LoRA Rank:&lt;/strong&gt; 32&lt;/p&gt;
&lt;p&gt;A balance between learning new patterns (like the Gaia Sky API) and retaining general knowledge.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Target Modules:&lt;/strong&gt; &lt;code&gt;q, k, v, o, gate, up, down&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;All major projection layers)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;BF16:&lt;/strong&gt; Yes&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Learning rate:&lt;/strong&gt; \(2.0\times10^{-4}\)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Epochs:&lt;/strong&gt; 1&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Optimizer:&lt;/strong&gt; AdamW 8-bit&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The dataset is downloaded directly from the hub. It gets tokenized properly and passed into the &lt;abbr title="Supervised Fine-Tuning"&gt;SFT&lt;/abbr&gt; Trainer object for fine-tuning. The full code is in the file &lt;code&gt;finetune.py&lt;/code&gt;, and is surprisingly compact. The heavy lifting is done by &lt;code&gt;unsloth&lt;/code&gt; and &lt;code&gt;torch&lt;/code&gt;, of course.&lt;/p&gt;
&lt;p&gt;Once the LoRA weights are trained, they are dead weight until converted into a format people could actually use. I wrote &lt;code&gt;push-model.py&lt;/code&gt; to automate the most tedious part of the pipeline:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Quantization:&lt;/strong&gt; Converting the model to &lt;strong&gt;Q4_K_M GGUF&lt;/strong&gt;, or whatever other quant. This reduces the model size enough that it can run on almost any modern laptop while keeping its capabilities mostly intact.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HF upload:&lt;/strong&gt; Automatically pushing the finished &lt;code&gt;.gguf&lt;/code&gt; file to HuggingFace so the community can pull it directly into LM Studio or Ollama.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After roughly an hour of training, and another one of quantizing and uploading, I had a fine-tuned &lt;strong&gt;Gaia Sky expert&lt;/strong&gt; that performs great. Or did I? The next section discusses the performance of the model.&lt;/p&gt;
&lt;h2 id="testing"&gt;Testing&lt;/h2&gt;
&lt;p&gt;Given the limited time I have and the low-parameter models used, my tests have been scarce. My expectations are not very high, but I still wanted to test the output of the fine-tuning and see how well the new knowledge was retained by the model. I only tested the Qwen 3.5 9B fine-tuned variant and compared it to the base model. You can get this model from HuggingFace:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/Langurmonkey/gaiasky-qwen-3.5-gguf"&gt;gaiasky-qwen-3.5-gguf@HuggingFace&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I used the same questions as in the &lt;a href="https://tonisagrista.com/blog/2025/gaiasky-ai-assistant"&gt;Gaia Sky AI assistant post&lt;/a&gt; from 2025. My evaluation of the results is totally subjective, and includes a comparison with the base Qwen 3.5 9B.&lt;/p&gt;
&lt;p&gt;First, I recommend using a low temperature setting (&lt;strong&gt;0.2&lt;/strong&gt; in my tests) to avoid hallucinations and make sure the model does not get very &amp;ldquo;creative&amp;rdquo;. Also, I would suggest using the following system prompt, which frames the conversation and sets the tone:&lt;/p&gt;
&lt;div class="container-block"&gt;
&lt;p&gt;You are the Gaia Sky AI Expert, a specialized assistant with deep knowledge of the Gaia Sky universe, its Python API (v1 and v2), and its Java source code. Your goal is to help users explore the cosmos, write scripts, and understand the engine&amp;rsquo;s inner workings.&lt;/p&gt;
&lt;h3 id="knowledge--capabilities"&gt;Knowledge &amp;amp; Capabilities:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;API Expertise: You know both the Legacy API (v1) and the Modern Component API (v2: camera, time, data, scene, graphics, camcorder, etc.). Prioritize the Modern API (v2) for new scripts unless Legacy is requested. Never invent method names, be 100% sure, otherwise suggest the user to look up the docs.&lt;/li&gt;
&lt;li&gt;Source Code: You understand the underlying Java architecture (libGDX, Ashley ECS, LWJGL3, OpenGL) and can explain how features like LOD, shaders, and data loaders are implemented.&lt;/li&gt;
&lt;li&gt;Documentation: You are familiar with the Gaia Sky capabilities, installation guides, and dataset management.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="behavioral-guidelines"&gt;Behavioral Guidelines:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Be Grounded: If a feature or method does not exist in your training data, do not invent it. State: &amp;ldquo;I don&amp;rsquo;t see that specific feature in the current Gaia Sky implementation.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Code Style: Always provide clean, commented Python or Java code. Use snake_case for Modern API calls.&lt;/li&gt;
&lt;li&gt;Tone: Professional, scientific, and enthusiastic about astrovisualization. Answer greetings warmly, then offer Gaia Sky-specific assistance.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p&gt;That said, let&amp;rsquo;s go for the first question.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: State the possible usages of Gaia Sky.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A: Gaia Sky is a powerful open-source universe explorer and planetarium software designed for both interactive exploration and scientific visualization. Its primary use cases include:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Interactive Exploration&lt;/strong&gt;: Navigate the Milky Way, explore exoplanets, view the solar system in high detail, and time-travel through cosmic history using an intuitive interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scientific Visualization&lt;/strong&gt;: Visualize complex astrophysical datasets (catalogs, simulations) with advanced rendering techniques like LOD (Level of Detail), shaders, and particle effects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Education &amp;amp; Outreach&lt;/strong&gt;: Create immersive presentations or tutorials to teach astronomy concepts through dynamic 3D models of celestial objects.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scripting &amp;amp; Automation&lt;/strong&gt;: Automate camera paths, data loading, and scene manipulation via Python (Modern API v2) or Java for reproducible workflows and research presentations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Analysis&lt;/strong&gt;: Load and manipulate large-scale astronomical datasets directly within the application to analyze spatial distributions without external tools.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is an excellent response to an easy question. It did not invent anything and correctly applied documentation knowledge like camera paths or LOD. Compared to base Qwen, which hallucinated a lot, this is much more grounded.&lt;/p&gt;
&lt;p&gt;Next is a question about the installation procedure on Linux.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: How do I install Gaia Sky on Linux?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A: To install Gaia Sky on Linux, follow these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Download the Installer&lt;/strong&gt;&lt;br&gt;
Visit &lt;a href="https://gaiasky.space/download"&gt;https://gaiasky.space/download&lt;/a&gt; and download the &lt;code&gt;.deb&lt;/code&gt; package for your architecture (e.g., &lt;code&gt;gaiasky_3.6.9_amd64.deb&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install via Package Manager&lt;/strong&gt;&lt;br&gt;
Open a terminal and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dpkg -i gaiasky_*.deb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If dependency errors occur, fix them with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt-get install -f
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Launch Gaia Sky&lt;/strong&gt;&lt;br&gt;
Run the command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gaiasky
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The application will start in your default browser or terminal window.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is not bad. I did not specify the distribution, so it just assumed Debian. The instructions themselves look fine. Still, I wanted to see if it had absorbed the knowledge for other distros, so I asked specifically for Arch Linux:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: How do I install it on Arch Linux specifically?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A: To install Gaia Sky on &lt;strong&gt;Arch Linux&lt;/strong&gt;, you have two primary options depending on whether you prefer the official repositories or building from source:&lt;/p&gt;
&lt;h4 id="option-1-using-the-official-aur-recommended"&gt;Option 1: Using the Official AUR (Recommended)&lt;/h4&gt;
&lt;p&gt;The most straightforward method is via the &lt;a href="https://aur.archlinux.org/"&gt;Arch User Repository (AUR)&lt;/a&gt;. You will need an AUR helper like &lt;code&gt;yay&lt;/code&gt;, &lt;code&gt;paru&lt;/code&gt;, or &lt;code&gt;pamac&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install via AUR Helper&lt;/strong&gt;:
Open your terminal and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;yay -S gaiasky
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# OR using paru&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;paru -S gaiasky
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will automatically resolve dependencies (Java, OpenGL libraries) and install the application.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Verify Installation&lt;/strong&gt;:
Once installed, launch it with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gaiasky
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="option-2-building-from-source"&gt;Option 2: Building from Source&lt;/h4&gt;
&lt;p&gt;If you prefer to compile Gaia Sky yourself or need a specific version not yet in the AUR:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clone the Repository&lt;/strong&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;git clone https://codeberg.org/gaiasky/gaiasky.git
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;cd&lt;/span&gt; gaiasky
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Build and Install&lt;/strong&gt;:
Use Maven to build the project (requires Java Development Kit):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mvn clean package -DskipTests
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After building, you can run it directly from the &lt;code&gt;target&lt;/code&gt; directory or install it using the provided Gradle wrapper if available in newer versions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="post-installation-notes-for-arch"&gt;Post-Installation Notes for Arch&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;: Ensure your system has up-to-date OpenGL drivers and Java (JDK 17+ is recommended). The AUR package typically handles these automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Location&lt;/strong&gt;: By default, datasets are stored in &lt;code&gt;~/.local/share/gaiasky/&lt;/code&gt;. You may want to create symbolic links from &lt;code&gt;/opt/gaiasky/data&lt;/code&gt; if you prefer system-wide data management.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="additional-notes"&gt;Additional Notes&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Ensure you have at least 4GB of free disk space for datasets and cache files.&lt;/li&gt;
&lt;li&gt;For GPU acceleration, verify that your graphics drivers support OpenGL 4.3+.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Very good response. It recommends using the AUR package (correct), and also provides instructions to build and run it from source (also correct). However, it hallucinates the use of Maven instead of Gradle.&lt;/p&gt;
&lt;p&gt;The base model produced a long wall of text with mostly nonsense that would look plausible to the non-expert. In general, the responses from the base model were much longer and their content was very often factually wrong.&lt;/p&gt;
&lt;p&gt;The next question is a bit more technical.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: How are cubemap textures used in Gaia Sky?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A: Cubemaps in Gaia Sky serve a specific purpose related to &lt;strong&gt;planetary rendering&lt;/strong&gt; rather than general environment mapping for the entire scene. They are primarily utilized to generate high-resolution, seamless spherical textures for planets (like Earth or Mars) by stitching together six square faces into a continuous surface. This technique allows for detailed texture representation without the distortion often seen in standard equirectangular projections near poles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Technical Details:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application&lt;/strong&gt;: Used specifically for planet textures where high fidelity is required across all latitudes and longitudes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structure&lt;/strong&gt;: Consists of six square images (front, back, left, right, top, bottom) that are algorithmically mapped onto a sphere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rendering Engine&lt;/strong&gt;: The underlying rendering engine uses these cubemap textures to apply surface details dynamically as the planet rotates or changes perspective.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This feature is distinct from standard environment maps used for sky reflections or star glow effects, focusing instead on accurate planetary visualization.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is excellent, even though it seems to imply that cubemaps are not used for environment maps and reflections, which is incorrect. However, it correctly identifies that they are mainly used to avoid distortions at the poles. Maybe it increased the importance of it because there is an entire documentation section devoted to cubemap textures in planets.&lt;/p&gt;
&lt;p&gt;The base model produced a long, Markdown-formatted, pretentious output that even included totally made-up scripts. Again, the fine-tuned response was much more grounded and correct.&lt;/p&gt;
&lt;p&gt;Now, onto the scripting questions. Here is where the fine-tuned model starts to hesitate and invent slightly. Here are the possible reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Low parameter count not being able to retain the information properly.&lt;/li&gt;
&lt;li&gt;Very low number of test and showcase scripts used in fine-tuning.&lt;/li&gt;
&lt;li&gt;Bad design of the training dataset.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I personally think it has more to do with the first two reasons than the third.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Q: How do I get the camera position with the APIv1 and scripting in Gaia Sky?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A: To retrieve the current camera position using the &lt;strong&gt;Legacy API (v1)&lt;/strong&gt;, you call &lt;code&gt;getPosition()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;from&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;py4j.java_gateway&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;JavaGateway&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;gateway&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;JavaGateway&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;gs&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;gateway&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;entry_point&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;gs&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;getPosition&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;print&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;pos&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[0.0, 0.0, 0.0]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;p&gt;For the &lt;strong&gt;Modern Component API (v2)&lt;/strong&gt;, you use &lt;code&gt;camera.get_position()&lt;/code&gt; from the &lt;code&gt;Camera&lt;/code&gt; module:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;from&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;py4j.java_gateway&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;JavaGateway&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;gateway&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;JavaGateway&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;gs&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;gateway&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;entry_point&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;from&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;gaiasky.camera&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Camera&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;cam&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Camera&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;gs&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cam&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;get_position&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;print&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;pos&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[0.0, 0.0, 0.0]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;p&gt;First, it invented &lt;code&gt;getPosition()&lt;/code&gt; (correct is &lt;code&gt;getCameraPosition()&lt;/code&gt;) for APIv1. For APIv2, it correctly used &lt;code&gt;camera.get_position()&lt;/code&gt;, but the way it gets the &lt;code&gt;camera&lt;/code&gt; object is incorrect. It should have done something like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;apiv2&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;gateway&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;entry_point&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;apiv2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;cam&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;apiv2&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;camera&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cam&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;get_position&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So, I think either this model is too small to be effectively fine-tuned for Gaia Sky scripting, or the training dataset is insufficient to achieve sufficient retention. Could also be both, as I said above.&lt;/p&gt;
&lt;p&gt;The base model has no idea about Gaia Sky scripting or anything related to it, so it just makes stuff up. Not even worth further mention.&lt;/p&gt;
&lt;h2 id="performance"&gt;Performance&lt;/h2&gt;
&lt;p&gt;I used the &lt;a href="#laptop2"&gt;Laptop 2&lt;/a&gt; described above for testing and inference, with 28/32 layers on the GPU and a context window of ~4k, and I consistently got about 12 tok/s. Performance is exactly the same as with the base models, so this section is this short.&lt;/p&gt;
&lt;h2 id="conclusions"&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;This fine-tuning experiment has yielded valuable insights into the strengths and limitations of domain-specific model adaptation at lower parameter counts for local use.&lt;/p&gt;
&lt;p&gt;I think the foundational approach was sound. The dataset curation process, with its surgical filtering, teacher-based distillation, and rigorous sanitization, successfully encoded domain knowledge into the model. Proof of this is evident in the testing: the fine-tuned model, as opposed to the base one, correctly answered conceptual and documentation-heavy questions about Gaia Sky&amp;rsquo;s purpose, installation, and rendering techniques without hallucinating. It understood architectural details like LOD and cubemaps, and avoided inventing features that don&amp;rsquo;t exist. This demonstrates that fine-tuning can be an effective alternative to RAG for teaching models about a specific domain.&lt;/p&gt;
&lt;p&gt;However, it also struggled. The 9B model hit a hard ceiling when it came to API scripting and method names. It invented &lt;code&gt;getPosition()&lt;/code&gt; instead of &lt;code&gt;getCameraPosition()&lt;/code&gt;, misunderstood how to instantiate APIv2 objects, and generally lacked the capacity to reliably retain the specific, syntactic details of the API surface. This is a classic problem: smaller models can absorb &lt;em&gt;concepts&lt;/em&gt; and &lt;em&gt;documentation&lt;/em&gt;, but struggle to memorize exact function signatures and usage patterns. With only 3,800+ training pairs and a 9B parameter budget, the model simply didn&amp;rsquo;t have enough capacity to encode both general knowledge &lt;em&gt;and&lt;/em&gt; precise API details.&lt;/p&gt;
&lt;p&gt;So, what are the next steps? I believe the 4B and 9B models are too small for reliable Gaia Sky scripting assistance. My next experiment will be to &lt;strong&gt;fine-tune the Qwen 3.5 27B model&lt;/strong&gt;. The jump from 9B to 27B parameters should provide substantially more capacity to encode API signatures without sacrificing general knowledge. Additionally, I could increase the scripting dataset by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Expanding the test and showcase scripts used in Type D training.&lt;/li&gt;
&lt;li&gt;Generating more synthetic scripting examples in Type A (API reference) with better coverage of edge cases and parameter variations.&lt;/li&gt;
&lt;li&gt;Adding adversarial examples (Type C) for the most commonly confused API patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That said, the hardware constraint is real. 27B requires more than my RTX 5080 can reasonably handle for full fine-tuning. However, with careful quantization (using 8-bit optimizers or even lower precision), 4-bit LoRA, and possibly gradient checkpointing, it may fit. If not, a cloud provider like Lambda Labs or Paperspace might be the way forward for a single training run.&lt;/p&gt;
&lt;p&gt;All in all, I think fine-tuning is a viable path for building domain-expert models, but it requires the right balance of dataset quality, model size, and hardware. For Gaia Sky specifically, a 27B model with a more robust scripting dataset would likely be the sweet spot before considering the jump to 70B+ models. I consider the infrastructure proven. It&amp;rsquo;s now a matter of scale.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Low-Rank Adaptation is a fine-tuning technique that enables large models to gain additional domain knowledge and adapt to specific tasks with minimal computational and memory overhead. Recommended reading: &lt;a href="https://arxiv.org/abs/2106.09685"&gt;LoRA: Low-Rank Adaptation of Large Language Models&lt;/a&gt;, by E. J. Hu et. al.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>7Artisans EF-FX adapter review</title><link>https://tonisagrista.com/blog/2026/7artisans-ef-fx/</link><pubDate>Tue, 10 Mar 2026 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2026/7artisans-ef-fx/</guid><description>&lt;p&gt;A while back I &lt;a href="https://tonisagrista.com/blog/2021/viltrox-ef-fx1-review"&gt;reviewed the Viltrox EF-FX1 adapter&lt;/a&gt;, my first attempt at bridging my old Canon EF and EF-S lenses over to my Fujifilm X-S10. The abridged version of that experience: it was frustrating. Firmware roulette, random errors, camera freezes requiring battery pulls, and auto focus performance that varied wildly between versions. I kept it because it was &lt;em&gt;just&lt;/em&gt; good enough, but it never stopped feeling like a workaround rather than a solution. Moreover, Viltrox never released any further firmware versions, so the 2.29 blob tested in that post really is the &lt;em&gt;last&lt;/em&gt; firmware.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;a href="https://tonisagrista.com/img/photography/ef-fx-7artisans.avif"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/photography/ef-fx-7artisans.avif" width="60%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;/a&gt;&lt;figcaption style="margin: 0 auto; width:60%;"&gt;
&lt;h4&gt;The 7Artisans EF-FX lens adapter. It is a good and cheap solution to use your old EF/EF-S lenses on your Fuji X-mount camera.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Fast forward to recently, and I decided to give the whole thing another shot. This time with the &lt;a href="https://7artisans.store/products/7artisans-ef-fx-lens-mount-adapter-with-auto-exposure-auto-focus-canon-ef-ef-s-lens-to-canon-eos-r-mirrorlescamera"&gt;&lt;strong&gt;7Artisans EF-FX adapter&lt;/strong&gt;&lt;/a&gt;. Same concept: an electronic adapter with auto focus support, aperture control, and EXIF data transmission, and at 119€ it sits right in the same price bracket as the Viltrox. But is it actually better? Spoiler: yes. Let me walk you through it.&lt;/p&gt;
&lt;h2 id="lenses-and-body"&gt;Lenses and body&lt;/h2&gt;
&lt;p&gt;Same test bench as last time, same lenses, same body — in the interest of a fair comparison:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Body&lt;/strong&gt;: Fujifilm X-S10&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lenses&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Canon EF 28-105mm f/3.5-4.5 USM II&lt;/li&gt;
&lt;li&gt;Canon EF 50mm f/1.8 II&lt;/li&gt;
&lt;li&gt;Sigma 10-20mm f/4-5.6 EX DC HSM&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="build-quality"&gt;Build quality&lt;/h2&gt;
&lt;p&gt;The adapter feels solid. The frame is all metal and has a reassuringly substantial weight to it. It is not heavy enough to unbalance a lens, but enough to communicate that it is not going to crack if you look at it wrong. Canon lenses mount securely, the Fuji X end clicks into the body with no play whatsoever, and the whole assembly feels like a single unit once locked. There is no wobble, no flex, no creaking.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;a href="https://tonisagrista.com/img/photography/ef-fx-comparison-angle.avif"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/photography/ef-fx-comparison-angle.avif" width="60%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;/a&gt;&lt;figcaption style="margin: 0 auto; width:60%;"&gt;
&lt;h4&gt;The Viltrox EF-FX (left) and the 7Artisans EF-FX (right). They look and feel very similar from the outside.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This one also has a Micro-USB port for firmware updates. Why is it using Micro-USB instead of USB-C in 2026? No fu***** idea. It is a bummer. The adapter itself is, however, better built than I expected at this price point.&lt;/p&gt;
&lt;h2 id="auto-focus-performance"&gt;Auto focus performance&lt;/h2&gt;
&lt;p&gt;This is where things get interesting, especially if you read the &lt;a href="https://tonisagrista.com/blog/2021/viltrox-ef-fx1-review"&gt;Viltrox review&lt;/a&gt; and remember the table of doom I put together there.&lt;/p&gt;
&lt;p&gt;With the 7Artisans adapter, I did not need a table. In good lighting conditions, auto focus with all three lenses is &lt;strong&gt;fast and reliable&lt;/strong&gt;. The Canon 50mm f/1.8 II in particular snaps to focus quickly and confidently. The difference compared to the Viltrox (on most firmware versions) is night and day. The 28-105mm, which was the most problematic lens with the Viltrox, also performs well: focus acquisition is smooth and I have not once seen the dreaded &amp;ldquo;TURN OFF THE CAMERA AND TURN ON AGAIN&amp;rdquo; message. Not once.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;a href="https://tonisagrista.com/img/photography/ef-fx-comparison.avif"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/photography/ef-fx-comparison.avif" width="60%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;/a&gt;&lt;figcaption style="margin: 0 auto; width:60%;"&gt;
&lt;h4&gt;The Viltrox EF-FX (left) and the 7Artisans EF-FX attached to the Canon 50mm f/1.8 II (right). The lens snaps into the adapter in an oddly satisfying way.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The Sigma 10-20mm is similarly well-behaved. At wide angles there is slightly more hunting in ambiguous scenes, but that is expected behaviour for any wide-angle lens and not something I&amp;rsquo;d pin on the adapter.&lt;/p&gt;
&lt;p&gt;The caveat is &lt;strong&gt;low light&lt;/strong&gt;. Drop the ambient light and things become noticeably more hit or miss. The adapter does not handle low-contrast, low-light scenes gracefully; focus will hunt, and sometimes give up altogether, or snap to a comically out-of-focus setting. This is not a dealbreaker for me, but if you shoot a lot of available-light indoor work or night photography with these lenses, temper your expectations accordingly. I know what to expect of these kind of cheap adapters, and this one is enough.&lt;/p&gt;
&lt;h2 id="no-firmware-archaeology-required"&gt;No firmware archaeology required&lt;/h2&gt;
&lt;p&gt;One of the most annoying aspects of the Viltrox experience was the firmware situation. Digging up old versions, flashing them one by one, and hoping for the best. I am happy to report that with the 7Artisans I simply mounted it and it worked. I updated to the latest firmware once, without incident, and have had no reason to go looking for alternatives. The update process was painless.&lt;/p&gt;
&lt;h2 id="aperture-control-and-exif"&gt;Aperture control and EXIF&lt;/h2&gt;
&lt;p&gt;Both work as expected. Aperture adjusts from the camera body across all three lenses, and EXIF data (focal length, aperture, lens identification) seem to be written correctly to files. Nothing surprising here, but it is worth confirming since these features were also present on the Viltrox. The adapter itself has no controls or buttons other than the lens release lever.&lt;/p&gt;
&lt;h2 id="wrap-up"&gt;Wrap-up&lt;/h2&gt;
&lt;p&gt;After the Viltrox experience, I was not sure a third-party EF-to-FX adapter could actually work well. I ordered the 7Artisans off Amazon fully ready to return it if it did not perform. However, it has changed my mind. It is not perfect (low light auto focus is genuinely unreliable), but in normal shooting conditions it is a solid, no-fuss solution that lets me keep using lenses I like on a camera I enjoy shooting with. There are no crashes, no firmware gymnastics, no error messages demanding I restart the camera mid-session.&lt;/p&gt;
&lt;p&gt;If you are coming from the Canon ecosystem and moving to Fuji X, and you want to bring your glass with you without spending much native-lens money, the 7Artisans EF-FX adapter is worth a serious look. For me, it is the upgrade the Viltrox never quite managed to be. Moreover, there is a world of old, cheap EF and EF-S lenses in second-hand markets that only get more attractive with time.&lt;/p&gt;</description></item><item><title>GGUF quantization guide</title><link>https://tonisagrista.com/blog/2026/quantization/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><author>Toni Sagristà Sellés</author><guid>https://tonisagrista.com/blog/2026/quantization/</guid><description>&lt;p&gt;I like running my own LLMs locally. Open models are becoming more and more powerful, with exciting releases like the latest Qwen 3.5 family scoring highly in benchmarks even in their smaller variants. This makes managing and running your own models more viable, as it becomes increasingly easy to repurpose old hardware for local inference with progressively better results. For local users and modest purposes, the GGUF format introduced by llama.cpp is the de-facto default.&lt;/p&gt;
&lt;p&gt;Since local inference is typically heavily restricted by the available hardware, several optimization techniques have been implemented to make the models leaner and faster. Perhaps the most important of these is &lt;strong&gt;quantization&lt;/strong&gt;, which trims down the bit count per parameter to achieve lower memory usage and (sometimes) faster inference&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. The challenge is that there are many different formats and strategies for quantization. In this post, I summarize them, providing a bird&amp;rsquo;s-eye view on the available techniques, their strengths, and their weaknesses.&lt;/p&gt;
&lt;h2 id="naming-conventions"&gt;Naming conventions&lt;/h2&gt;
&lt;p&gt;Most GGUF quantization names follow this pattern: &lt;strong&gt;&lt;code&gt;Q{bits}{method}{size}&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Quantized format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2/3/4/5/6/8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bits per weight (lower = smaller file, more compression)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;K&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;K-quant&amp;rdquo;: grouped/blockwise quantization with per-group scales&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;I&lt;/code&gt; / &lt;code&gt;IQ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&amp;ldquo;I-quant&amp;rdquo;: importance-matrix/non-linear quantization for aggressive compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt; / &lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Legacy ungrouped formats (symmetric/asymmetric)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_S&lt;/code&gt; / &lt;code&gt;_M&lt;/code&gt; / &lt;code&gt;_L&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Small/Medium/Large: precision mix across tensor types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_XS&lt;/code&gt; / &lt;code&gt;_XXS&lt;/code&gt; / &lt;code&gt;_NL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra-small / Non-linear variants for I-quants&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="unquantized-formats"&gt;Unquantized formats&lt;/h2&gt;
&lt;p&gt;These are base formats with no compression. Usually, but not always, models are trained in these formats.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Bits&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FP32&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32-bit float&lt;/td&gt;
&lt;td&gt;Full precision, ~26GB for 7B model&lt;/td&gt;
&lt;td&gt;Research, debugging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FP16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16-bit float&lt;/td&gt;
&lt;td&gt;Half precision, ~13GB for 7B model&lt;/td&gt;
&lt;td&gt;GPU training/inference baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BF16&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;16-bit brain float&lt;/td&gt;
&lt;td&gt;Same size as FP16, better dynamic range for training&lt;/td&gt;
&lt;td&gt;Training on modern GPUs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These preserve maximum accuracy but require significant VRAM. Most local users quantize to reduce size by 75-90%.&lt;/p&gt;
&lt;h2 id="legacy-quantization-formats-q_0-q_1"&gt;Legacy quantization formats (&lt;code&gt;Q*_0&lt;/code&gt;, &lt;code&gt;Q*_1&lt;/code&gt;)&lt;/h2&gt;
&lt;p&gt;Simple per-block linear quantization. Fast but less accurate at low bits.&lt;/p&gt;
&lt;p&gt;Following is a table that contains the format name, number of bits, size (for a 7B model), perplexity, and some notes/recommendations. The &lt;strong&gt;perplexity&lt;/strong&gt; represents the difference between the quantized model and the base model, with lower scores indicating better accuracy and less uncertainty.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Bits&lt;/th&gt;
&lt;th&gt;Size (7B)&lt;/th&gt;
&lt;th&gt;Perplexity \(\Delta\)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q8_0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~8-bit&lt;/td&gt;
&lt;td&gt;~6.7 GB&lt;/td&gt;
&lt;td&gt;+0.0004&lt;/td&gt;
&lt;td&gt;Near-lossless; safe INT8 baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q5_1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5-bit&lt;/td&gt;
&lt;td&gt;~4.7 GB&lt;/td&gt;
&lt;td&gt;+0.0415&lt;/td&gt;
&lt;td&gt;Legacy; superseded by Q5_K_M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q5_0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5-bit&lt;/td&gt;
&lt;td&gt;~4.3 GB&lt;/td&gt;
&lt;td&gt;+0.0796&lt;/td&gt;
&lt;td&gt;Legacy; balanced but outdated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q4_1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4-bit&lt;/td&gt;
&lt;td&gt;~3.9 GB&lt;/td&gt;
&lt;td&gt;+0.1846&lt;/td&gt;
&lt;td&gt;Legacy; substantial quality loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q4_0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4-bit&lt;/td&gt;
&lt;td&gt;~3.5 GB&lt;/td&gt;
&lt;td&gt;+0.2499&lt;/td&gt;
&lt;td&gt;Legacy; high quality loss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;K-quants or I-quants are generally preferred over legacy formats at \(\leq 5\) bits. Q8_0 remains useful for compatibility.&lt;/p&gt;
&lt;h2 id="k-quant-formats-modern-default"&gt;K-Quant formats (modern default)&lt;/h2&gt;
&lt;p&gt;Use two-level block quantization (small blocks \(\rightarrow\) super-blocks) with double-quantized scales for better quality-per-bit.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Effective Bits&lt;/th&gt;
&lt;th&gt;Size (7B)&lt;/th&gt;
&lt;th&gt;Perplexity \(\Delta\)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q6_K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~6.0&lt;/td&gt;
&lt;td&gt;~5.15 GB&lt;/td&gt;
&lt;td&gt;+0.0044&lt;/td&gt;
&lt;td&gt;&amp;ldquo;Almost lossless&amp;rdquo; with savings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q5_K_L&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5.3&lt;/td&gt;
&lt;td&gt;~4.6 GB&lt;/td&gt;
&lt;td&gt;+0.010&lt;/td&gt;
&lt;td&gt;High-quality 5-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q5_K_M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5.1&lt;/td&gt;
&lt;td&gt;~4.45 GB&lt;/td&gt;
&lt;td&gt;+0.0142&lt;/td&gt;
&lt;td&gt;Recommended high-quality 5-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q5_K_S&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4.9&lt;/td&gt;
&lt;td&gt;~4.33 GB&lt;/td&gt;
&lt;td&gt;+0.0353&lt;/td&gt;
&lt;td&gt;Recommended balanced 5-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q4_K_L&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4.7&lt;/td&gt;
&lt;td&gt;~4.0 GB&lt;/td&gt;
&lt;td&gt;+0.040&lt;/td&gt;
&lt;td&gt;Relaxed 4-bit mix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q4_K_M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4.5&lt;/td&gt;
&lt;td&gt;~3.80 GB&lt;/td&gt;
&lt;td&gt;+0.0535&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Most popular default&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q4_K_S&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4.3&lt;/td&gt;
&lt;td&gt;~3.56 GB&lt;/td&gt;
&lt;td&gt;+0.1149&lt;/td&gt;
&lt;td&gt;Smaller 4-bit, more loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q3_K_L&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3.5&lt;/td&gt;
&lt;td&gt;~3.35 GB&lt;/td&gt;
&lt;td&gt;+0.1803&lt;/td&gt;
&lt;td&gt;Aggressive 3-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q3_K_M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3.3&lt;/td&gt;
&lt;td&gt;~3.06 GB&lt;/td&gt;
&lt;td&gt;+0.2437&lt;/td&gt;
&lt;td&gt;Balanced 3-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q3_K_S&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3.1&lt;/td&gt;
&lt;td&gt;~2.75 GB&lt;/td&gt;
&lt;td&gt;+0.5505&lt;/td&gt;
&lt;td&gt;Very small, high loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Q2_K&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2.5&lt;/td&gt;
&lt;td&gt;~2.67 GB&lt;/td&gt;
&lt;td&gt;+0.8698&lt;/td&gt;
&lt;td&gt;Extreme compression, not recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Q4_K_M&lt;/code&gt;&lt;/strong&gt; seems to be the community sweet spot, as it offers \(\sim 75\%\) size reduction with minimal noticeable quality loss for most tasks.&lt;/p&gt;
&lt;h2 id="i-quant-formats-aggressive-compression"&gt;I-Quant formats (aggressive compression)&lt;/h2&gt;
&lt;p&gt;IQs use non-linear reconstruction, lookup tables, and importance-matrix calibration for maximum quality at very low bit counts. They trade decoding speed for size.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Effective Bits&lt;/th&gt;
&lt;th&gt;Size (7B)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ4_NL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4.5&lt;/td&gt;
&lt;td&gt;~3.9 GB&lt;/td&gt;
&lt;td&gt;Non-linear 4-bit; CPU-friendly speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ4_XS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~4.25&lt;/td&gt;
&lt;td&gt;~3.7 GB&lt;/td&gt;
&lt;td&gt;Best quality/size at 4-bit; slightly slower decode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ3_M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3.6&lt;/td&gt;
&lt;td&gt;~3.2 GB&lt;/td&gt;
&lt;td&gt;High-quality 3-bit I-quant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ3_S&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3.4&lt;/td&gt;
&lt;td&gt;~3.0 GB&lt;/td&gt;
&lt;td&gt;Balanced 3-bit aggressive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ3_XS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3.2&lt;/td&gt;
&lt;td&gt;~2.8 GB&lt;/td&gt;
&lt;td&gt;Very small 3-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ3_XXS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3.0&lt;/td&gt;
&lt;td&gt;~2.6 GB&lt;/td&gt;
&lt;td&gt;Extreme 3-bit compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ2_M&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2.7&lt;/td&gt;
&lt;td&gt;~2.4 GB&lt;/td&gt;
&lt;td&gt;High-quality 2-bit (rare)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ2_S&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2.5&lt;/td&gt;
&lt;td&gt;~2.2 GB&lt;/td&gt;
&lt;td&gt;Aggressive 2-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ2_XS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2.3&lt;/td&gt;
&lt;td&gt;~2.0 GB&lt;/td&gt;
&lt;td&gt;Very aggressive 2-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IQ2_XXS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~2.1&lt;/td&gt;
&lt;td&gt;~1.9 GB&lt;/td&gt;
&lt;td&gt;Extreme; significant quality loss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I-quants require &lt;strong&gt;importance matrix (imatrix) calibration&lt;/strong&gt; during quantization for best results. Without it, quality can degrade noticeably.&lt;/p&gt;
&lt;h2 id="special-formats"&gt;Special formats&lt;/h2&gt;
&lt;p&gt;There are even more aggressive quantization methods, like ternary quantization, which converts weights to ternary values.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Bits&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TQ1_0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1.6&lt;/td&gt;
&lt;td&gt;Ternary quantization \(\{-1, 0, +1\}\), for massive models like DeepSeek where fitting in VRAM is critical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="decision-guide"&gt;Decision guide&lt;/h2&gt;
&lt;p&gt;Below I attempt to provide some general rules as to what quant to choose in certain situations.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Best balance overall \(\rightarrow\) &lt;code&gt;Q4_K_M&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Max quality, still compressed \(\rightarrow\) &lt;code&gt;Q5_K_M&lt;/code&gt; or &lt;code&gt;Q6_K&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tight VRAM, acceptable quality loss \(\rightarrow\) &lt;code&gt;Q4_K_S&lt;/code&gt; or &lt;code&gt;IQ4_XS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Max compression \(\rightarrow\) &lt;code&gt;IQ3_XS&lt;/code&gt; or &lt;code&gt;Q3_K_S&lt;/code&gt; \(\Delta\) &lt;span class="red"&gt;test quality!&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;Near-lossless accuracy \(\rightarrow\) &lt;code&gt;Q8_0&lt;/code&gt; or keep &lt;code&gt;FP16&lt;/code&gt;/&lt;code&gt;BF16&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CPU inference \(\rightarrow\) &lt;code&gt;Q4_K_M&lt;/code&gt; or &lt;code&gt;IQ4_NL&lt;/code&gt; \(\because\) &lt;span class="orange"&gt;better decode speed&lt;/span&gt;
&lt;/li&gt;
&lt;li&gt;Fit a ~50B model on 16GB VRAM \(\rightarrow\) &lt;code&gt;IQ3_S&lt;/code&gt; or &lt;code&gt;Q3_K_M&lt;/code&gt; \(+\) CPU offload&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusions"&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;The set of quantization methods available may look like the wild west at first glance, but this mess is not without some order. There are always good reasons behind every quantization type. Here are some takeaways:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Bits \(\neq\) quality alone&lt;/strong&gt;: A well-designed 4-bit format (&lt;code&gt;Q4_K_M&lt;/code&gt;, &lt;code&gt;IQ4_XS&lt;/code&gt;) can outperform a naive 5-bit legacy format.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suffixes matter&lt;/strong&gt;: &lt;code&gt;_M&lt;/code&gt; variants selectively keep sensitive layers at higher precision, improving quality with minimal size cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware matters&lt;/strong&gt;: I-quants compress more but decode slower on CPUs; K-quants often give better tokens/sec on consumer hardware.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calibration helps&lt;/strong&gt;: Models quantized with an importance matrix (imatrix) retain more accuracy, especially at \(\leq 3\) bits.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test your use case&lt;/strong&gt;: Perplexity benchmarks are only guides. Always validate outputs for your specific tasks.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For the latest format support and benchmarks, check the &lt;a href="https://github.com/ggerganov/llama.cpp"&gt;llama.cpp repository&lt;/a&gt; or community hubs like Hugging Face, where curators like &lt;a href="https://huggingface.co/bartowski"&gt;bartowski&lt;/a&gt; and &lt;a href="https://huggingface.co/unsloth"&gt;Unsloth&lt;/a&gt; publish tested GGUF variants.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;For an exceptionally good (visual) guide as to how quantization is actually performed, I recommend reading this &lt;em&gt;ngrok&lt;/em&gt; blog post: &lt;a href="https://ngrok.com/blog/quantization"&gt;Quantization from the ground up&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;A good technical explanation of perplexity is in &lt;a href="https://ngrok.com/blog/quantization#perplexity"&gt;this section of this &lt;em&gt;ngrok&lt;/em&gt; post&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;em&gt;C. Zhu and S. Han and H. Mao and W. J. Dally&lt;/em&gt;. &amp;ldquo;Trained Ternary Quantization.&amp;rdquo; &lt;em&gt;arXiv preprint&lt;/em&gt; &lt;a href="https://arxiv.org/abs/1612.01064"&gt;arXiv:1612.01064&lt;/a&gt; (2016).&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>LM Studio on systemd linger</title><link>https://tonisagrista.com/blog/2026/lmster-service/</link><pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate><author>Toni Sagristà Sellés</author><guid>https://tonisagrista.com/blog/2026/lmster-service/</guid><description>&lt;p&gt;The release of &lt;strong&gt;LM Studio 0.4.5&lt;/strong&gt; has introduced a much needed feature in this local LLM suite that has it much more attractive with respect to other similar projects. &lt;a href="https://lmstudio.ai/docs/lmlink"&gt;LM Link&lt;/a&gt; allows you to connect multiple LM Studio instances across your network to share models and perform inference seamlessly.&lt;/p&gt;
&lt;p&gt;By sheer chance, I was just playing around with setting up an LM Studio server in an old laptop that I planned to use for inference. I would connect AnythingLLM clients to it to make API requests. The timing of 0.4.5 was perfect for me, as I could now use LM Studio for inference directly, and forget about using up my own Tailscale network. But some setup was needed in the laptop. To make this work effectively, the LM Studio server needs to run in the background, start automatically on boot, and persist even when I&amp;rsquo;m not logged in.&lt;/p&gt;
&lt;p&gt;The LM Studio website provides the source of a &lt;a href="https://lmstudio.ai/docs/developer/core/headless_llmster#create-systemd-service"&gt;service file&lt;/a&gt;. It suggests creating it as a system-wide service, which is weird, as the default installation method (at least on Linux) sets everything up in the user home directory. I modified it a bit to make things clean, as I want this to be a user &lt;code&gt;systemd&lt;/code&gt; service. It keeps the process tied to your user environment but, with a little tweak called lingering, allows it to run without an active SSH or GUI session. Here is the setup.&lt;/p&gt;
&lt;p&gt;By default, user services stop the moment the user logs out. To prevent this and allow the LM Studio daemon to start at boot and stay alive, run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;loginctl enable-linger &lt;span style="color:#58a1dd"&gt;$USERNAME&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, create a directory for your user services if it doesn&amp;rsquo;t exist:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir -p ~/.config/systemd/user/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After that, create a file named &lt;code&gt;lms.service&lt;/code&gt; in that directory (&lt;code&gt;~/.config/systemd/user/lms.service&lt;/code&gt;), with the following contents:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#58a1dd"&gt;Unit&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;Description&lt;/span&gt;=&lt;span style="color:#58a1dd"&gt;LM&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Studio&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;After&lt;/span&gt;=&lt;span style="color:#58a1dd"&gt;network&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#58a1dd"&gt;Service&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;Type&lt;/span&gt;=&lt;span style="color:#58a1dd"&gt;forking&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# This line kills any existing lms processes to prevent the &amp;#34;left-over&amp;#34; error&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;ExecStartPre&lt;/span&gt;=&lt;span style="color:#58a1dd"&gt;-&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;home&lt;/span&gt;/$&lt;span style="color:#58a1dd"&gt;USERNAME&lt;/span&gt;/.&lt;span style="color:#58a1dd"&gt;lmstudio&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;bin&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;lms&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;daemon&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;down&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;ExecStartPre&lt;/span&gt;=/&lt;span style="color:#58a1dd"&gt;home&lt;/span&gt;/$&lt;span style="color:#58a1dd"&gt;USERNAME&lt;/span&gt;/.&lt;span style="color:#58a1dd"&gt;lmstudio&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;bin&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;lms&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;daemon&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;up&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;ExecStart&lt;/span&gt;=/&lt;span style="color:#58a1dd"&gt;home&lt;/span&gt;/$&lt;span style="color:#58a1dd"&gt;USERNAME&lt;/span&gt;/.&lt;span style="color:#58a1dd"&gt;lmstudio&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;bin&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;lms&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;server&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;start&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# Give it a specific PID file if lms supports it, otherwise systemd guesses&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;ExecStop&lt;/span&gt;=/&lt;span style="color:#58a1dd"&gt;home&lt;/span&gt;/$&lt;span style="color:#58a1dd"&gt;USERNAME&lt;/span&gt;/.&lt;span style="color:#58a1dd"&gt;lmstudio&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;bin&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;lms&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;server&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;stop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;ExecStopPost&lt;/span&gt;=/&lt;span style="color:#58a1dd"&gt;home&lt;/span&gt;/$&lt;span style="color:#58a1dd"&gt;USERNAME&lt;/span&gt;/.&lt;span style="color:#58a1dd"&gt;lmstudio&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;bin&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;lms&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;daemon&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;down&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# Ensure systemd doesn&amp;#39;t get confused by the CLI exiting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;RemainAfterExit&lt;/span&gt;=&lt;span style="color:#58a1dd"&gt;yes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#58a1dd"&gt;Install&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;WantedBy&lt;/span&gt;=&lt;span style="color:#58a1dd"&gt;default&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once the file is saved, tell systemd to reload its configuration and enable the service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# Reload the user daemon&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl --user daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# Enable the service to start on boot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl --user &lt;span style="color:#58a1dd"&gt;enable&lt;/span&gt; lms.service
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# Start the service now&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl --user start lms.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you want to load a specific model by default, add an additional &lt;code&gt;ExecStartPre&lt;/code&gt; line, like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;ExecStartPre&lt;/span&gt;=/&lt;span style="color:#58a1dd"&gt;home&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;YOUR_USERNAME&lt;/span&gt;/.&lt;span style="color:#58a1dd"&gt;lmstudio&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;bin&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;lms&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;load&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;openai&lt;/span&gt;/&lt;span style="color:#58a1dd"&gt;gpt-oss-20b&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;--yes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can check the service status with &lt;code&gt;systemctl --user lms.service&lt;/code&gt;. And that is it. You can now use your old hardware for inference with small local LLMs from any of your other machines.&lt;/p&gt;</description></item><item><title>Yet another architectural update for Play Kid</title><link>https://tonisagrista.com/blog/2026/playkid-update-again/</link><pubDate>Wed, 28 Jan 2026 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2026/playkid-update-again/</guid><description>&lt;p&gt;I finished my previous post on Play Kid, only two days ago, with the following words:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Next, I&amp;rsquo;ll probably think about adding Game Boy Color support, but not before taking some time off from this project.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yeah, this was a lie.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/logo-3x.avif"decoding="async" /&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;p&gt;I have previously written about Play Kid, my Game Boy emulator. &lt;a href="https://tonisagrista.com/blog/2026/playkid"&gt;Here&lt;/a&gt;, I introduced it and talked about the base implementation and the quirks of the Game Boy CPU, PPU, and hardware in general. &lt;a href="https://tonisagrista.com/blog/2026/playkid-update"&gt;Here&lt;/a&gt;, I explained the tech stack update from SDL2 to Rust-native libraries. In that last post, I mentioned the dependency version hell I unwillingly descended into as a result of adopting &lt;a href="https://crates.io/crates/pixels"&gt;&lt;code&gt;pixels&lt;/code&gt;&lt;/a&gt; as my rendering library. This forced me to stay on very old versions of &lt;code&gt;wgpu&lt;/code&gt; and &lt;code&gt;egui&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I want my Game Boy emulator to use the latest crate versions for various reasons, so this could not be. I saw a simple and direct path of update, which consisted on adopting &lt;a href="https://crates.io/crates/eframe"&gt;&lt;code&gt;eframe&lt;/code&gt;&lt;/a&gt; to manage the application life cycle, directly drawing the LCD to a texture, and dropping &lt;code&gt;pixels&lt;/code&gt; altogether.&lt;/p&gt;
&lt;p&gt;One of the things that nagged me about the &lt;code&gt;pixels&lt;/code&gt; crate is that its integration with &lt;code&gt;egui&lt;/code&gt; was kind of lackluster. There was no easy way to render the &lt;code&gt;pixels&lt;/code&gt; frame buffer to an &lt;code&gt;egui&lt;/code&gt; panel, so I had to render it centered inside the window. The immediate mode GUI lived in mostly in &lt;code&gt;Window&lt;/code&gt; widgets on top of it. Unfortunately, these windows occluded the Game Boy LCD. In a proper debugger, you must be able to see the entire LCD plus the debug interface.&lt;/p&gt;
&lt;p&gt;So I dropped &lt;code&gt;pixels&lt;/code&gt; and adopted a render-to-texture approach. In it, you create the LCD texture at the beginning from the &lt;code&gt;egui&lt;/code&gt; context, and then copy the LCD contents to it in the &lt;code&gt;update()&lt;/code&gt; method.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;new&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.) -&amp;gt; &lt;span style="color:#58a1dd"&gt;Self&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Create texture from egui context and Game Boy LCD width and height.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;texture&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;_cc&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;egui_ctx&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;load_texture&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;lcd_screen&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;egui&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;ColorImage&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;new&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#58a1dd"&gt;DISPLAY_WIDTH&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;DISPLAY_HEIGHT&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;vec!&lt;/span&gt;[&lt;span style="color:#58a1dd"&gt;egui&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;Color32&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;BLACK&lt;/span&gt;; &lt;span style="color:#58a1dd"&gt;DISPLAY_WIDTH&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;DISPLAY_HEIGHT&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;egui&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;TextureOptions&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;NEAREST&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Create app struct.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Self&lt;/span&gt; {&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;update&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Render LCD to texture.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;frame_ready&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;size&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; [&lt;span style="color:#58a1dd"&gt;DISPLAY_WIDTH&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;DISPLAY_HEIGHT&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;color_image&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;egui&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;ColorImage&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;from_rgba_unmultiplied&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;size&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;machine&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;memory&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ppu&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;fb_front&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;screen_texture&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#58a1dd"&gt;set&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;color_image&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;egui&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;TextureOptions&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;NEAREST&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With this, we can easily render the texture in &lt;code&gt;egui&lt;/code&gt;&amp;rsquo;s &lt;code&gt;CentralPanel&lt;/code&gt;, and the debug interface in a &lt;code&gt;SidePanel&lt;/code&gt; to the right. This is the result:&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/debug-mode-3.avif" width="80%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:80%;"&gt;
&lt;h4&gt;The debug panel, showing the machine state and a code disassembly.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Some additional tweaks here and there, and the UI looks much more polished and professional in version 0.3.0.&lt;/p&gt;
&lt;p&gt;Version 0.4.0 enables loading ROM files from the UI. Initially I thought about making the cartridge struct optional with &lt;code&gt;Option&amp;lt;Cartridge&amp;gt;&lt;/code&gt;, but this spiraled out of control fast. I found that making the full &lt;code&gt;Machine&lt;/code&gt; (which contains the &lt;code&gt;Memory&lt;/code&gt;, &lt;code&gt;PPU&lt;/code&gt;, &lt;code&gt;APU&lt;/code&gt;, &lt;code&gt;Joystick&lt;/code&gt;, etc.) optional worked much better, as there was only one reference to it in the top-level struct, &lt;code&gt;PlayKid&lt;/code&gt;. And, like so, you can dynamically load ROM files from the UI:&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/open-rom.avif" width="80%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:80%;"&gt;
&lt;h4&gt;Play Kid with the top menu bar and the &amp;lsquo;Open ROM&amp;rsquo; menu entry.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So, what&amp;rsquo;s in the future for Play Kid? Well, there are a couple of features that I&amp;rsquo;d really like to add at some point:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Save states&lt;/strong&gt;&amp;mdash;Currently, Play Kid emulates the SRAM by saving and restoring it from files for supported games. I would like to add saving and restoring the full state of the emulator in what is known as save states. Possibly, the &lt;a href="https://crates.io/crates/serde"&gt;&lt;code&gt;serde&lt;/code&gt;&lt;/a&gt; crate can help with this.&lt;/li&gt;
&lt;li&gt;&lt;abbr title="Game Boy Color"&gt;&lt;strong&gt;GBC&lt;/strong&gt;&lt;/abbr&gt;&amp;mdash;Of course, I would like adding Game Boy Color support. It is not trivial, but also not exceedingly complicated. I never owned a GBC, so I&amp;rsquo;d see this as a good opportunity to explore its game catalog.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Game Boy emulator tech stack update</title><link>https://tonisagrista.com/blog/2026/playkid-update/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2026/playkid-update/</guid><description>&lt;div class="hint"&gt;
&lt;div class="hint-title"&gt;&lt;i class="fa fa-info-circle"&gt;&lt;/i&gt;&amp;nbsp;&amp;nbsp;&lt;strong&gt;Notice&lt;/strong&gt;&lt;/div&gt;
&lt;div class="hint-body"&gt;
This post is about Play Kid version &lt;code&gt;0.2.0&lt;/code&gt;, which uses &lt;code&gt;pixels&lt;/code&gt; to create and manage the pixel frame buffer. Since then, versions &lt;code&gt;0.3.0&lt;/code&gt; and &lt;code&gt;0.4.0&lt;/code&gt; have been released, which dropped &lt;code&gt;pixels&lt;/code&gt; in favor of rendering directly to a texture. See &lt;a href="https://tonisagrista.com/blog/2026/playkid-update-again"&gt;this write-up&lt;/a&gt; for more information.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In my &lt;a href="https://tonisagrista.com/blog/2026/playkid/"&gt;previous post&lt;/a&gt;, I shared the journey of building &lt;a href="https://tonisagrista.com/projects/playkid"&gt;&lt;strong&gt;Play Kid&lt;/strong&gt;&lt;/a&gt;, my Game Boy emulator. At the time, I was using &lt;strong&gt;SDL2&lt;/strong&gt; to handle the &amp;ldquo;heavy lifting&amp;rdquo; of graphics, audio, and input. This was released as v0.1.0. It worked, and it worked well, but it always felt a bit like a &amp;ldquo;guest&amp;rdquo; in the Rust ecosystem. SDL2 is a C library at heart, and while the Rust wrappers are good, they bring along some baggage like shared library dependencies and difficult integration with Rust-native UI frameworks.&lt;/p&gt;
&lt;p&gt;So I decided to perform a heart transplant on Play Kid. For version v0.2.0 I’ve moved away from SDL2 entirely, replacing it with a stack of modern, native Rust libraries: &lt;a href="https://crates.io/crates/wgpu"&gt;&lt;strong&gt;&lt;code&gt;wgpu&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://crates.io/crates/pixels"&gt;&lt;strong&gt;&lt;code&gt;pixels&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://crates.io/crates/egui"&gt;&lt;strong&gt;&lt;code&gt;egui&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://crates.io/crates/winit"&gt;&lt;strong&gt;&lt;code&gt;winit&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;, &lt;a href="https://crates.io/crates/rodio"&gt;&lt;strong&gt;&lt;code&gt;rodio&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;, and &lt;a href="https://crates.io/crates/gilrs"&gt;&lt;strong&gt;&lt;code&gt;gilrs&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;winit&lt;/code&gt; &amp;amp; &lt;code&gt;pixels&lt;/code&gt;: These handle the windowing and the actual Game Boy frame buffer. &lt;code&gt;pixels&lt;/code&gt; allows me to treat the 160x144 LCD as a simple pixel buffer while &lt;code&gt;wgpu&lt;/code&gt; handles the hardware-accelerated scaling and aspect ratio correction behind the scenes.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;egui&lt;/code&gt;: This was a big step-up. Instead of my minimal homegrown UI library from the SDL2 version, I now have access to a full-featured, immediate-mode GUI. This allowed me to build the debugger I had in mind from the beginning.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rodio&lt;/code&gt; &amp;amp; &lt;code&gt;gilrs&lt;/code&gt;: These replaced SDL2’s audio and controller handling with pure-Rust alternatives that feel much more ergonomic to use alongside the rest of the machine.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="debug-panel"&gt;Debug panel&lt;/h2&gt;
&lt;p&gt;The most visible change is the new &lt;strong&gt;Debug Panel&lt;/strong&gt;.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/debug-mode-2.avif" width="75%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:75%;"&gt;
&lt;h4&gt;The new integrated debugger features a real-time disassembly view and breakpoint management.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;One of the coolest additions is the &lt;strong&gt;Code disassembly&lt;/strong&gt; panel. It decodes the ROM instructions in real-time, highlighting the current &lt;code&gt;PC&lt;/code&gt; and allowing me to toggle breakpoints just by clicking on a line. The breakpoints themselves are now managed in a dedicated list, shown in red at the bottom.&lt;/p&gt;
&lt;p&gt;The rest of the debug panel shows what we already had: the state of the CPU, the PPU, and the joypad.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;video width="75%" poster="/img/playkid/playkid-ui.jpg" preload="none"controls="controls"
&gt;
&lt;source src="https://tonisagrista.com/img/playkid/playkid-ui.mp4" autostart="false"&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;figcaption style="margin: 0 auto; width:75%;"&gt;
&lt;h4&gt;Playing around with the new Play Kid UI based on egui.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="dependency-hell"&gt;Dependency hell&lt;/h2&gt;
&lt;p&gt;Of course, no modern Rust migration is complete without a descent into &lt;strong&gt;dependency hell&lt;/strong&gt;. This new stack comes with a major catch: &lt;code&gt;pixels&lt;/code&gt; is a bit of a picky gatekeeper. Its latest version is 0.15 (January 2025). It is pinned to an older version of &lt;code&gt;wgpu&lt;/code&gt; (0.19 vs the current 28.0), and it essentially freezes the rest of the project in a time capsule.&lt;/p&gt;
&lt;p&gt;To keep the types compatible, I’m forced to stay on &lt;code&gt;egui&lt;/code&gt; 0.26 (current is 0.33) and &lt;code&gt;winit&lt;/code&gt; 0.29 (current is 0.30), even though the rest of the ecosystem has moved on to much newer, shinier versions. It’s kind of frustrating. You get the convenience of the &lt;code&gt;pixels&lt;/code&gt; buffer, but you pay for it by being locked out of the latest API improvements and features. Navigating these version constraints felt like solving a hostage negotiation between crate maintainers. Not very fun.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Despite the dependency issues, I think the project is now in a much better place. The code is cleaner, the debugger is much better, and it’s easier to ship binaries for Linux, Windows, and macOS via GitHub Actions.&lt;/p&gt;
&lt;p&gt;If you’re interested in seeing the new architecture or trying out the new debugger, the code is updated on &lt;a href="https://codeberg.org/langurmonkey/playkid"&gt;Codeberg&lt;/a&gt; and &lt;a href="https://github.com/langurmonkey/playkid"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, I&amp;rsquo;ll probably think about adding Game Boy Color support, but not before taking some time off from this project.&lt;/p&gt;</description></item><item><title>Play Kid</title><link>https://tonisagrista.com/projects/playkid/</link><pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate><guid>https://tonisagrista.com/projects/playkid/</guid><description>&lt;p&gt;&lt;strong&gt;Play Kid&lt;/strong&gt; is yet another Game Boy emulator, written in Rust. But hey, it is MY Game Boy emulator, and I&amp;rsquo;m proud of it. I wrote some words about its development &lt;a href="https://tonisagrista.com/blog/2026/playkid"&gt;here&lt;/a&gt;. Find all posts about Play Kid &lt;a href="https://tonisagrista.com/tags/play-kid"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/logo-3x.avif"decoding="async" /&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/grid.avif" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Play Kid running different games with different color palettes.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Here are the main features of Play Kid:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All CPU instructions implemented.&lt;/li&gt;
&lt;li&gt;Full memory map implemented.&lt;/li&gt;
&lt;li&gt;Modes: ROM, MBC1, MBC2, MBC3.&lt;/li&gt;
&lt;li&gt;Audio is implemented, with 4 channels, envelopes, sweep, and stereo.&lt;/li&gt;
&lt;li&gt;Supports game controllers.&lt;/li&gt;
&lt;li&gt;Multiple color palettes.&lt;/li&gt;
&lt;li&gt;Save screenshot of current frame buffer.&lt;/li&gt;
&lt;li&gt;FPS counter.&lt;/li&gt;
&lt;li&gt;Respects 160:144 aspect ratio by letter-boxing.&lt;/li&gt;
&lt;li&gt;Debug panel:
&lt;ul&gt;
&lt;li&gt;Step instruction.&lt;/li&gt;
&lt;li&gt;Step scanline.&lt;/li&gt;
&lt;li&gt;Pause/continue current execution.&lt;/li&gt;
&lt;li&gt;Displays internal state of CPU, PPU, and Joypad.&lt;/li&gt;
&lt;li&gt;Full program disassembly, with breakpoints.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save RAM to &lt;code&gt;.sav&lt;/code&gt; files to emulate the battery-backed SRAM. Those are saved every minute.&lt;/li&gt;
&lt;li&gt;Working games/roms:
&lt;ul&gt;
&lt;li&gt;Passes &lt;code&gt;dmg-acid2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tetris&lt;/li&gt;
&lt;li&gt;Pokémon&lt;/li&gt;
&lt;li&gt;Super Mario Land&lt;/li&gt;
&lt;li&gt;Super Mario Land 2: 6 Golden Coins&lt;/li&gt;
&lt;li&gt;Wario Land (Super Mario Land 3)&lt;/li&gt;
&lt;li&gt;Wario Land II&lt;/li&gt;
&lt;li&gt;Bugs Bunny Crazy Castle&lt;/li&gt;
&lt;li&gt;The Amazing Spider-Man&lt;/li&gt;
&lt;li&gt;Dr. Mario&lt;/li&gt;
&lt;li&gt;Probably many, many more&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;For &lt;a href="https://codeberg.org/langurmonkey/playkid/releases"&gt;Linux, macOS, and Windows&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="downloads"&gt;Downloads&lt;/h1&gt;
&lt;p&gt;You can grab packages for Linux, macOS, and Windows here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://codeberg.org/langurmonkey/playkid/releases"&gt;Downloads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/langurmonkey/rts-engine"&gt;Mirror (GitHub)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="build"&gt;Build&lt;/h1&gt;
&lt;p&gt;Build the project with &lt;code&gt;cargo build&lt;/code&gt;.&lt;/p&gt;
&lt;h1 id="run"&gt;Run&lt;/h1&gt;
&lt;p&gt;The usual Rust stuff.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cargo run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can also pass in a ROM file with &lt;code&gt;cargo run -- your-rom.gb&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Make the binary with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cargo build --release
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="operation"&gt;Operation&lt;/h1&gt;
&lt;p&gt;If you don&amp;rsquo;t pass in any ROM file as an argument, you need to select using the top menu bar, &lt;kbd&gt;File&lt;/kbd&gt;▶&lt;kbd&gt;Open ROM&amp;hellip;&lt;/kbd&gt;.&lt;/p&gt;
&lt;p&gt;Here are the Joypad keyboard mappings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;kbd&gt;enter&lt;/kbd&gt; - Start button&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;space&lt;/kbd&gt; - Select button&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;a&lt;/kbd&gt; - A button&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;b&lt;/kbd&gt; - B button&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The keyboard is clumsy for playing Game Boy games, so you can use any game controller. Controllers are detected when hot-plugged.&lt;/p&gt;
&lt;p&gt;Additionally, there are some more actions available:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;kbd&gt;p&lt;/kbd&gt; - change the palette colors&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;w&lt;/kbd&gt; - trigger the SRAM save operation to &lt;code&gt;.sav&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;f&lt;/kbd&gt; - toggle FPS monitor&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;s&lt;/kbd&gt; - save a screenshot, with name &lt;code&gt;screenshot_[time].jpg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;d&lt;/kbd&gt; - enter debug mode&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;Esc&lt;/kbd&gt; - exit the emulator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also use the provided UI.&lt;/p&gt;
&lt;h1 id="debug-panel"&gt;Debug panel&lt;/h1&gt;
&lt;p&gt;You can open the debug panel any time by pressing &lt;kbd&gt;d&lt;/kbd&gt;, by clicking on &lt;kbd&gt;Machine&lt;/kbd&gt;▶&lt;kbd&gt;Debug panel&amp;hellip;&lt;/kbd&gt;, or activate it at launch with the &lt;code&gt;-d&lt;/code&gt;/&lt;code&gt;--debug&lt;/code&gt; flag. The debug panel shows up in a translucent window. It provides a view of the internal state of the emulator, with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Current address, instruction, operands, and opcode, to the top.&lt;/li&gt;
&lt;li&gt;Internal state of CPU, PPU, and JOYP, to the left.&lt;/li&gt;
&lt;li&gt;Disassembly of the program, to the right.&lt;/li&gt;
&lt;li&gt;Breakpoints.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/debug-mode-3.avif" width="80%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:80%;"&gt;
&lt;h4&gt;The debug panel, showing the machine state and a code disassembly.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;You can use the provided UI controls to work with debug mode. You can also use the keyboard. These are the key bindings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;kbd&gt;F6&lt;/kbd&gt; - step a single instruction&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;F7&lt;/kbd&gt; - step a scanline&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;F9&lt;/kbd&gt; - continue execution until breakpoint (if paused), or pause execution (if running)&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;r&lt;/kbd&gt; - reset the CPU&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;d&lt;/kbd&gt; - exit debug mode and go back to normal full-speed emulation&lt;/li&gt;
&lt;li&gt;&lt;kbd&gt;Esc&lt;/kbd&gt; - exit the emulator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can also use breakpoints. A list with the current breakpoint addresses is provided at the bottom. To create a breakpoint, either &lt;strong&gt;click on the address&lt;/strong&gt; in the disassembly panel, or enter it (in &lt;code&gt;$abcd&lt;/code&gt; format) into the text field and click &lt;kbd&gt;+&lt;/kbd&gt;. Remove a breakpoint by clicking the &lt;kbd&gt;×&lt;/kbd&gt; in the breakpoints list. Clear all current breakpoints with &lt;kbd&gt;Clear all&lt;/kbd&gt;.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;video width="75%" poster="/img/playkid/playkid-ui.jpg" preload="none"controls="controls"
&gt;
&lt;source src="https://tonisagrista.com/img/playkid/playkid-ui.mp4" autostart="false"&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;figcaption style="margin: 0 auto; width:75%;"&gt;
&lt;h4&gt;Playing around with the Play Kid debug UI.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h1 id="cli-args"&gt;CLI args&lt;/h1&gt;
&lt;p&gt;There are some CLI arguments that you can use:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;Play Kid 0.4.0
Toni Sagristà - tonisagrista.com
Minimalist Game Boy emulator for the cool kids.
Usage: playkid [OPTIONS] [INPUT]
Arguments:
[INPUT] Path to the input ROM file to load
Options:
-s, --scale &amp;lt;SCALE&amp;gt; Initial window scale. It can also be resized manually [default: 4]
-d, --debug Activate debug mode. Use `d` to stop program at any point
-f, --fps Show FPS counter. Use `f` to toggle on and off
--skipcheck Skip global checksum, header checksum, and logo sequence check
-h, --help Print help
-V, --version Print version
&lt;/code&gt;&lt;/pre&gt;&lt;h1 id="sdl2-version"&gt;SDL2 version&lt;/h1&gt;
&lt;p&gt;Play Kid started as an SDL2 application, but it was moved to a pure Rust tech stack using &lt;code&gt;pixels&lt;/code&gt;, &lt;code&gt;winit&lt;/code&gt;, &lt;code&gt;egui&lt;/code&gt;, and &lt;code&gt;rodio&lt;/code&gt;. This makes it much easier to build for different targets (including WASM!). Additionally, the SDL2 version contains a minimalist homegrown UI library that I&amp;rsquo;m particularly proud about, but it can&amp;rsquo;t hold a candle to &lt;code&gt;egui&lt;/code&gt;. It looks like this:&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/debug-mode-sdl2.avif" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;The debug mode.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The SDL2 version is forever tagged &lt;code&gt;playkid-sdl2&lt;/code&gt; (&lt;a href="https://codeberg.org/langurmonkey/playkid/src/tag/playkid-sdl2"&gt;playkid-sdl2@codeberg&lt;/a&gt;, &lt;a href="https://github.com/langurmonkey/playkid/tree/playkid-sdl2"&gt;playkid-sdl2@github&lt;/a&gt;).&lt;/p&gt;
&lt;h1 id="useful-links"&gt;Useful links&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Pandocs: &lt;a href="https://gbdev.io/pandocs/"&gt;https://gbdev.io/pandocs/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Complete tech reference: &lt;a href="https://gekkio.fi/files/gb-docs/gbctr.pdf"&gt;https://gekkio.fi/files/gb-docs/gbctr.pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Game Boy CPU manual: &lt;a href="http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf"&gt;http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Game Boy CPU instructions: &lt;a href="https://meganesu.github.io/generate-gb-opcodes/"&gt;https://meganesu.github.io/generate-gb-opcodes/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Implementing a Game Boy emulator</title><link>https://tonisagrista.com/blog/2026/playkid/</link><pubDate>Tue, 20 Jan 2026 00:00:00 +0000</pubDate><author>Toni Sagristà Sellés</author><guid>https://tonisagrista.com/blog/2026/playkid/</guid><description>&lt;div class="hint"&gt;
&lt;div class="hint-title"&gt;&lt;i class="fa fa-info-circle"&gt;&lt;/i&gt;&amp;nbsp;&amp;nbsp;&lt;strong&gt;Notice&lt;/strong&gt;&lt;/div&gt;
&lt;div class="hint-body"&gt;
This post is about Play Kid version &lt;code&gt;0.1.0&lt;/code&gt;, which works with SDL2. Since then, version &lt;code&gt;0.2.0&lt;/code&gt; has been released. This new version uses Rust native crates like &lt;code&gt;winit&lt;/code&gt;, &lt;code&gt;egui&lt;/code&gt;, &lt;code&gt;rodio&lt;/code&gt;, and &lt;code&gt;pixels&lt;/code&gt;. See &lt;a href="https://tonisagrista.com/blog/2026/playkid-update"&gt;this write-up&lt;/a&gt; for more information on the new version.
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;When I was a kid my parents got me and my brothers a brand new Game Boy. I used to play it a lot, and I was mesmerized and amazed at how such a tiny brick could give life to so many wonderful worlds. Some of my favorite games were &lt;strong&gt;Super Mario Land&lt;/strong&gt; (1, 2, and 3, Wario Land), &lt;strong&gt;The Amazing Spider-Man&lt;/strong&gt;, and &lt;strong&gt;Kirby&amp;rsquo;s Dream Land&lt;/strong&gt;. We also had some other games, like &lt;strong&gt;Bugs Bunny Crazy Castle&lt;/strong&gt; or &lt;strong&gt;Tetris&lt;/strong&gt;, that I also played quite a lot. I remember fondly too.&lt;/p&gt;
&lt;p&gt;The first Super Mario Land in particular was magical to me. I would complete it, and then do it again in one sitting. There were no save games, as it used &lt;abbr title="Memory Bank Controller 1"&gt;MBC1&lt;/abbr&gt; with no &lt;abbr title="Random Access Memory"&gt;RAM&lt;/abbr&gt; (more on that later). You really had to &lt;em&gt;git gud&lt;/em&gt;. The second play through had the same levels but more enemies, in what must have been one of the first implementations of &lt;em&gt;New Game+&lt;/em&gt;. After this harder mode was completed, you could select and play any level. Too bad all the progress went away as soon as you powered off the device. &lt;strong&gt;Super Mario Land 2: 6 Golden Coins&lt;/strong&gt; already introduced save games (used MBC1+RAM+Battery). It was a huge step-up in QoL for Game Boy games. We also had the original &lt;abbr title="Nintendo Entertainment System"&gt;NES&lt;/abbr&gt;, and later the &lt;abbr title="Super NES"&gt;SNES&lt;/abbr&gt;, but the Game Boy always remained that cozy little fun device that you could just pick up and play for a quick session. It was a significant part of my childhood.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;video width="45%" poster="/img/playkid/supermarioland-thumb.jpg" preload="none"controls="controls"
&gt;
&lt;source src="https://tonisagrista.com/img/playkid/supermarioland-levels2-4.mp4" autostart="false"&gt;
Your browser does not support the video tag.
&lt;/video&gt;&lt;figcaption style="margin: 0 auto; width:45%;"&gt;
&lt;h4&gt;Me playing Super Mario Land, levels 2 to 4, with my own emulator.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Some time ago, I wrote a &lt;a href="https://tonisagrista.com/blog/2021/chip8-implementation/"&gt;CHIP-8 emulator&lt;/a&gt; in Rust, and found the experience very rewarding. The consensus online seems to be that the recommended next step, as far as emulator development goes, is the original Game Boy. I know that the internet is flooded with &amp;ldquo;Implementing a Game Boy emulator&amp;rdquo; kind of articles, and (judging by the GitHub counts) this is a very unoriginal project, but I still wanted to try my hand at it. So I took the bait and started working on it. Today, I&amp;rsquo;m excited to share my very own Game Boy emulator.&lt;/p&gt;
&lt;p&gt;The original Game Boy, also referred to as &lt;abbr title="Dot Matrix Game"&gt;DMG&lt;/abbr&gt; in nerdy spaces and released in 1989, is an excellent target for emulation. Its hardware is rather well-documented, its architecture is relatively simple compared to modern systems, yet it&amp;rsquo;s complex enough to present real challenges, which I for sure faced.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/logo-3x.avif"decoding="async" /&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;p&gt;I called my implementation &lt;a href="https://tonisagrista.com/projects/playkid"&gt;&lt;strong&gt;Play Kid&lt;/strong&gt;&lt;/a&gt;. It is written in Rust and uses &lt;abbr title="Simple DirectMedia Layer 2"&gt;SDL2&lt;/abbr&gt; for graphics, sound, and input. Here are the main features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All CPU instructions implemented.&lt;/li&gt;
&lt;li&gt;Full memory map implemented.&lt;/li&gt;
&lt;li&gt;Modes: &lt;abbr title="Read Only Memory"&gt;ROM&lt;/abbr&gt;, MBC1, MBC2, MBC3.&lt;/li&gt;
&lt;li&gt;Audio is implemented, with 4 channels, envelopes, sweep, and stereo.&lt;/li&gt;
&lt;li&gt;Supports game controllers via SDL (also hot-plugging).&lt;/li&gt;
&lt;li&gt;Multiple color palettes.&lt;/li&gt;
&lt;li&gt;Save screenshot of current frame buffer.&lt;/li&gt;
&lt;li&gt;Respects 160:144 aspect ratio by letter-boxing.&lt;/li&gt;
&lt;li&gt;Debug mode:
&lt;ul&gt;
&lt;li&gt;Step instruction.&lt;/li&gt;
&lt;li&gt;Step scanline.&lt;/li&gt;
&lt;li&gt;Pause/continue current execution.&lt;/li&gt;
&lt;li&gt;FPS counter.&lt;/li&gt;
&lt;li&gt;Displays internal state.&lt;/li&gt;
&lt;li&gt;Breakpoints.&lt;/li&gt;
&lt;li&gt;Uses own minimal UI library with horizontal/vertical layouts, labels, buttons, and text fields.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save RAM to &lt;code&gt;.sav&lt;/code&gt; files to emulate the battery-backed SRAM.&lt;/li&gt;
&lt;li&gt;Automatically adapts to multi-DPI setups by scaling the UI.&lt;/li&gt;
&lt;li&gt;Working games/roms:
&lt;ul&gt;
&lt;li&gt;Passes &lt;code&gt;dmg-acid2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Tetris&lt;/li&gt;
&lt;li&gt;Pokémon Red&lt;/li&gt;
&lt;li&gt;Super Mario Land&lt;/li&gt;
&lt;li&gt;Super Mario Land 2: 6 Golden Coins&lt;/li&gt;
&lt;li&gt;Super Mario Land 3: Wario Land&lt;/li&gt;
&lt;li&gt;Wario Land II&lt;/li&gt;
&lt;li&gt;Bugs Bunny Crazy Castle&lt;/li&gt;
&lt;li&gt;The Amazing Spider-Man&lt;/li&gt;
&lt;li&gt;Kirby&amp;rsquo;s Dream Land&lt;/li&gt;
&lt;li&gt;Dr. Mario&lt;/li&gt;
&lt;li&gt;Probably many more&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Works on &lt;a href="https://codeberg.org/langurmonkey/playkid/releases"&gt;Linux, macOS, and Windows&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this post, I navigate the general architecture of the Game Boy, emphasize interesting hardware quirks that made it a challenge to emulate properly, and present my implementation. I won&amp;rsquo;t go into too much detail, as I took care to make the code understandable and well-commented. In any case, for more details on a specific subject, you can refer to the code repository itself.&lt;/p&gt;
&lt;h2 id="foreword"&gt;Foreword&lt;/h2&gt;
&lt;p&gt;I have based my implementation, mainly, on three resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gbdev.io/pandocs/"&gt;Pan Docs&lt;/a&gt; &amp;ndash; possibly the best documentation on the Game Boy system out there.&lt;/li&gt;
&lt;li&gt;&lt;a href="http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf"&gt;Game Boy CPU Manual&lt;/a&gt; &amp;ndash; documents the Game Boy architecture in great detail.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://meganesu.github.io/generate-gb-opcodes/"&gt;Instruction table&lt;/a&gt; &amp;ndash; all 8-bit &lt;abbr title="operation codes"&gt;opcodes&lt;/abbr&gt; into a single table, complete with descriptions for each one of them. Very handy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On certain occasions, I found some of the Opcode descriptions to be unclear or a bit ambiguous. In these cases, I referred to actual pre-existing implementations. By far, the implementation I liked the most is &lt;a href="https://github.com/mvdnes/rboy"&gt;RBoy&lt;/a&gt;. It is a very compact and mostly uncommented Game Boy Color emulator, written in Rust. Most of the instructions are the same as in the Game Boy DMG, so it worked very well for me.&lt;/p&gt;
&lt;h2 id="game-boy-essentials"&gt;Game Boy essentials&lt;/h2&gt;
&lt;p&gt;Before we get into the details, let&amp;rsquo;s talk a bit about how the Game Boy actually works. Emulation is basically about mimicking hardware in software. We&amp;rsquo;re not just running Game Boy code, we&amp;rsquo;re simulating an entire machine cycle by cycle.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;
&lt;source srcset="https://tonisagrista.com/img/playkid/gameboy.jxl" type="image/jxl" /&gt;
&lt;img src="https://tonisagrista.com/img/playkid/gameboy.avif"
alt="The original Nintendo Game Boy. Original by Evan-Amos." loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption&gt;
&lt;p class="fig-attribution"&gt;The original Nintendo Game Boy. Original by &lt;a href="https://commons.wikimedia.org/wiki/User:Evan-Amos"&gt;Evan-Amos&lt;/a&gt;.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The Game Boy&amp;rsquo;s execution model is pretty simple. The CPU has a 16-bit &lt;abbr title="Program Counter"&gt;PC&lt;/abbr&gt; register that points to the next instruction in memory. Each cycle, it fetches the byte at that address, decodes it into an instruction, executes it, and increments the PC. Instructions are composed of opcode+operand, and can be one, two, or three bytes long. Each instruction takes a specific number of machine cycles (M-cycles) to complete, usually between 1 and 6. Since the CPU runs at around 4.19 MHz (master clock) and one M-cycle equals four clock cycles (&lt;strong&gt;T-cycles&lt;/strong&gt;), timing is important.&lt;/p&gt;
&lt;p&gt;The system uses memory-mapped I/O. This means hardware components like the display, audio, and input are accessed by reading and writing to specific memory addresses. Want to check if the &lt;kbd&gt;A&lt;/kbd&gt; button is pressed? Read from address &lt;code&gt;0xFF00&lt;/code&gt;. Need to start a &lt;kbd&gt;DMA&lt;/kbd&gt; transfer? Write to &lt;code&gt;0xFF46&lt;/code&gt;. This design makes the architecture simpler, but it also means that memory access can trigger all sorts of side effects, and you need to manage the mapping in your code.&lt;/p&gt;
&lt;p&gt;Interrupts let hardware grab the CPU&amp;rsquo;s attention. When something happens (the display finishing a frame, or a timer overflow), the corresponding interrupt flag is set. If interrupts are enabled (via the &lt;abbr title="Interrupt Master Enable"&gt;IME&lt;/abbr&gt; flag), the CPU pauses what it&amp;rsquo;s doing, pushes the current PC onto the stack, and jumps to a predefined interrupt handler address. This lets games sync with hardware events instead of constantly checking for them.&lt;/p&gt;
&lt;p&gt;The hardware components all move forward together based on the CPU&amp;rsquo;s cycle count. When the CPU executes a 4 M-cycle instruction, the &lt;abbr title="Pixel-Processing Unit"&gt;PPU&lt;/abbr&gt; advances 16 T-cycles worth of scanline rendering, the &lt;abbr title="Audio-Processing Unit"&gt;APU&lt;/abbr&gt; moves its sound generation forward, and timers tick along. Everything runs in lockstep, which means accurate emulation needs cycle-level precision across all the different parts.&lt;/p&gt;
&lt;p&gt;Understanding this basic execution model (fetch, decode, execute, repeat, with interrupts and memory-mapped hardware mixed in) is key before getting into the specifics. Each part of the system we&amp;rsquo;ll look at next works within this framework.&lt;/p&gt;
&lt;h2 id="the-cpu"&gt;The CPU&lt;/h2&gt;
&lt;p&gt;The heart of the Game Boy is a modified Intel 8080/Zilog Z80 processor running at approximately 4.19 MHz. Nintendo&amp;rsquo;s custom chip, sometimes called the SM83 or LR35902, includes most Z80 instructions but removes some and adds a few Game Boy-specific ones.&lt;/p&gt;
&lt;p&gt;The CPU features eight 8-bit registers (A, B, C, D, E, F, H, L) that can be paired into four 16-bit registers (AF, BC, DE, HL), plus a 16-bit &lt;abbr title="Stack Pointer"&gt;SP&lt;/abbr&gt; and the aforementioned PC. The F register is special, as its four most significant bits hold flags that track the results of operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Zero (Z)&lt;/li&gt;
&lt;li&gt;Subtraction (N)&lt;/li&gt;
&lt;li&gt;Half-Carry (H)&lt;/li&gt;
&lt;li&gt;Carry (C)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In my implementation, the CPU is contained in &lt;code&gt;machine.rs&lt;/code&gt;, which contains the main components of the hardware (memory, registers, PPU, APU, etc.), controls the execution state (with debug), and implements the instructions.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// # Machine
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// The machine contains the registers, the memory, and the display, and
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// controls the execution and CPU state. It also implements the CPU, which
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// decodes and executes instructions.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;struct&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Machine&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;&amp;#39;a&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;&amp;#39;b&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The registers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;registers&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;Registers&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The main memory.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;memory&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;Memory&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;&amp;#39;a&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The display.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;display&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;Display&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;&amp;#39;b&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The UI state.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;ui_state&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;Rc&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;RefCell&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;UIState&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Interrupt master enable flag.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;ime&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;bool&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// EI operation is delayed by one instruction, so we use this counter.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;ei&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// DI operation is delayed by one instruction, so we use this counter.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;di&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Flag that holds the running status.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;running&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;bool&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// CPU halted.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;halted&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;bool&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// T-states, basic unit of time, and 1:1 with the clock.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u32&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// M-cycles, base unit for CPU instructions, and 1:4 with the clock.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;m_cycles&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u32&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The debug manager.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;debug&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;DebugManager&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Print FPS every second.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;fps&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;bool&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The event pump.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;event_pump&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;EventPump&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The registers are implemented in a struct, which is trivial:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// # Registers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// We have 7 1-bit registers (`a`, `b`, `c`, `d`, `e`, `h`, `l`),
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// which can be accessed individually, or together as 16 bits,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// in the combinations `af`, `bc`, `de` and `hl`.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// We also have the flags register, `f`, whose 4 most significant bits are the flags
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// zero `z`, subtraction `n`, half-carry `h` and carry `c`.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// Additionally, we have two 16-bit special registers, the stack pointer `sp`, and
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// the program counter `pc`.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;struct&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Registers&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;b&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;c&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;d&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;e&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Flags register.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// - Bit 7: `z`, zero flag
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// - Bit 6: `n`, subtraction flag (BCD)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// - Bit 5: `h`, half-carry flag (BCD)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// - Bit 4: `c`, carry flag
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;f&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;h&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;l&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Stack pointer register.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sp&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u16&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Program counter register.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pc&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u16&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This struct is complented with handy methods to read and write the registers, either individually or as 16-bit combinations. As you can see, the code is &lt;strong&gt;very&lt;/strong&gt; well documented.&lt;/p&gt;
&lt;p&gt;The instructions live in an &lt;code&gt;enum&lt;/code&gt;, as do their operands. The instructions enum contains the 256 8-bit instructions, and the 256 16-bit instructions prefixed by &lt;code&gt;0xCB&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// # Instructions
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// This enum contains all supported instructions.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// Instructions that can act with both r8 and r16 registers are first named
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// by default in their r8 version. The r16 versions add a `16` at the end of the name.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;enum&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// NoOp.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;NOP&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Stop.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;STOP&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Halt.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;HALT&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Jump HL.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;JPHL&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Jump.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;JP&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;CC&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Relative jumps.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;JR&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;CC&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Push.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;PUSH&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R16EXT&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Pop.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;POP&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R16EXT&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Load.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;LD&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R8&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;LDcp&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R8&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;R8&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;LD16&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// Enumerates the r8 registers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;enum&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;R8&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;A&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;B&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;C&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;D&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;E&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;H&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;L&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;HL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// Enumerates the r16 registers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;enum&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;BC&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;DE&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;HL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;SP&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// Enumerates r16 registers for POP and PUSH, which contain AF.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;enum&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;R16EXT&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;BC&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;DE&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;HL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;AF&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// Enumerates the R16 registers to be used in (some) load operations.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;enum&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;R16LD&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;BC&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;DE&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;HLp&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;HLm&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;A8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;C&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;A16&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// Enumerates jump conditions, mostly flags.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;enum&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;CC&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;NONE&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;NZ&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Z&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;NC&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;C&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// RST&amp;#39;s target address, divided by 8.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;enum&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;TGT3&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;T0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;T1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;T2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;T3&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;T4&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;T5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;T6&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;T7&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And then, I have a &lt;code&gt;from_byte(byte: u8)&lt;/code&gt; method that matches opcodes to their actual enum values:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;impl&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Construct an instruction from its byte representation.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;from_byte&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;byte&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;) -&amp;gt; &lt;span style="color:#58a1dd"&gt;Option&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;byte&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// NOP
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x00&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Some&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;NOP&lt;/span&gt;()),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// STOP
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x10&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Some&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;STOP&lt;/span&gt;()),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// LD (r16)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x01&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Some&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;LD16&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;BC&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x11&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Some&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;LD16&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;DE&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x21&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Some&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;LD16&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;HL&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x31&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Some&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;LD16&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;SP&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Undocumented OPCODE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;println!&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;: Unimplemented instruction &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{:#02x}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;, treating as NOP&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;WARN&amp;#34;&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;yellow&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;byte&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Some&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;NOP&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I also have a &lt;code&gt;from_byte_0xcb(byte: u8)&lt;/code&gt; method used in the actual implementation to decode the 16-bit opcodes.&lt;/p&gt;
&lt;p&gt;Actually implementing the CPU meant building a &lt;code&gt;match&lt;/code&gt; with cases for ever single one of the 512 instructions. Each instruction updates the PC, manipulates registers or memory, and sets flags according to specific rules. Some instructions were very straightforward to implement; others, like &lt;abbr title="Decimal Adjust Accumulator"&gt;DAA&lt;/abbr&gt;, required careful consideration of edge cases and bit manipulation. The implementation structure is quite straightforward:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Execute a single instruction, and returns the number of cycles it takes.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;execute&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;run_instr&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;RunInstr&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;opcode&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;) -&amp;gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Actually execute the instruction.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;run_instr&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;instr&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// NOP: no operation.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;NOP&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// STOP
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;STOP&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;stop&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// HALT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;HALT&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// HALT only halts if IME is enabled.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ime&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;halt&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Otherwise, HALT acts like NOP.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// LD r16
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;LD16&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;r16&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;r16&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;BC&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;nw&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read16&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;registers&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;set_bc&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;nw&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;DE&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;nw&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read16&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;registers&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;set_de&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;nw&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;HL&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;nw&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read16&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;registers&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;set_hl&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;nw&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;R16&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;SP&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;registers&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;sp&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read16&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// OPCODE 16-bit (0xCB).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;OPCODE16&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Read next byte, construct instruction, execute 0xCB instruction.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;opcode0xcb&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read8&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;instr0xcb&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Instruction&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;from_byte_0xcb&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;opcode0xcb&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;msg&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;format!&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Incorrect 0xCB opcode: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{:#04X}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;opcode0xcb&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;execute_0xcb&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;instr0xcb&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;expect&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;msg&lt;/span&gt;), &lt;span style="color:#58a1dd"&gt;opcode0xcb&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Never should happen.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;panic!&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Instruction is not implemented: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{:#04X}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;opcode&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And, of course, there is an analogous &lt;code&gt;execute_0xcb(...)&lt;/code&gt; method that contains the implementation for all the 16-bit opcodes.&lt;/p&gt;
&lt;p&gt;The CPU doesn&amp;rsquo;t run in isolation and needs to be cycle-accurate. Each instruction takes a specific number of machine cycles (M-cycles), typically 1-6, and the rest of the system needs to advance accordingly. Getting timing right is quite important, because the graphics hardware, audio, and even some games depend on precise cycle counts. This is why &lt;code&gt;execute(...) -&amp;gt; u8&lt;/code&gt; returns an unsigned integer. This is the number of cycles that the instruction takes. The main loop handles the input events, and then runs as many cycles per frame as necessary.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;while&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;running&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;frame_start_time&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;std&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;time&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;Instant&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;now&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Handle input events.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;handle_events&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cycles_this_frame&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;usize&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;while&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cycles_this_frame&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;constants&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;CYCLES_PER_FRAME&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;t&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;m&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;machine_cycle&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;m_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;m&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;t&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;cycles_this_frame&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;t&lt;/span&gt; &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;usize&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Fill the LCD buffer if needed.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;display&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;render_lcd&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;memory&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Flush and present the frame.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;display&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;canvas&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;flush&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;display&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;present&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;frame_count&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Sleep for the remaining time in the frame.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;sleep_next_frame&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;frame_start_time&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The machine cycle runs a single CPU cycle. A CPU cycle does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read next opcode from the memory address pointed by PC&lt;/li&gt;
&lt;li&gt;Increment PC&lt;/li&gt;
&lt;li&gt;Decode the instruction&lt;/li&gt;
&lt;li&gt;Run the instruction &amp;ndash; &lt;code&gt;execute(...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run a memory cycle&lt;/li&gt;
&lt;li&gt;Update the IME and handle interrupts&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The code looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Main loop of the machine.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cycle&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;) -&amp;gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Fetch next instruction, and parse it.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;opcode&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read8&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;run_instr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;RunInstr&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;new&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;opcode&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;memory&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;registers&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Execute the instruction.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;execute&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;run_instr&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;opcode&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Run a machine cycle.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;machine_cycle&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;) -&amp;gt; (&lt;span style="color:#ff636f"&gt;u32&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;u32&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;bool&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// CPU instruction.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// One machine cycle (M-cycle) is 4 clock cycles.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;m_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;running&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;halted&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// CPU is halted, don&amp;#39;t execute instructions but still consume 1 M-cycle.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Run next CPU instruction.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cycle&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;u32&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// NOOP instruction.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;m_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;4&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Memory cycle.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;m_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Memory cycle.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;_&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;r&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;memory&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cycle&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;r&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Close down.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;running&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt; (&lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;false&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;m_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Update IME.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ime_update&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Handle interrupts if necessary.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;interrupt_m_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;interrupt_handling&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Return.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;interrupt_m_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#58a1dd"&gt;interrupt_m_cycles&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;4&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;interrupt_m_cycles&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (&lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;m_cycles&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We&amp;rsquo;ll talk about the memory cycle when we reach the memory section. For now, let&amp;rsquo;s look at interrupts, as it was an interesting challenge. The Game Boy supports five interrupt types: V-Blank, LCD STAT, Timer, Serial, and Joypad. The CPU checks for pending interrupts after each instruction, and if interrupts are enabled and one is pending, it pushes the PC onto the stack and jumps to the corresponding interrupt vector. Implementing the IME flag and its one-instruction delay after the EI instruction was a subtle detail that broke several games until I got it right. This is handled in &lt;code&gt;ime_update()&lt;/code&gt; and &lt;code&gt;interrupt_handling()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Updates the IME (Interrupt Master Enable) flag.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// This is necessary because the effect of the EI and DI instructions
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// is delayed by one instruction.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ime_update&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;di&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;di&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ime&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ei&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ei&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ime&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Interrupt handling. The IF bit corresponding to this interrupt, and the IME flag
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// are reset by the CUP. IF acknowledges the interrupt, and IME prevents any other
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// interrupts from being handled until re-enabled (with RETI).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// In this case, the corresponding interrupt handler is called by pushing the PC
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// to the stack, and then setting it to the address of the interrupt handler.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;interrupt_handling&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;) -&amp;gt; &lt;span style="color:#ff636f"&gt;u32&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pending&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;memory&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ie&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;memory&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;iff&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Wake from HALT if any interrupt is pending (even if IME is disabled).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pending&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;halted&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;halted&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// If no interrupts are pending OR IME is disabled, return.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ime&lt;/span&gt; &lt;span style="color:#ff636f"&gt;||&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pending&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Reset IME.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ime&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// IE and IF have the following format:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;//
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// | 7 6 5 | 4 | 3 | 2 | 1 | 0 |
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// | 1 | Joypad | Serial | Timer | LCD | VBlank |
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;//
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;i&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;5&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bit&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;i&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pending&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bit&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;memory&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;iff&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;bit&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pc&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;registers&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;pc&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;push_stack&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;pc&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;registers&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;pc&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bit&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x01&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0040&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x02&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0048&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x04&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0050&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x08&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0058&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x10&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0060&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;panic!&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Unknown interrupt bit &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;bit&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;break&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;5&lt;/span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// 5 M-cycles = 20 T-cycles
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="memory"&gt;Memory&lt;/h2&gt;
&lt;p&gt;The Game Boy&amp;rsquo;s 16-bit address space is divided into distinct regions, each serving a specific purpose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x0000&lt;/code&gt;-&lt;code&gt;0x3FFF&lt;/code&gt;: ROM bank 0 (16 KB, fixed)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x4000&lt;/code&gt;-&lt;code&gt;0x7FFF&lt;/code&gt;: ROM bank 1-N (16 KB, switchable via MBCs, more on that later)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x8000&lt;/code&gt;-&lt;code&gt;0x9FFF&lt;/code&gt;: Video RAM (8 KB)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0xA000&lt;/code&gt;-&lt;code&gt;0xBFFF&lt;/code&gt;: External RAM (8 KB, cartridge)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0xC000&lt;/code&gt;-&lt;code&gt;0xDFFF&lt;/code&gt;: Work RAM (8 KB)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0xE000&lt;/code&gt;-&lt;code&gt;0xFDFF&lt;/code&gt;: Echo RAM (mirror of 0xC000-0xDDFF)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0xFE00&lt;/code&gt;-&lt;code&gt;0xFE9F&lt;/code&gt;: &lt;abbr title="Object Attribute Memory"&gt;OAM&lt;/abbr&gt; (sprite data)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0xFF00&lt;/code&gt;-&lt;code&gt;0xFF7F&lt;/code&gt;: &lt;abbr title="Input/Output"&gt;I/O&lt;/abbr&gt; registers&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0xFF80&lt;/code&gt;-&lt;code&gt;0xFFFE&lt;/code&gt;: High RAM (127 bytes)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0xFFFF&lt;/code&gt;: &lt;abbr title="Interrupt Enable"&gt;IE&lt;/abbr&gt; register&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Implementing memory access required a dispatcher that routes reads and writes to the appropriate hardware component. Reading from &lt;abbr title="Video RAM"&gt;VRAM&lt;/abbr&gt; accesses the PPU&amp;rsquo;s memory, writing to &lt;code&gt;0xFF00&lt;/code&gt;-&lt;code&gt;0xFF7F&lt;/code&gt; triggers side effects in various subsystems, and accessing cartridge ROM or RAM goes through the mapper.&lt;/p&gt;
&lt;p&gt;The memory struct contains the Work RAM, the High RAM, I/O registers, the &lt;abbr title="Interrupt Flag"&gt;IF&lt;/abbr&gt; and IE, a reference to the PPU, a reference to the APU, a reference to the cartridge, the timer, and the joypad.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// # Memory
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// The Game Boy uses a 2-byte address space (0x0000 to 0xFFFF) to map the different
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;/// types of memory (RAM, VRAM, Cartridge memory, etc.)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;struct&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Memory&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;&amp;#39;a&lt;/span&gt;&lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Work RAM.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;wram&lt;/span&gt;: [&lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;; &lt;span style="color:#58a1dd"&gt;constants&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;WRAM_SIZE&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// High RAM.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;hram&lt;/span&gt;: [&lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;; &lt;span style="color:#58a1dd"&gt;constants&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;HRAM_SIZE&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// I/O registers.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;io&lt;/span&gt;: [&lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;; &lt;span style="color:#58a1dd"&gt;constants&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;IO_SIZE&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// IF: interrupt flag.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;iff&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// IE flag: interrupt enable.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ie&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Cartridge reference.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cart&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;&amp;#39;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Cartridge&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The PPU, Picture Processing Unit.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ppu&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;PPU&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The timer.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;timer&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;Timer&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The joypad.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;joypad&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;Joypad&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// The APU, Audio Processing Unit.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;apu&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;APU&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The actual mapping is implemented in the &lt;code&gt;read()&lt;/code&gt; and &lt;code&gt;write()&lt;/code&gt; operations. The address is matched to the specific range in the address space and forwarded to the appropriate component. For instance, the read method:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Read a byte of memory at the given `address`.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;read8&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;address&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u16&lt;/span&gt;) -&amp;gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;address&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x0000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x3FFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// 16kB bank #0 (cartridge).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cart&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;address&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x4000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x7FFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// 16kB switchable ROM bank (cartridge).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cart&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;address&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x8000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x9FFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// VRAM.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ppu&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;address&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0xA000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0xBFFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// 8kB switchable RAM bank (cartridge).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cart&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read_ram&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;address&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;.]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Memory-mapped I/O registers control everything from &lt;abbr title="Liquid Crystal Display"&gt;LCD&lt;/abbr&gt; modes to audio channels. Some registers are read-only, some write-only, and some have bits that behave differently on read versus write. The &lt;abbr title="Direct Memory Access"&gt;DMA&lt;/abbr&gt; register at &lt;code&gt;0xFF46&lt;/code&gt; is particularly interesting. Writing to it triggers a transfer of 160 bytes from ROM or RAM to OAM, during which the CPU should only access High RAM. Getting DMA timing correct was essential for sprite-heavy games.&lt;/p&gt;
&lt;h2 id="the-ppu"&gt;The PPU&lt;/h2&gt;
&lt;p&gt;The Picture Processing Unit is what puts pixels on the screen. It generates a 160×144 display running at around 60 Hz. Getting it working meant understanding scanlines, timing, and how backgrounds, windows, and sprites all come together.&lt;/p&gt;
&lt;p&gt;The PPU works one scanline at a time. Each scanline takes 456 T-cycles (dots), and a full frame has 154 scanlines: 144 visible ones and 10 during V-Blank. While rendering a scanline, the PPU goes through different modes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mode 2 (OAM Search)&lt;/strong&gt;: Scanning which sprites will be visible on this scanline (80 dots)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode 3 (Pixel Transfer)&lt;/strong&gt;: Actually drawing the pixels (variable, ~172 dots)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode 0 (H-Blank)&lt;/strong&gt;: Horizontal blank, waiting for the scanline to finish (~204 dots)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode 1 (V-Blank)&lt;/strong&gt;: Vertical blank, screen done rendering (10 scanlines × 456 dots)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;During V-Blank, games usually update their state and prepare for the next frame. My implementation transitions between modes based on the current scanline and dot count:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;new_mode&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ly&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;144&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// VBlank.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;fdot&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;80&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt;, &lt;span style="color:#828b96;font-style:italic"&gt;// OAM Scan.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;81&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;252&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;3&lt;/span&gt;, &lt;span style="color:#828b96;font-style:italic"&gt;// Drawing.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#828b96;font-style:italic"&gt;// HBlank.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The background is made up of 8x8 pixel tiles stored in VRAM. Each tile uses 2 bits per pixel, giving you four shades of gray (or colors if you apply a palette). There are two tile data areas (at &lt;code&gt;0x8000&lt;/code&gt; and &lt;code&gt;0x8800&lt;/code&gt;) and two tile maps that tell you which tiles go where. The LCDC register controls which areas to use, and you can think of it as configuring how the PPU interprets the data in VRAM.&lt;/p&gt;
&lt;p&gt;Fetching tile data means reading two bytes per tile row and combining them to get the color IDs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;tile_addr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;tile_addr_base&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;line&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;low_byte&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;tile_addr&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;high_byte&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;tile_addr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pixels&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; [&lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;&lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;; &lt;span style="color:#a6be9d"&gt;8&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;i&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;8&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;color_id&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; ((&lt;span style="color:#58a1dd"&gt;high_byte&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; (&lt;span style="color:#a6be9d"&gt;7&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;i&lt;/span&gt;)) &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x1&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt; &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; ((&lt;span style="color:#58a1dd"&gt;low_byte&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; (&lt;span style="color:#a6be9d"&gt;7&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;i&lt;/span&gt;)) &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;pixels&lt;/span&gt;[&lt;span style="color:#58a1dd"&gt;i&lt;/span&gt;] &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;color_id&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Windows work similarly to backgrounds but ignore scrolling. They&amp;rsquo;re positioned absolutely on screen using WX and WY registers. My implementation tracks an internal window line counter (WLY) that only increments when the window actually renders on a scanline, which is important for games that turn the window on and off mid-frame.&lt;/p&gt;
&lt;p&gt;Sprites (or objects) are 8x8 or 8x16 pixels that sit on top of the background. The OAM holds up to 40 sprite definitions, each with position, tile index, and attributes like palette selection, flip flags, and priority. The hardware can only show 10 sprites per scanline, and when more than 10 overlap, priority goes to sprites with lower X coordinates first, then lower OAM index:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;// Game Boy priority: X asc, then OAM index asc.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;sprites&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;sort_by&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;|&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;a&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;b&lt;/span&gt;&lt;span style="color:#ff636f"&gt;|&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;x&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;b&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;x&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;x&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cmp&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;b&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;x&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;oam_index&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cmp&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;b&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;oam_index&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One of the trickier parts was getting sprite priority right. The priority bit in the sprite attributes controls whether a sprite appears above all background pixels or only above background color 0. I store the background color IDs in a separate buffer as I render the background, then check against those values when drawing sprites:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bg_color_id&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;priorities&lt;/span&gt;[&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ly&lt;/span&gt; &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;usize&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;constants&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;DISPLAY_WIDTH&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;x_pos&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;obj_behind_bg&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;sprite&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;attributes&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x80&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;!=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sprite_has_priority&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;obj_behind_bg&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Priority bit set: only draw sprite over BG color 0.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;bg_color_id&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Priority bit clear: always draw sprite (over any BG color).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This lets me respect the priority rules properly without having to re-read background data.&lt;/p&gt;
&lt;p&gt;I cache tile data while rendering each scanline to avoid repeatedly decoding the same tiles. For backgrounds and windows, I cache up to 32 tiles (one row). For sprites, I use a &lt;code&gt;HashMap&lt;/code&gt; keyed by tile ID, line number, and flip flags.&lt;/p&gt;
&lt;p&gt;The PPU&amp;rsquo;s timing had to be cycle-accurate. Each frame needs exactly 70224 T-cycles (154 scanlines × 456 dots). The main loop advances the PPU by the number of cycles the CPU just executed, and the PPU updates its mode and LY register (current scanline) accordingly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cycle&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u32&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;is_ppu_enabled&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;fdot&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Update mode if it changed.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;new_mode&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;mode&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;update_mode&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;new_mode&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Handle transition to next scanline.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;fdot&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;456&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;fdot&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;456&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ly&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ly&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;%&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;154&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Getting the timing right was kind of important because some games rely on precise V-Blank and H-Blank timing. Otherwise, you get visual glitches, and good luck debugging them.&lt;/p&gt;
&lt;p&gt;In order to test my PPU implementation, I turned to the &lt;a href="https://github.com/mattcurrie/dmg-acid2"&gt;DMG Acid2&lt;/a&gt; test. This is a rather exhaustive PPU test in the form of a ROM. If you see a happy face with &lt;code&gt;HELLO WORLD!&lt;/code&gt; above, and &lt;code&gt;dmg-acid2 by Matt Currie&lt;/code&gt; below, you&amp;rsquo;re good. In my case, the right chin was missing. I spent an ginormous amount of time trying to get it to render properly. This is what I was getting:&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/acid2-chin.jpg"
alt="Oh no, the right chin is missing!" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption&gt;
&lt;p class="fig-attribution"&gt;Oh no, the right chin is missing!&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I was so close. The &lt;a href="https://github.com/mattcurrie/dmg-acid2?tab=readme-ov-file#failure-examples"&gt;README&lt;/a&gt; has an entry that looks exactly like it. The right chin is rendered using the window, and the window tile map, so I kept looking at the window code. It was a pain, and for the life of me, I could not find the issue even though I looked incessantly. In the end, I fed my code to an LLM and it found the error right away: In my read and write VRAM operations, I had WX and WY backwards!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// This is NOT correct!
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// WY.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0xFF4A&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;wx&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// WX.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0xFF4B&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;wy&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This was frustrating, but it really goes to show what random side effects such a stupid mistake can have.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/acid2-good.jpg"
alt="After countless hours, the stupid bug was mended and Acid2 was good!" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption&gt;
&lt;p class="fig-attribution"&gt;After countless hours, the stupid bug was mended and Acid2 was good!&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Now that my PPU was more or less feature-complete, I tested it with other games. Seeing the first sprite render correctly, or watching the background scroll smoothly in Mario for the first time made it worth it. The PPU is obviously the most (only?) visible part of the emulator, so getting it to a state where it was able to run all the games I wanted to run felt good.&lt;/p&gt;
&lt;p&gt;At the very end, when the graphics were already in an excellent shape, I implemented multiple color palettes. The original Game Boy was monochrome, but you can map those four gray shades to any colors you want. IIRC, the Super Game Boy did something similar, and it allowed you to create your own too! I included a few classic palettes (DMG Classic, Pocket, etc.) and some more creative ones. You can cycle through them while playing with &lt;kbd&gt;p&lt;/kbd&gt;, which makes the games look fresh.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/grid.avif"
alt="Some of the palettes in Play Kid." loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption&gt;
&lt;p class="fig-attribution"&gt;Some of the palettes in Play Kid.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;At this point, I could play lots of games, but I had no audio. This had to be mended.&lt;/p&gt;
&lt;h2 id="the-apu"&gt;The APU&lt;/h2&gt;
&lt;p&gt;I consider the Audio Processing Unit (APU) to be one of the most boring parts of this project. It synthesizes sound through four channels: two pulse wave channels (square waves), one wave channel (custom waveform), and one noise channel (pseudo-random). Each channel has its own registers for frequency, volume, envelope, and duty cycle.&lt;/p&gt;
&lt;p&gt;The APU is fully synchronized with the CPU and the PPU, and tus runs at 4.19 MHz. This is the master clock that we&amp;rsquo;ve seen previously. However, the APU doesn&amp;rsquo;t do everything at 4.19 MHz. Internally, it uses a frame sequencer that divides this master clock down to 512 Hz to manage high-level audio events:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Length Counters: Clocked at 256 Hz (every 2nd step).&lt;/li&gt;
&lt;li&gt;Frequency Sweep: Clocked at 128 Hz (every 4th step).&lt;/li&gt;
&lt;li&gt;Volume Envelopes: Clocked at 64 Hz (every 8th step).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It steps through eight different phases, some updating length counters, some update the sweep function, and some updating volume envelopes. My implementation tracks this with a frame timer that increments with each T-cycle and runs at 512 Hz (4194304 Hz / 8192 = 512 Hz):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;frame_timer&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;t_cycles&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;frame_timer&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;8192&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// 512Hz is 4.19 Mhz / 8192.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;frame_timer&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;8192&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Phases from 0 to 7.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;frame_sequencer&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt; &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;4&lt;/span&gt; &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;6&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;step_length&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;frame_sequencer&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt; &lt;span style="color:#ff636f"&gt;||&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;frame_sequencer&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;6&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;step_sweep&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;7&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;step_envelope_ch1&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;step_envelope_ch2&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;step_envelope_ch4&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;frame_sequencer&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;frame_sequencer&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;%&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;8&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each pulse channel (1 and 2) generates square waves with four different duty cycles. The duty cycle controls the waveform shape and is stored as patterns:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;duty_idx&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;0xFF11&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;6&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;usize&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;patterns&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;], &lt;span style="color:#828b96;font-style:italic"&gt;// 12.5%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;], &lt;span style="color:#828b96;font-style:italic"&gt;// 25%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;], &lt;span style="color:#828b96;font-style:italic"&gt;// 50%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [&lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;], &lt;span style="color:#828b96;font-style:italic"&gt;// 75%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;signal&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;patterns&lt;/span&gt;[&lt;span style="color:#58a1dd"&gt;duty_idx&lt;/span&gt;][&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch1_duty_step&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;volume_multiplier&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch1_volume&lt;/span&gt; &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;f32&lt;/span&gt; &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;15.0&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.05&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The wave channel (channel 3) is different. It plays back samples from a 32-nibble waveform stored in Wave RAM. Games can write custom waveforms here, which makes this channel super flexible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;// Get 4-bit sample from Wave RAM (32 samples total, 2 per byte).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;byte&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;wave_ram&lt;/span&gt;[&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch3_sample_idx&lt;/span&gt; &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sample&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch3_sample_idx&lt;/span&gt; &lt;span style="color:#ff636f"&gt;%&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;byte&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;4&lt;/span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// High nibble.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;byte&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0F&lt;/span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Low nibble.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;// Volume shift based on NR32 register.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;sample&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;volume_shift&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The noise channel (channel 4) uses a &lt;abbr title="Linear Feedback Shift Register"&gt;LFSR&lt;/abbr&gt; to generate pseudo-random noise. The LFSR is a 15-bit shift register that &lt;abbr title="eXclusive OR"&gt;XOR&lt;/abbr&gt;s bits 0 and 1, then shifts the result back in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bit0&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch4_lfsr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x01&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bit1&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch4_lfsr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x01&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;result&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bit0&lt;/span&gt; &lt;span style="color:#ff636f"&gt;^&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bit1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch4_lfsr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch4_lfsr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;result&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;14&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;if&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;nr43&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x08&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;!=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Short mode (7-bit)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch4_lfsr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch4_lfsr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;1&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;6&lt;/span&gt;)) &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;result&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;6&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates the characteristic noise used for drums, explosions, and other effects.&lt;/p&gt;
&lt;p&gt;The sweep function on channel 1 was particularly tricky. It modulates the frequency over time by shifting the current frequency right and either adding or subtracting the result. The overflow check happens both when sweep is triggered and when calculating the new frequency:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;calculate_sweep_freq&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;) -&amp;gt; &lt;span style="color:#ff636f"&gt;u16&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;nr10&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;read&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;0xFF10&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sweep_step&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;nr10&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x07&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;descending&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;nr10&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x08&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;!=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;delta&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch1_sweep_shadow_freq&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sweep_step&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;new_freq&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;descending&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch1_sweep_shadow_freq&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;saturating_sub&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;delta&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch1_sweep_shadow_freq&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;delta&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Overflow check.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;new_freq&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2047&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ch1_enabled&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;new_freq&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Getting the timing and overflow behavior right required reading Pan Docs extensively, and I also looked at RBoy&amp;rsquo;s implementation.&lt;/p&gt;
&lt;p&gt;Sound samples are generated at 44100 Hz and mixed into stereo output based on the NR51 register, which controls left/right panning for each channel. SDL2&amp;rsquo;s audio queue system fit naturally with the APU design. I generate samples as the emulator runs and queue them for SDL to play back:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;let&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;l_sample&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;r_sample&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;generate_sample&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;buffer&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;push&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;l_sample&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;buffer&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;push&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;r_sample&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;buffer&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;len&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2048&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Throttle if queue is getting too full
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;while&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;device&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;size&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;8192&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;std&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;thread&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;sleep&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;std&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;time&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;Duration&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;from_millis&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;device&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;queue_audio&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;buffer&lt;/span&gt;).&lt;span style="color:#58a1dd"&gt;unwrap&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;buffer&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;clear&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Keeping audio and video synchronized without drift required careful management of cycle counts and buffer sizes. The throttling mechanism prevents the emulator from running too fast when the audio queue backs up.&lt;/p&gt;
&lt;p&gt;I implemented channel 1 first, and then ran Super Mario Land. I could hear some sounds (jumps, parts of the music), but the experience was underwhelming. Since all channels follow a similar structure, I had channel 2 working in no time. Now I could hear the iconic melody of the first stage, all the jumps, coins, and enemies. I implemented the other channels soon after.&lt;/p&gt;
&lt;p&gt;Interestingly, in Super Mario Land, the music is mostly in channels 2 and 3. Channel 1 is for things like jumps, coins, and some high-pitched parts of melodies. Channel 4 is used in the random noise that some enemies produce when they die (like the bomb turtles), and some percussion beats in the background.&lt;/p&gt;
&lt;p&gt;I still have no idea whether my APU contains any bugs, or if it sounds exactly as it&amp;rsquo;s supposed to. I have no way to tell. To my ears, it sounds amazing.&lt;/p&gt;
&lt;h2 id="cartridges"&gt;Cartridges&lt;/h2&gt;
&lt;p&gt;Most Game Boy cartridges aren&amp;rsquo;t just ROM chips. They contain &lt;abbr title="Memory Bank Controller"&gt;MBC&lt;/abbr&gt;s that expand the addressable memory beyond the CPU&amp;rsquo;s 64 KB limit. Different games use different MBCs, each with its own banking scheme.&lt;/p&gt;
&lt;p&gt;Every cartridge has a header header that contains essential information about the game, stored in the first few bytes of the cartridge&amp;rsquo;s ROM. This information helps the system identify and load the game properly. The header includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Logo&lt;/strong&gt; &amp;ndash; the first 48 bytes (&lt;code&gt;0x00&lt;/code&gt;-&lt;code&gt;0x2F&lt;/code&gt;) contain the Nintendo logo, ensuring the authenticity of the cartridge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Title&lt;/strong&gt; &amp;ndash; the next 16 bytes (&lt;code&gt;0x34&lt;/code&gt;-&lt;code&gt;0x43&lt;/code&gt;) store the 8-character game title, which is displayed when the game is loaded.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Game Boy/GBC Flag&lt;/strong&gt; &amp;ndash; the byte at &lt;code&gt;0x143&lt;/code&gt; indicates whether the game is for the original Game Boy (&lt;code&gt;0x00&lt;/code&gt;), or a Game Boy Color-compatible game (&lt;code&gt;0x80&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cartridge Type&lt;/strong&gt; &amp;ndash; the byte at &lt;code&gt;0x147&lt;/code&gt; specifies the cartridge type, such as ROM-only or with memory banks. Here is a non-exhaustive list of common values:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0x00&lt;/code&gt; &amp;ndash; ROM only&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x01&lt;/code&gt; &amp;ndash; MBC1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x02&lt;/code&gt; &amp;ndash; MBC1+RAM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x03&lt;/code&gt; &amp;ndash; MBC1+RAM+BATTERY&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x05&lt;/code&gt; &amp;ndash; MBC2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x06&lt;/code&gt; &amp;ndash; MBC2+BATTERY&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x11&lt;/code&gt; &amp;ndash; MBC3&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x12&lt;/code&gt; &amp;ndash; MBC3+RAM&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0x13&lt;/code&gt; &amp;ndash; MBC3+RAM+BATTERY&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROM Size&lt;/strong&gt; &amp;ndash; the byte at &lt;code&gt;0x148&lt;/code&gt; specifies the ROM size in powers of 2. For example, &lt;code&gt;0x00&lt;/code&gt; is 32KB, &lt;code&gt;0x01&lt;/code&gt; is 64KB, &lt;code&gt;0x02&lt;/code&gt; is 128KB, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAM Size&lt;/strong&gt; &amp;ndash; the byte at &lt;code&gt;0x149&lt;/code&gt; specifies the RAM size in kilobytes. For example, &lt;code&gt;0x00&lt;/code&gt; means no RAM, &lt;code&gt;0x03&lt;/code&gt; means 32KB, &lt;code&gt;0x04&lt;/code&gt; means 128KB, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Destination Code&lt;/strong&gt; &amp;ndash; located at &lt;code&gt;0x14A&lt;/code&gt;, this byte indicates the region the game is intended for. &lt;code&gt;0x00&lt;/code&gt; is for Japan, &lt;code&gt;0x01&lt;/code&gt; for Overseas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Licensee Code&lt;/strong&gt; &amp;ndash; the byte at &lt;code&gt;0x14B&lt;/code&gt; contains a code identifying the licensed company that produced the game.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mask ROM Version&lt;/strong&gt; &amp;ndash; the byte at &lt;code&gt;0x14C&lt;/code&gt; is the version number of the game’s mask ROM (usually set to &lt;code&gt;0x00&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complement Checksum&lt;/strong&gt; &amp;ndash; the byte at &lt;code&gt;0x14D&lt;/code&gt; is used for the checksum calculation to verify the integrity of the ROM data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global Checksum&lt;/strong&gt; &amp;ndash; the last 2 bytes (&lt;code&gt;0x14E&lt;/code&gt;-&lt;code&gt;0x14F&lt;/code&gt;) store a checksum value that ensures the data integrity of the entire ROM.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This header allows the Game Boy to load and display the game properly by checking for compatibility and loading the correct resources for the specified hardware.&lt;/p&gt;
&lt;p&gt;When it comes to MBCs, I implemented ROM-only and MBC1/2/3 into Play Kid.&lt;/p&gt;
&lt;dl&gt;
&lt;dt&gt;&lt;strong&gt;ROM-only&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;ROM-only cartridges are the simplest, with no banking at all. These are limited to 32 KB and include simple games like Tetris. No mapper logic needed, just direct reads from the ROM data.&lt;/dd&gt;
&lt;dd&gt;
&lt;p&gt;Tetris and Dr. Mario use ROM-only cartridges.&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;&lt;strong&gt;MBC1&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;MBC1 is the most common mapper, supporting up to 2 MB of ROM and 32 KB of RAM. It uses banking to switch between ROM and RAM banks by writing to specific address ranges. Understanding that writes to ROM addresses don&amp;rsquo;t actually write to ROM but instead configure the mapper took some mental adjustment.&lt;/dd&gt;
&lt;dd&gt;
&lt;p&gt;MBC1 has two banking modes controlled by a mode register. In ROM banking mode (mode 0), you get access to more ROM banks but only one RAM bank. In RAM banking mode (mode 1), you can switch between multiple RAM banks but lose access to the upper ROM banks:&lt;/p&gt;
&lt;/dd&gt;
&lt;dd&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;write&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;address&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u16&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;value&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;address&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// RAM enable.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x0000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x1FFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ram_enabled&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;value&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0F&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0A&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// ROM bank number (lower 5 bits).
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x2000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x3FFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;value&lt;/span&gt; &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;usize&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x1F&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;; &lt;span style="color:#828b96;font-style:italic"&gt;// Bank 0 becomes bank 1.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rom_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rom_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x60&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bank&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// RAM bank or upper ROM bank bits.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x4000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x5FFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bits&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;value&lt;/span&gt; &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;usize&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x03&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;banking_mode&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rom_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;bits&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;5&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rom_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x1F&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ram_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bits&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Banking mode select.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x6000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x7FFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;banking_mode&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;value&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x01&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/dd&gt;
&lt;dd&gt;
&lt;p&gt;Super Mario Land (1 and 2), Wario Land, Kirby&amp;rsquo;s Dream Land, and The Amazing Spider-Man all use MBC1.&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;&lt;strong&gt;MBC2&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;MBC2 has a quirky design. It includes 512×4 bits of built-in RAM, and bit 8 of the write address determines whether you&amp;rsquo;re configuring ROM banking or RAM enable. Both operations share the same address range (&lt;code&gt;0x0000&lt;/code&gt;-&lt;code&gt;0x1FFF&lt;/code&gt;):&lt;/dd&gt;
&lt;dd&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;addr&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x0000&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x1FFF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;addr&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;8&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x01&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Bit 8 is 0: RAM enable.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ram_enabled&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;val&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0F&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0A&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Bit 8 is 1: ROM bank select.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rom_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;val&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x0F&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rom_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rom_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/dd&gt;
&lt;dd&gt;
&lt;p&gt;Since MBC2&amp;rsquo;s RAM is only 4 bits per byte, reads return the value OR&amp;rsquo;d with 0xF0, and writes mask to the lower 4 bits.&lt;/p&gt;
&lt;/dd&gt;
&lt;dd&gt;
&lt;p&gt;I think I have not tested any game that uses MBC2.&lt;/p&gt;
&lt;/dd&gt;
&lt;dt&gt;&lt;strong&gt;MBC3&lt;/strong&gt;&lt;/dt&gt;
&lt;dd&gt;MBC3 adds a real-time clock (RTC) alongside standard ROM/RAM banking. The RTC includes seconds, minutes, hours, and days counters that can be accessed by setting the RAM bank register to values &lt;code&gt;0x08&lt;/code&gt;-&lt;code&gt;0x0C&lt;/code&gt;. I understand that some games use the RTC to track time-based events, but I have not tested this:&lt;/dd&gt;
&lt;dd&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;read_ram&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;address&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;u16&lt;/span&gt;) -&amp;gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ram_enabled&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0xFF&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ram_bank&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x00&lt;/span&gt;&lt;span style="color:#ff636f"&gt;..=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;0x03&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Standard RAM banks.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;offset&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ram_bank&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x2000&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;address&lt;/span&gt; &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;usize&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0xA000&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ram&lt;/span&gt;[&lt;span style="color:#58a1dd"&gt;offset&lt;/span&gt; &lt;span style="color:#ff636f"&gt;%&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;ram&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;len&lt;/span&gt;()]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x08&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_seconds&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x09&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_minutes&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x0A&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_hours&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x0B&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_days_low&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;0x0C&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_days_high&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0xFF&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/dd&gt;
&lt;dd&gt;
&lt;p&gt;The RTC is latched by writing 0 then 1 to address range &lt;code&gt;0x6000&lt;/code&gt;-&lt;code&gt;0x7FFF&lt;/code&gt;. This copies the current time into the registers so the game can read a consistent value. My implementation updates the RTC with system time:&lt;/p&gt;
&lt;/dd&gt;
&lt;dd&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;update_rtc&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;now&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;SystemTime&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;now&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#58a1dd"&gt;duration_since&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;UNIX_EPOCH&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#58a1dd"&gt;unwrap&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#58a1dd"&gt;as_secs&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_seconds&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;now&lt;/span&gt; &lt;span style="color:#ff636f"&gt;%&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;60&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_minutes&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; ((&lt;span style="color:#58a1dd"&gt;now&lt;/span&gt; &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;60&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;%&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;60&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_hours&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; ((&lt;span style="color:#58a1dd"&gt;now&lt;/span&gt; &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;3600&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;%&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;24&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;days&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;now&lt;/span&gt; &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;86400&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;u16&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_days_low&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;days&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0xFF&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Day MSB and carry flag.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;day_msb&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; ((&lt;span style="color:#58a1dd"&gt;days&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;8&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x01&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;as&lt;/span&gt; &lt;span style="color:#ff636f"&gt;u8&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_days_high&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_days_high&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0xFE&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;|&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;day_msb&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;days&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;511&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rtc_days_high&lt;/span&gt; &lt;span style="color:#ff636f"&gt;|=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0x80&lt;/span&gt;; &lt;span style="color:#828b96;font-style:italic"&gt;// Set overflow flag.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/dd&gt;
&lt;dd&gt;
&lt;p&gt;Pokémon and Wario Land II use MBC3.&lt;/p&gt;
&lt;/dd&gt;
&lt;/dl&gt;
&lt;p&gt;Finally, I implemented battery-backed SRAM. It meant saving the contents of cartridge RAM to &lt;code&gt;.sav&lt;/code&gt; files when the emulator exits and loading them on startup, if they exist. You can also trigger the save operation at any time with &lt;kbd&gt;w&lt;/kbd&gt;. I get the RAM from the cartridge, and then persist it (if not empty):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-rust" data-lang="rust"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;/// Save SRAM of current cartridge to `.sav` file.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;pub&lt;/span&gt; &lt;span style="color:#ff636f"&gt;fn&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;save_sram&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;rom_path&lt;/span&gt;: &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#ff636f"&gt;str&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;save_path&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Path&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;new&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;rom_path&lt;/span&gt;).&lt;span style="color:#58a1dd"&gt;with_extension&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;sav&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Only save if the mapper actually has RAM.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ram_data&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;match&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cart_type&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;CartridgeType&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;MBC1&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;mbc&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;mbc&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;get_ram&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;CartridgeType&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;MBC2&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;mbc&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;mbc&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;get_ram&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;CartridgeType&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;MBC3&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;mbc&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;mbc&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;get_ram&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;return&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;ram_data&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;is_empty&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;||&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;matches!&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;cart_type&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;CartridgeType&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;MBC2&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;_&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Ok&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;mut&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;file&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;File&lt;/span&gt;::&lt;span style="color:#58a1dd"&gt;create&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;&amp;amp;&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;save_path&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;let&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;_&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;file&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;write_all&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;ram_data&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;println!&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;: Save data written to disk: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;WR&amp;#34;&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;magenta&lt;/span&gt;(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;save_path&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;display&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The load operation checks for the file, loads it if it exists, and passes it to each MBC&amp;rsquo;s &lt;code&gt;set_ram()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;This feature is essential for saving progress in many games. Currently, the &lt;code&gt;.sav&lt;/code&gt; files are written to the same directory where the ROM file is, with the same name and &lt;code&gt;sav&lt;/code&gt; extension.&lt;/p&gt;
&lt;h2 id="ui-and-debug-mode"&gt;UI and debug mode&lt;/h2&gt;
&lt;p&gt;In order to help me debug the emulator as development progressed, I implemented a terminal-based debug interface that let me look under the hood and inspect the internal state of the machine. This is implemented in the &lt;code&gt;src/debugcli.rs&lt;/code&gt; file in the repository, but it is now unused.&lt;/p&gt;
&lt;p&gt;When the emulator was already working quite well, I implemented a GUI debug mode with a custom immediate-mode UI library built on top of SDL2 rendering. You can enable it at any time with &lt;kbd&gt;d&lt;/kbd&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;abbr title="User Interface"&gt;UI&lt;/abbr&gt; library supports horizontal and vertical layouts, labels, buttons, and text fields. Enough to build a useful debug interface without pulling in a fat dependency. It&amp;rsquo;s minimal but effective. Writing it was not trivial. I knew that UI libraries are complex and take a lot of work to get right, and this little spin-off sub-project only confirmed this knowledge.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/playkid/debug-mode-sdl2.avif"
alt="Super Mario Land 2 running in Play Kid with the debug UI." loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption&gt;
&lt;p class="fig-attribution"&gt;Super Mario Land 2 running in Play Kid with the debug UI.&lt;/p&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The debug mode features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Current instruction, operand, and PC&lt;/li&gt;
&lt;li&gt;Opcode&lt;/li&gt;
&lt;li&gt;CPU status (either RUNNING or HALTED)&lt;/li&gt;
&lt;li&gt;M- and T-cycles&lt;/li&gt;
&lt;li&gt;CPU registers and flags&lt;/li&gt;
&lt;li&gt;PPU state (LCDC, STAT, LYC, LY, LX)&lt;/li&gt;
&lt;li&gt;Joypad state for all inputs (U, D, L, R, A, B, START, SELECT)&lt;/li&gt;
&lt;li&gt;Breakpoints&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can step through execution one instruction (&lt;kbd&gt;F6&lt;/kbd&gt;) or one scanline (&lt;kbd&gt;F7&lt;/kbd&gt;) at a time, pause and continue (&lt;kbd&gt;F9&lt;/kbd&gt;), and set breakpoints at specific addresses. You can also reset the CPU with &lt;kbd&gt;r&lt;/kbd&gt;. I also included an FPS counter (&lt;kbd&gt;f&lt;/kbd&gt;). You can enable and disable the debug mode at any time with &lt;kbd&gt;d&lt;/kbd&gt;.&lt;/p&gt;
&lt;p&gt;One quality-of-life feature I&amp;rsquo;m proud of is automatic &lt;abbr title="Dots per Inch"&gt;DPI&lt;/abbr&gt; scaling. On high-DPI displays, the UI scales appropriately so everything remains readable without manual configuration. This was a pain to handle manually. The emulator also respects the Game Boy&amp;rsquo;s 160:144 aspect ratio by letterboxing the display. It&amp;rsquo;s a small detail, but it ensures games look correct rather than stretched or squashed.&lt;/p&gt;
&lt;h2 id="future-work"&gt;Future work&lt;/h2&gt;
&lt;p&gt;Here are some items I would like to do at some point:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Move away from SDL2 in favor of Rust-native libraries like &lt;code&gt;winit&lt;/code&gt;+&lt;code&gt;pixels&lt;/code&gt;+&lt;code&gt;rodio&lt;/code&gt; for easy cross-compilation, and web. I actually have a branch where I implemented the APU using &lt;code&gt;rodio&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Implement more MBCs if I stumble upon games that need them.&lt;/li&gt;
&lt;li&gt;Add Game Boy Color support. As I understand it, it is not too complicated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusions"&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;Building Play Kid taught me more about computer architecture, bit manipulation, and systems programming than any tutorial could. Every component of the Game Boy (CPU, memory, graphics, audio) works in concert, and understanding their interactions required patience, testing, and a lot of debugging.&lt;/p&gt;
&lt;p&gt;Rust was an excellent choice for this project. My CHIP-8 emulator was already written in Rust, so this was a safe choice for me. I think the language has been praised enough at this time, so I&amp;rsquo;ll spare this part. SDL2 provided a simple, cross-platform foundation for rendering, input, and audio without unnecessary complexity.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in emulation, I think the Game Boy is a solid choice as first project if you have previous programming experience. The documentation is excellent and the hardware is approachable.&lt;/p&gt;
&lt;p&gt;Play Kid&amp;rsquo;s source code is on &lt;a href="https://codeberg.org/langurmonkey/playkid"&gt;Codeberg&lt;/a&gt; and &lt;a href="https://github.com/langurmonkey/playkid"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Google *unkills* JPEG XL?</title><link>https://tonisagrista.com/blog/2025/google-unkills-jpegxl/</link><pubDate>Fri, 28 Nov 2025 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2025/google-unkills-jpegxl/</guid><description>&lt;p&gt;I&amp;rsquo;ve written about JPEG XL in the past. First, I noted &lt;a href="https://tonisagrista.com/blog/2022/jpeg-xl-chrome"&gt;Google&amp;rsquo;s move to kill the format in Chromium&lt;/a&gt; in favor of the homegrown and inferior AVIF.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; Then, I had a deeper look at the format, and visually &lt;a href="https://tonisagrista.com/blog/2023/jpegxl-vs-avif"&gt;compared JPEG XL with AVIF&lt;/a&gt; on a handful of images.&lt;/p&gt;
&lt;p&gt;The latter post started with a quick support test:&lt;/p&gt;
&lt;p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;
&lt;source srcset="https://tonisagrista.com/img/2023/02/jxl-avif/support-jxl-yes.jxl" type="image/jxl" /&gt;
&lt;img src="https://tonisagrista.com/img/2023/02/jxl-avif/support-jxl-no.jpg" width="50%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;
&lt;source srcset="https://tonisagrista.com/img/2023/02/jxl-avif/support-avif-yes.avif" type="image/avif" /&gt;
&lt;img src="https://tonisagrista.com/img/2023/02/jxl-avif/support-avif-no.jpg" width="50%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;If you are browsing this page around 2023, chances are that your browser supports AVIF but does not support JPEG XL.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Well, here we are at the end of 2025, and this very sentence still holds true. Unless you are one of the 17% of users using Safari&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;, or are adventurous enough to use a niche browser like &lt;a href="https://thorium.rocks"&gt;Thorium&lt;/a&gt;, &lt;a href="https://librewolf.net/"&gt;LibreWolf&lt;/a&gt; or the newer &lt;a href="https://zen-browser.app"&gt;Zen Browser&lt;/a&gt;, chances are you see the AVIF banner in green and the JPEG XL image in black/red.&lt;/p&gt;
&lt;p&gt;The good news is, this will change soon. In a dramatic turn of events, the Chromium team has reversed its &lt;code&gt;Obsolete&lt;/code&gt; tag, and has decided to support the format in Blink (the engine behind Chrome/Chromium/Edge). Given Chrome&amp;rsquo;s position in the browser market share, I predict the format will become a &lt;em&gt;de factor&lt;/em&gt; standard for images in the near future.&lt;/p&gt;
&lt;h2 id="lets-recap"&gt;Let&amp;rsquo;s recap&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been following JPEG XL since its experimental support in Blink. What started as a promising feature was quickly axed by the team in a bizarre and ridiculous manner. First, they asked the community for feedback on the format. Then, the community responded very positively. And I don&amp;rsquo;t only mean a couple of guys in their basement. &lt;a href="https://issues.chromium.org/issues/40168998#comment17"&gt;Meta&lt;/a&gt;, &lt;a href="https://issues.chromium.org/issues/40168998#comment65"&gt;Intel&lt;/a&gt;, &lt;a href="https://issues.chromium.org/issues/40168998#comment71"&gt;Cloudinary&lt;/a&gt;, &lt;a href="https://issues.chromium.org/issues/40168998#comment39"&gt;Adobe&lt;/a&gt;, &lt;a href="https://issues.chromium.org/issues/40168998#comment69"&gt;&lt;code&gt;ffmpeg&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://issues.chromium.org/issues/40168998#comment70"&gt;&lt;code&gt;libvips&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://issues.chromium.org/issues/40168998#comment67"&gt;Krita&lt;/a&gt;, and many more. After that came the infamous comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href="mailto:da...@chromium.org"&gt;da...@chromium.org&lt;/a&gt;&lt;a href="mailto:da...@chromium.org"&gt;da...@chromium.org&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;#85 Oct 31, 2022 12:34AM&lt;/p&gt;
&lt;p&gt;Thank you everyone for your comments and feedback regarding JPEG XL. We will be removing the JPEG XL code and flag from Chromium for the following reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Experimental flags and code should not remain indefinitely&lt;/li&gt;
&lt;li&gt;There is not enough interest from the entire ecosystem to continue experimenting with JPEG XL&lt;/li&gt;
&lt;li&gt;The new image format does not bring sufficient incremental benefits over existing formats to warrant enabling it by default&lt;/li&gt;
&lt;li&gt;By removing the flag and the code in M110, it reduces the maintenance burden and allows us to focus on improving existing formats in Chrome&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes, right, &amp;ldquo;&lt;em&gt;not enough interest from the entire ecosystem&lt;/em&gt;&amp;rdquo;. Sure.&lt;/p&gt;
&lt;p&gt;Anyway, following this comment, a steady stream of messages pointed out how wrong that was, from all the organizations mentioned above and many more. People were noticing in blog posts, videos, and social media interactions.&lt;/p&gt;
&lt;p&gt;Strangely, the following few years have been pretty calm for JPEG XL. However, a few notable events did take place. First, the Firefox team &lt;a href="https://github.com/mozilla/standards-positions/pull/1064"&gt;showed interest in a JPEG XL Rust decoder&lt;/a&gt;, after describing their stance on the matter as &amp;ldquo;neutral&amp;rdquo;. They were concerned about the increased attack surface resulting from including the current 100K+ lines C++ &lt;a href="https://github.com/libjxl/libjxl"&gt;&lt;code&gt;libjxl&lt;/code&gt;&lt;/a&gt; reference decoder, even though most of those lines are testing code. In any case, they kind of requested a &amp;ldquo;memory-safe&amp;rdquo; decoder. This seems to have kick-started the Rust implementation, &lt;a href="https://chromium-review.googlesource.com/c/chromium/src/&amp;#43;/7184969"&gt;jxl-rs&lt;/a&gt;, from Google Research.&lt;/p&gt;
&lt;p&gt;To top it off, a couple of weeks ago, the PDF Association announced their intent to adopt JPEG XL as a preferred image format in their PDF specification. The CTO of the PDF Association, Peter Wyatt, expressed their desire to include JPEG XL as the preferred format for HDR content in PDF files.&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id="chromiums-new-stance"&gt;Chromium&amp;rsquo;s new stance&lt;/h2&gt;
&lt;p&gt;All of this pressure exerted steadily over time made the Chromium team reconsider the format. They tried to kill it in favor of AVIF, but that hasn&amp;rsquo;t worked out. Rick Byers, on behalf of Chromium, &lt;a href="https://groups.google.com/a/chromium.org/g/blink-dev/c/WjCKcBw219k/m/NmOyvMCCBAAJ"&gt;made a comment&lt;/a&gt; in the Blink developers Google group about the team welcoming a performant and memory-safe JPEG XL decoder in Chromium. He stated that the change of stance was in light of the positive signs from the community we have exposed above (Safari support, Firefox updating their position, PDF, etc.). Quickly after that, the &lt;a href="https://issues.chromium.org/issues/40168998#comment505"&gt;Chromium issue&lt;/a&gt; state was changed from &lt;code&gt;Obsolete&lt;/code&gt; to &lt;code&gt;Assigned&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="about-jpeg-xl"&gt;About JPEG XL&lt;/h2&gt;
&lt;p&gt;This is great news for the format, and I believe it will give it the final push for mass adoption. The format is excellent for all kinds of purposes, and I&amp;rsquo;ll be adopting it pretty much instantly for this and the Gaia Sky website when support is shipped. Some of the features that make it superior to the competition are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lossless re-compression of JPEG images. This means you can re-compress your current JPEG library without losing information and benefit from a ~30% reduction in file size for free. This is a killer feature that no other format has.&lt;/li&gt;
&lt;li&gt;Support for wide gamut and HDR.&lt;/li&gt;
&lt;li&gt;Support for image sizes of up to 1,073,741,823x1,073,741,824. You won&amp;rsquo;t run out of image space anytime soon. AVIF is ridiculous in this aspect, capping at 8,193x4,320. WebP goes up to 16K&lt;sup&gt;2&lt;/sup&gt;, while the original 1992 JPEG supports 64K&lt;sup&gt;2&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;Maximum of 32 bits per channel. No other format (except for the defunct JPEG 2000) offers this.&lt;/li&gt;
&lt;li&gt;Maximum of 4,099 channels. Most other formats support 4 or 5, with the exception of JPEG 2000, which supports 16,384.&lt;/li&gt;
&lt;li&gt;JXL is super resilient to generation loss.&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;JXL supports progressive decoding, which is essential for web delivery, IMO. WebP or HEIC have no such feature. Progressive decoding in AVIF was added a few years back.&lt;/li&gt;
&lt;li&gt;Support for animation.&lt;/li&gt;
&lt;li&gt;Support for alpha transparency.&lt;/li&gt;
&lt;li&gt;Depth map support.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a full codec feature breakdown, see &lt;a href="https://jpegxl.info/resources/battle-of-codecs.html"&gt;Battle of the Codecs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;JPEG XL is the future of image formats. It checks all the right boxes, and it checks them well. Support in the overwhelmingly most popular browser engine is probably going to be a crucial stepping stone in the format&amp;rsquo;s path to stardom. I&amp;rsquo;m happy that the Chromium team reconsidered their inclusion, but I am sad that it took so long and so much pressure from the community to achieve it.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;a href="https://aomediacodec.github.io/av1-avif/"&gt;https://aomediacodec.github.io/av1-avif/&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;a href="https://jpegxl.info/resources/battle-of-codecs.html"&gt;https://jpegxl.info/resources/battle-of-codecs.html&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;a href="https://radar.cloudflare.com/reports/browser-market-share-2025-q1"&gt;https://radar.cloudflare.com/reports/browser-market-share-2025-q1&lt;/a&gt;&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=DjUPSfirHek&amp;amp;t=2284s"&gt;https://www.youtube.com/watch?v=DjUPSfirHek&amp;t=2284s&lt;/a&gt;&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;&lt;a href="https://youtu.be/qc2DvJpXh-A"&gt;https://youtu.be/qc2DvJpXh-A&lt;/a&gt;&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Motion trails in Gaia Sky</title><link>https://tonisagrista.com/blog/2025/motion-trails/</link><pubDate>Sat, 14 Jun 2025 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2025/motion-trails/</guid><description>&lt;p&gt;Astronomical scenes, especially those rendered interactively, often feature supraluminal camera motion over immense distances. Sometimes, these motions are rendered by applying trail effects to light-emitting objects to enhance the faster-than-light velocity sensation. Gaia Sky will get an implementation of &lt;em&gt;motion trails&lt;/em&gt; in the next version (3.6.9). Motion trails are a visual effect that stretches stars, galaxies, and other light-emitting particles in the direction of the velocity vector of the camera, giving a sense of speed and enhancing the perception of motion through space. This technique is inspired by relativistic visualizations and classic star streak effects, but it is grounded in angular motion rather than raw velocity.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/06/motion-trails-starfield.jpg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Vertex-based stretching of stars in supraluminal travel in Gaia Sky.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In this post, I describe the technical details that made implementing a performant, vertex-based solution into Gaia Sky possible.&lt;/p&gt;
&lt;!-- Loading MathJax --&gt;
&lt;script type="text/javascript" id="MathJax-script" async src="https://tonisagrista.com/js/mathjax3.js"&gt;&lt;/script&gt;
&lt;h2 id="traditional-approach"&gt;Traditional approach&lt;/h2&gt;
&lt;p&gt;The typical way to render such an effect is through a &lt;strong&gt;post-processing motion blur pass&lt;/strong&gt;, especially common in game engines and cinematic visualizations. The process is roughly the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Separate Render Targets&lt;/strong&gt; &amp;mdash; stars, particles, or other emissive elements are rendered to dedicated framebuffers (with color and velocity).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Velocity Buffers&lt;/strong&gt; &amp;mdash; each rendered object encodes its screen-space motion vector based on differences between current and previous frame transforms.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Post-Processing Blur Shader&lt;/strong&gt; &amp;mdash; a fullscreen pixel shader smears pixels along their velocity vector, creating blur or streaks. This can be implemented with linear sampling or more advanced temporal accumulation techniques.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This method applies uniformly to all rendered elements, works in screen space, typically using a pixel shader, and reuses the pre-existing motion blur infrastructure in engines. However, it requires additional render targets and history buffers, it is almost impossible to control and isolate per-object, and it does not handle transparencies or additive blending correctly. What can we do about it?&lt;/p&gt;
&lt;h2 id="our-approach"&gt;Our approach&lt;/h2&gt;
&lt;p&gt;In Gaia Sky, the effect is instead implemented directly in the &lt;strong&gt;vertex shader&lt;/strong&gt;. Each object is a quad billboard oriented toward the camera. Trails are computed procedurally based on the screen-space velocity of the objects (difference in projected position over time steps), the stretch direction based on camera motion, and a distance-based fade-out.&lt;/p&gt;
&lt;p&gt;This technique is simpler and has some nice bonuses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It avoids history buffers or post-processing passes.&lt;/li&gt;
&lt;li&gt;It is fully compatible with additive blending and transparency.&lt;/li&gt;
&lt;li&gt;It scales well with hundreds of thousands of objects.&lt;/li&gt;
&lt;li&gt;It is tunable based on distance, screen velocity, and apparent motion.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short, this vertex-based approach integrates the effect tightly into Gaia Sky’s real-time astronomical renderer, improving performance, visual clarity, and configurability.&lt;/p&gt;
&lt;h2 id="the-shader"&gt;The shader&lt;/h2&gt;
&lt;p&gt;The code lives in the snippet that computes the billboard rotation, i.e., the part of the code that orients the quad to the camera to make it a billboard. This snippet uses the following inputs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s_vert_pos: vec4&lt;/code&gt; &amp;mdash; vertex position.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s_obj_pos: vec3&lt;/code&gt; &amp;mdash; object position w.r.t. camera.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s_proj_view: mat4&lt;/code&gt; &amp;mdash; camera view-projection matrix.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s_size: float&lt;/code&gt; &amp;mdash; quad size.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u_camUp: vec3&lt;/code&gt; &amp;mdash; world-space camera up vector.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u_camVel: vec3&lt;/code&gt; &amp;mdash; world-space camera velocity.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u_dt: float&lt;/code&gt; &amp;mdash; delta time in seconds between frames.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u_uToMpc: float&lt;/code&gt; &amp;mdash; conversion factor from internal units to Mpc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shader chunk computes the final vertex position and puts it into &lt;code&gt;gpos: vec4&lt;/code&gt;.
In the next subsections I break down the shader parts and explain each one separately.&lt;/p&gt;
&lt;h3 id="0-initial-orientation"&gt;0. Initial orientation&lt;/h3&gt;
&lt;p&gt;Here we just compute the orientation base of the quad/billboard, taking as reference the vector from the camera to the center of the object (&lt;code&gt;s_obj_pos&lt;/code&gt;).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;s_obj&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;normalize&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;s_obj_pos&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;s_right&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;normalize&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;cross&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;u_camUp&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;s_obj&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;s_up&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;normalize&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;cross&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;s_obj&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;s_right&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="1-rotation-and-velocity-projection"&gt;1. Rotation and velocity projection&lt;/h3&gt;
&lt;p&gt;The quad is oriented toward the camera using a rotation quaternion. We then rotate the camera velocity into the quad&amp;rsquo;s local space to determine stretch direction.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec4&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_quat&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;q_look_at&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;s_obj&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;s_up&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec4&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_quat_inv&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;q_conj&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;s_quat&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;local_pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_vert_pos&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;xyz&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;centered_pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;local_pos&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;local_cam_vel&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;qrot&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;s_quat_inv&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;u_camVel&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-estimate-screen-space-movement"&gt;2. Estimate screen-space movement&lt;/h3&gt;
&lt;p&gt;We simulate where the object would be next frame by offsetting it by the camera velocity &lt;code&gt;u_camVel&lt;/code&gt; using the frame time &lt;code&gt;u_dt&lt;/code&gt;. This gives a screen-space motion estimate in NDC (Normalized Device Coordinates). Note the division by the \(w\) component of clip coordinates, needed to convert to NDC.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;obj_world&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_obj_pos&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;obj_next&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;obj_world&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;u_camVel&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;u_dt&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec4&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;clip_now&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_proj_view&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec4&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;obj_world&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec4&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;clip_next&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_proj_view&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec4&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;obj_next&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec2&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ndc_now&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;clip_now&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;xy&lt;/span&gt; &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;clip_now&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;w&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec2&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ndc_next&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;clip_next&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;xy&lt;/span&gt; &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;clip_next&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;w&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;screenVel&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;length&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;ndc_now&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ndc_next&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-calculate-trail-stretch-amount"&gt;3. Calculate trail stretch amount&lt;/h3&gt;
&lt;p&gt;The stretch magnitude is driven by the screen-space velocity &lt;code&gt;screenVel&lt;/code&gt;, shaped using a nonlinear power function and clamped. The actual parameters need to be tuned to every scene.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;stretch_dir&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;normalize&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;vec2&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;local_cam_vel&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;x&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;local_cam_vel&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;y&lt;/span&gt;).&lt;span style="color:#58a1dd"&gt;xyx&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;stretch&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;pow&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;screenVel&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;450.0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;2.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;stretch&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;clamp&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;stretch&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0.0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;6.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-distance-based-fadeout"&gt;4. Distance-based fadeout&lt;/h3&gt;
&lt;p&gt;To avoid unnecessary GPU load from distant galaxies (which barely move visually), the effect fades out smoothly between 30 and 50 Mpc. This only ever affects very distant, high-z galaxies due to the distances involved in the thresholds.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;// Object distance in Mpc&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;distMpc&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;length&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;s_obj_pos&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;u_uToMpc&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;fade&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;smoothstep&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;50.0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;30.0&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;distMpc&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;stretch&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;fade&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="5-apply-trail-effect"&gt;5. Apply trail effect&lt;/h3&gt;
&lt;p&gt;Directionally stretch the quad along the motion vector. Brightness is adjusted to maintain consistent visual intensity. Otherwise, the stars appear brighter as they are stretched, which is bad.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;if&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;screenVel&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.0001&lt;/span&gt; &lt;span style="color:#ff636f"&gt;||&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;fade&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.01&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;stretch&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;} &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Stretch around center position&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;taper&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;dot&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;stretch_dir&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;centered_pos&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;centered_pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;stretch&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;taper&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;stretch_dir&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;brightnessScale&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2.0&lt;/span&gt; &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; (&lt;span style="color:#a6be9d"&gt;1.0&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;stretch&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;v_col&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rgb&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;clamp&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;brightnessScale&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0.0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="6-final-assembly"&gt;6. Final assembly&lt;/h3&gt;
&lt;p&gt;We recenter, scale, rotate back to world space, and project to clip space.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;local_pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;centered_pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_size&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;world_pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_obj_pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;qrot&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;s_quat&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;local_pos&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;// Final vertex position&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;gpos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;s_proj_view&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec4&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;world_pos&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;1.0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;You have already seen the image at the top of this post. But an image without motion is frankly unimpressive. You can see a video of the effect in this Bluesky post:&lt;/p&gt;
&lt;blockquote class="bluesky-embed" data-bluesky-uri="at://did:plc:lyzlck7lyaeo5g5xcu3raydh/app.bsky.feed.post/3lrq3hfuzt22y" data-bluesky-cid="bafyreia5ly3dthnl42xmu6aaglis75jfmt4eljndglpvdpa7iree35dcqe" data-bluesky-embed-color-mode="system"&gt;&lt;p lang="en"&gt;#GaiaSky will get a star streaks shader in the next release. This effect elongates stars in the direction of motion to create a sort of Star Trek travel effect. Of course, this can be turned off in the settings.&lt;br&gt;&lt;br&gt;&lt;a href="https://bsky.app/profile/did:plc:lyzlck7lyaeo5g5xcu3raydh/post/3lrq3hfuzt22y?ref_src=embed"&gt;[image or embed]&lt;/a&gt;&lt;/p&gt;&amp;mdash; Gaia Sky (&lt;a href="https://bsky.app/profile/did:plc:lyzlck7lyaeo5g5xcu3raydh?ref_src=embed"&gt;@gaiasky.bsky.social&lt;/a&gt;) &lt;a href="https://bsky.app/profile/did:plc:lyzlck7lyaeo5g5xcu3raydh/post/3lrq3hfuzt22y?ref_src=embed"&gt;Jun 16, 2025 at 15:26&lt;/a&gt;&lt;/blockquote&gt;&lt;script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"&gt;&lt;/script&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This vertex-based trail method avoids the cost and complexity of traditional post-processing while remaining physically intuitive and performant. This represents an ideal fit for real-time astronomical rendering, and for Gaia Sky in particular.&lt;/p&gt;
&lt;p&gt;Some improvements to consider for the future could be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Potential support for color or spectral shifts (red/blue) based on velocity and stretch.&lt;/li&gt;
&lt;li&gt;Adaptive fall-off curves could optimize performance.&lt;/li&gt;
&lt;li&gt;A GPU culling pass could eliminate distant objects entirely.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Quadruple joins the fight!</title><link>https://tonisagrista.com/blog/2025/quadruple-joins-party/</link><pubDate>Sat, 10 May 2025 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2025/quadruple-joins-party/</guid><description>&lt;!-- Loading MathJax --&gt;
&lt;script type="text/javascript" id="MathJax-script" async src="https://tonisagrista.com/js/mathjax3.js"&gt;&lt;/script&gt;
&lt;p&gt;A few days ago I wrote about &lt;a href="https://tonisagrista.com/blog/2025/apfloat-bigdecimal/"&gt;benchmarking arbitrary precision floating-point libraries in Java&lt;/a&gt;. I found out that &lt;code&gt;BigDecimal&lt;/code&gt; is not as slow as it is said to be, beating &lt;code&gt;Apfloat&lt;/code&gt; at the same precision level by a long margin in most operations. However, for &lt;a href="https://gaiasky.space"&gt;Gaia Sky&lt;/a&gt;, I don&amp;rsquo;t need hundreds of significant digits at all. It turns out 27 significant digits are enough to represent the whole universe with a precision of 1 meter.&lt;/p&gt;
&lt;p&gt;The observable universe has a radius of about \(4.4 \times 10^{26}\) meters. To express the entire range down to 1 meter, we need to calculate the number of significant digits \(d\) as follows:&lt;/p&gt;
&lt;p&gt;$$
\begin{align}
d &amp;amp;= \log_{10} \left(\frac{R}{\text{precision}}\right) \\ \\
&amp;amp;= \log_{10} \left(\frac{4.4 \times 10^{26}}{1}\right) \\ \\
&amp;amp;= \log_{10}(4.4 \times 10^{26}) \\ \\
&amp;amp;= \log_{10}(4.4) + \log_{10}(10^{26}) \\ \\
\approx 0.643 + 26 &amp;amp;= 26.643
\end{align}
$$&lt;/p&gt;
&lt;p&gt;In terms of bits, IEEE 754 double precision (64-bit) provides around 15–17 decimal digits of precision, which is enough for the Solar System, but insufficient for the whole universe. In contrast, IEEE 754 quadruple precision (128-bit) provides around 34 decimal digits of precision, which is in fact more than enough. It uses 113 bits of significand precision, \(log_{10}(2^{113}) \approx 34\) digits. The range of values we can precisely represent in the universe is \(\approx \frac{4.4 \times 10^{26}}{10^{34}} = 4.4 \times 10^{-8}\) meters. This is 4.4 nanometers! As said, this is more than sufficient for our purposes.&lt;/p&gt;
&lt;h2 id="enter-quadruple"&gt;Enter Quadruple&lt;/h2&gt;
&lt;p&gt;Browsing through GitHub I found the &lt;a href="https://github.com/m-vokhm/Quadruple"&gt;&lt;code&gt;Quadruple&lt;/code&gt; library&lt;/a&gt;, which provides an implementation of 128-bit floating point numbers in Java. The implementation is very compact, and includes &lt;strong&gt;addition&lt;/strong&gt;, &lt;strong&gt;subtraction&lt;/strong&gt;, &lt;strong&gt;multiplication&lt;/strong&gt;, &lt;strong&gt;division&lt;/strong&gt;, and &lt;strong&gt;square root&lt;/strong&gt;. I decided to put it to the test using my JMH benchmark.&lt;/p&gt;
&lt;p&gt;I created a new benchmark called &amp;ldquo;ThreeWay&amp;rdquo;, which tests these operations (plus allocation) for &lt;code&gt;Apfloat&lt;/code&gt;, &lt;code&gt;BigDecimal&lt;/code&gt;, and &lt;code&gt;Quadruple&lt;/code&gt;. In the arbitrary precision library I&amp;rsquo;m using only 32 significant digits of precision instead of 34. I do 1 set of 1 second as warm-up, and 5 iterations of 5 seconds for the measurement (see &lt;a href="https://codeberg.org/langurmonkey/java-arbitrary-precision-benchmark/src/branch/master/src/main/java/com/tonisagrista/ThreeWay.java"&gt;source&lt;/a&gt;).&lt;/p&gt;
&lt;h2 id="results"&gt;Results&lt;/h2&gt;
&lt;p&gt;Below are the specs of the system I used to run the tests, and the specific software versions used. This time around I ran the benchmarks in my laptop while it was plugged in. Only the CPU and the memory should play a significant role.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# JMH version: 1.37
# VM version: JDK 21.0.7, OpenJDK 64-Bit Server VM, 21.0.7+6
CPU: Intel(R) Core(TM) i7-8550U (8) @ 4.00 GHz
GPU 1: NVIDIA GeForce GTX 1070 [Discrete]
GPU: Intel UHD Graphics 620 @ 1.15 GHz [Integrated]
Memory: 16.00 GiB
Swap: 8.00 GiB
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here are the benchmark results.&lt;/p&gt;
&lt;h3 id="addition"&gt;Addition&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-TWAddition.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Three-way Addition results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-TWAddition.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Of course, &lt;code&gt;Quadruple&lt;/code&gt; is compact and only needs to care about 128 bits, while &lt;code&gt;Apfloat&lt;/code&gt; and &lt;code&gt;BigDecimal&lt;/code&gt; are generic to any precision, so we can expect &lt;code&gt;Quadruple&lt;/code&gt; to be faster. And it is.&lt;/p&gt;
&lt;h3 id="subtraction"&gt;Subtraction&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-TWSubtraction.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Three-way Subtraction results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-TWSubtraction.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Same with subtraction.&lt;/p&gt;
&lt;h3 id="multiplication"&gt;Multiplication&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-TWMultiplication.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Three-way Multiplication results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-TWMultiplication.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;And multiplication.&lt;/p&gt;
&lt;h3 id="division"&gt;Division&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-TWDivision.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Three-way Division results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-TWDivision.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Division is also faster with the newcomer.&lt;/p&gt;
&lt;h3 id="allocation-from-string"&gt;Allocation (from string)&lt;/h3&gt;
&lt;p&gt;Finally, the allocation test. First, we test allocation from a string representation of a floating point number.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-TWAlloc.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Three-way Allocation results (from string) &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-TWAlloc.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Surprising. Let&amp;rsquo;s analyze this. We use &lt;abbr title="Java Object Layout"&gt;JOL&lt;/abbr&gt; to find out the instance size of each object.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Quadruple&lt;/code&gt; has an instance size of 40 bytes (2 longs, 1 int, 1 boolean, plus header).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BigDecimal&lt;/code&gt; has an instance size of also 40 bytes (2 ints, 1 long, 2 references to &lt;code&gt;BigInteger&lt;/code&gt; and &lt;code&gt;String&lt;/code&gt;, plus header).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Apfloat&lt;/code&gt; has an instance size of 24 (3 references plus the object header).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is unlikely that the issue is the instance size. It most definitely comes down to the code to convert the string into the internal representation of each type. This code seems to be much slower for &lt;code&gt;Quardruple&lt;/code&gt; than it is for the others. Let&amp;rsquo;s see how it fares allocating from a &lt;code&gt;double&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="allocation-from-double"&gt;Allocation (from double)&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-TWAllocationDouble.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Three-way Allocation results (from double) &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-TWAllocationDouble.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The story is reversed. &lt;code&gt;Quadruple&lt;/code&gt; is much faster than the others when allocating an object from a &lt;code&gt;double&lt;/code&gt;. I never allocate from strings, so this is not that bad actually.&lt;/p&gt;
&lt;h2 id="analysis"&gt;Analysis&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s not much to say. &lt;code&gt;Quadruple&lt;/code&gt; is obviously much faster in a very significant way than the others. This is, of course, to be expected if we consider that &lt;code&gt;Quadruple&lt;/code&gt; only deals with float-128 types and does not have to care about higher precisions. It may be enough for your purposes, like it is for mine. If this is the case, it may make sense to use it.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats&lt;/h2&gt;
&lt;p&gt;There are a couple of important caveats to consider if you want to use &lt;code&gt;Quadruple&lt;/code&gt; as it is now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Only the basic operations are implemented (add, sub, div, mul, sqrt). If you need anything else, you are on your own.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Quadruple&lt;/code&gt; instances are &lt;strong&gt;mutable&lt;/strong&gt;. This is a bad design decision in my opinion, and would bar it from adopting further improvements that will land soon to Java like value types (&lt;a href="https://en.wikipedia.org/wiki/Project_Valhalla_%28Java_language%29"&gt;project Valhalla&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Instantiation from &lt;code&gt;String&lt;/code&gt; is very slow.&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Benchmarking arbitrary precision libraries in Java</title><link>https://tonisagrista.com/blog/2025/apfloat-bigdecimal/</link><pubDate>Wed, 07 May 2025 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2025/apfloat-bigdecimal/</guid><description>&lt;p&gt;&lt;em&gt;&lt;span class="orange"&gt;Edit (2025-05-08):&lt;/span&gt;
Changed some test parameters and re-run the tests. Also added bar plots.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note: I have since &lt;a href="https://tonisagrista.com/blog/2025/quadruple-joins-party/"&gt;written a new blog&lt;/a&gt; which includes &lt;a href="https://m-vokhm.github.io/Quadruple/"&gt;Quadruple&lt;/a&gt; to the benchmarks, beating both &lt;code&gt;Apfloat&lt;/code&gt; and &lt;code&gt;BigDecimal&lt;/code&gt; consistently.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I recently set out to compare the performance of &lt;a href="http://www.apfloat.org"&gt;&lt;code&gt;Apfloat&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/math/BigDecimal.html"&gt;&lt;code&gt;BigDecimal&lt;/code&gt;&lt;/a&gt; for arbitrary precision arithmetic in Java. I use arbitrary precision floating point numbers in key places of the update cycle in Gaia Sky, so it made sense to explore this. My initial approach was a naive benchmark: a simple &lt;code&gt;main()&lt;/code&gt; method running arithmetic operations in a loop and measuring the time taken. The results were strongly in favor of &lt;code&gt;BigDecimal&lt;/code&gt;, even for large precision values. This was unexpected, as the general consensus I &lt;a href="https://stackoverflow.com/questions/277309/java-floating-point-high-precision-library"&gt;found&lt;/a&gt; &lt;a href="https://groups.google.com/g/javaposse/c/YDYDPbzxntc?pli=1"&gt;online&lt;/a&gt; &lt;a href="http://www.apfloat.org/apfloat_java/"&gt;suggested&lt;/a&gt; that &lt;code&gt;Apfloat&lt;/code&gt; is more performant, especially for higher precision operations (hundreds of digits).&lt;/p&gt;
&lt;p&gt;To get more accurate and reliable measurements, I decided to implement a proper &lt;abbr title="Java Microbenchmark Harness"&gt;JMH&lt;/abbr&gt; benchmark. The benchmark project source is available in &lt;a href="https://codeberg.org/langurmonkey/java-arbitrary-precision-benchmark"&gt;this repository&lt;/a&gt;. The benchmarks test addition, subtraction, multiplication, division, power, natural logarithm, and sine for both &lt;code&gt;Apfloat&lt;/code&gt; and &lt;code&gt;BigDecimal&lt;/code&gt; at different precision levels.&lt;/p&gt;
&lt;h2 id="why-jmh"&gt;Why JMH?&lt;/h2&gt;
&lt;p&gt;JMH is a benchmarking framework specifically designed for measuring performance in Java applications. It is developed by the OpenJDK team and provides a robust methodology for generating reliable and reproducible benchmark results by accounting for JVM warm-up, runtime optimizations, and other factors that can skew measurements. Given the surprising results in the naive implementation, using JMH allowed me to get more accurate measurements and mitigate potential inaccuracies caused by JVM behavior.&lt;/p&gt;
&lt;h2 id="the-benchmark-implementation"&gt;The Benchmark Implementation&lt;/h2&gt;
&lt;p&gt;The JMH benchmark project is structured to measure the average time taken for each arithmetic operation over several iterations and precision levels. Here&amp;rsquo;s the structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Separate benchmarks for &lt;strong&gt;addition&lt;/strong&gt;, &lt;strong&gt;subtraction&lt;/strong&gt;, &lt;strong&gt;multiplication&lt;/strong&gt;, &lt;strong&gt;division&lt;/strong&gt;, &lt;strong&gt;natural logarithm&lt;/strong&gt;, &lt;strong&gt;power&lt;/strong&gt;, and &lt;strong&gt;sine&lt;/strong&gt;, additionally to an &lt;strong&gt;allocation&lt;/strong&gt; test.&lt;/li&gt;
&lt;li&gt;Each benchmark tests &lt;code&gt;Apfloat&lt;/code&gt; and &lt;code&gt;BigDecimal&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create the actual objects at benchmark level to factor out allocation costs. Specific benchmark to test allocation overhead.&lt;/li&gt;
&lt;li&gt;Settled on four precision levels, on a scale ranging from &lt;em&gt;low&lt;/em&gt; and &lt;em&gt;high&lt;/em&gt; precision settings, represented as the number of digits. They are &lt;strong&gt;25&lt;/strong&gt;, &lt;strong&gt;50&lt;/strong&gt;, &lt;strong&gt;500&lt;/strong&gt;, and &lt;strong&gt;1000&lt;/strong&gt; digits.&lt;/li&gt;
&lt;li&gt;Average time mode.&lt;/li&gt;
&lt;li&gt;Every benchmark function only runs one operation once. The allocation test creates a couple of objects and consumes them.&lt;/li&gt;
&lt;li&gt;One warm-up iterations of one second each to minimize JVM effects (&lt;code&gt;@Warmup(iterations = 1, time = 1)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Three main iterations of five seconds each for the measurement (&lt;code&gt;@Measurement(iterations = 3, time = 5)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Send results into &lt;code&gt;Blackhole&lt;/code&gt; to prevent JIT optimizations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is an example for the &lt;code&gt;Sin&lt;/code&gt; benchmark:&lt;/p&gt;
&lt;details class="collapsedcode"&gt;
&lt;summary&gt;
&lt;span class="code"&gt;Code: &lt;/span&gt;&lt;span class="summary"&gt;benchmark implementation&lt;/span&gt;
&lt;/summary&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#1d2432;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;h3 id="the-results"&gt;The Results&lt;/h3&gt;
&lt;p&gt;Below are the specs of the system I used to run the tests and the specific software versions used. Only the CPU and the memory should play a significant role.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# JMH version: 1.37
# VM version: JDK 21.0.7, OpenJDK 64-Bit Server VM, 21.0.7+6
CPU: Intel(R) Core(TM) i7-7700 (8) @ 4.20 GHz
GPU 1: NVIDIA GeForce GTX 1070 [Discrete]
GPU 2: Intel HD Graphics 630 [Integrated]
Memory: 32.00 GiB
Swap: 8.00 GiB
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And here are the benchmark results.&lt;/p&gt;
&lt;h3 id="addition"&gt;Addition&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-Addition.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Addition results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-Addition.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;We already see that &lt;code&gt;BigDecimal&lt;/code&gt; is much faster in all precisions. It is not even close.&lt;/p&gt;
&lt;h3 id="subtraction"&gt;Subtraction&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-Subtraction.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Subtraction results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-Subtraction.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In the subtraction benchmark &lt;code&gt;BigDecimal&lt;/code&gt; comes out on top as well.&lt;/p&gt;
&lt;h3 id="multiplication"&gt;Multiplication&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-Multiplication.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Multiplication results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-Multiplication.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The same story repeats for multiplication.&lt;/p&gt;
&lt;h3 id="division"&gt;Division&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-Division.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Division results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-Division.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Again. Division is a notoriously costly operation, but &lt;code&gt;BigDecimal&lt;/code&gt; still comes out comfortably on top.&lt;/p&gt;
&lt;p&gt;Now, let&amp;rsquo;s test some more involved arithmetic operations, like the natural logarithm, the sine, and the power function. In &lt;code&gt;Apfloat&lt;/code&gt;, those are directly implemented in the library. For &lt;code&gt;BigDecimal&lt;/code&gt;, we use the &lt;a href="https://github.com/eobermuhlner/big-math"&gt;&lt;code&gt;big-math&lt;/code&gt; project&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="log"&gt;Log&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-Log.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Log results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-Log.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The logarithm is faster with &lt;code&gt;Apfloat&lt;/code&gt; at the higher precision settings, but it &lt;code&gt;BigDecimal&lt;/code&gt; still wins in the lower precisions.&lt;/p&gt;
&lt;h3 id="sin"&gt;Sin&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-Sin.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Sin results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-Sin.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The sine is much faster in &lt;code&gt;BigDecimal&lt;/code&gt; in all precision settings.&lt;/p&gt;
&lt;h3 id="pow"&gt;Pow&lt;/h3&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-Pow.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Pow results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-Pow.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;And finally, the power repeats the same story, with &lt;code&gt;BigDecimal&lt;/code&gt; sitting comfortably on the throne again.&lt;/p&gt;
&lt;h3 id="allocation"&gt;Allocation&lt;/h3&gt;
&lt;p&gt;For science, I thought it would be cool to test the allocation overhead, so I prepared the &lt;strong&gt;Allocation&lt;/strong&gt; test, which allocates two instances of either &lt;code&gt;Apfloat&lt;/code&gt; or &lt;code&gt;BigDecimal&lt;/code&gt; and consumes them.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/05/jmh-result-Allocation.svg" width="100%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:100%;"&gt;
&lt;h4&gt;Allocation results &amp;ndash; &lt;a href="https://jmh.morethan.io/?source=https://tonisagrista.com/files/2025/apfloat-bigdecimal/jmh-result-Allocation.json"&gt;Interactive view&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;We see that allocation is very costly in both libraries. However, while &lt;code&gt;Apfloat&lt;/code&gt; seems to be roughly constant with the precision, &lt;code&gt;BigDecimal&lt;/code&gt; shows a higher cost with 25 digits, the lowest precision setting. I though this was very sus, so I re-ran the test a bunch of times and with more iterations and longer times, and got back the same result. I&amp;rsquo;m not sure what&amp;rsquo;s the root cause for this, but it is surprising and intriguing.&lt;/p&gt;
&lt;p&gt;Since both &lt;code&gt;Apfloat&lt;/code&gt; and &lt;code&gt;BigDecimal&lt;/code&gt; are immutable, allocation costs need to be factored in. New objects need to be allocated every time new operands are needed.&lt;/p&gt;
&lt;h2 id="analysis"&gt;Analysis&lt;/h2&gt;
&lt;p&gt;Contrary to expectations, &lt;code&gt;BigDecimal&lt;/code&gt; consistently outperformed &lt;code&gt;Apfloat&lt;/code&gt; across all operations and precision levels, including the higher precisions (500 and 1000 digits) where &lt;code&gt;Apfloat&lt;/code&gt; was expected to excel. There is a single case when &lt;code&gt;Apfloat&lt;/code&gt; is faster, and that is in the high precision natural logarithm benchmark. I think it&amp;rsquo;s safe to say that this is due to the particular implementation or algorithm being used. Otherwise, the disparity is particularly noticeable in division and sine operations, where &lt;code&gt;Apfloat&lt;/code&gt; is significantly slower than &lt;code&gt;BigDecimal&lt;/code&gt;.
Specifically, &lt;code&gt;BigDecimal&lt;/code&gt; was several times faster than &lt;code&gt;Apfloat&lt;/code&gt; in most operations and precisions. Those are, in my opinion, significant results.&lt;/p&gt;
&lt;p&gt;Finally, allocation seems to be faster with &lt;code&gt;Apfloat&lt;/code&gt;, and there&amp;rsquo;s a weird dependency on the precision for &lt;code&gt;BigDecimal&lt;/code&gt; which I found strange.&lt;/p&gt;
&lt;h2 id="questions-and-next-steps"&gt;Questions and Next Steps&lt;/h2&gt;
&lt;p&gt;I was genuinely surprised by the outcome of these benchmarks, as it contradicts the general consensus regarding &lt;code&gt;Apfloat&lt;/code&gt;’s supposed performance advantage in high-precision arithmetic. I am reaching out to the community to validate my methodology and results. Are these findings trustworthy, or did I overlook something crucial in my benchmarking approach? Feedback and insights are very much welcome.&lt;/p&gt;</description></item><item><title>Oh Garmin, not you!</title><link>https://tonisagrista.com/blog/2025/garmin-not-you/</link><pubDate>Sat, 29 Mar 2025 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2025/garmin-not-you/</guid><description>&lt;p&gt;I&amp;rsquo;ve been a Garmin user for many years, shelling out non-trivial amounts of monies for their sports watches. My first Garmin watch was a &lt;a href="https://www.garmin.com/en-US/p/107143/pn/010-01039-00"&gt;Forerunner 10&lt;/a&gt; (black/red). Battery life was abysmal, but back then this was the norm. Today, I&amp;rsquo;m sporting a &lt;a href="https://www.garmin.com/en-US/p/780139"&gt;Forerunner 255&lt;/a&gt;, which I love. It&amp;rsquo;s not top-of-the-line, but it is more than enough for my modest purposes. These devices have been my trusty companions on countless runs, football matches, and hikes, providing invaluable data without any hidden costs. But now, Garmin has decided to introduce Garmin Connect+, a subscription service priced at €8.99 per month or €89.99 annually. Really, Garmin?​&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;
&lt;source srcset="https://tonisagrista.com/img/2025/03/fr255.jxl" type="image/jxl" /&gt;
&lt;source srcset="https://tonisagrista.com/img/2025/03/fr255.avif" type="image/avif" /&gt;
&lt;img src="https://tonisagrista.com/img/2024/05/phd-hats.jpg" width="75%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:75%;"&gt;
&lt;h4&gt;My Garmin Forerunner 255.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;!-- more --&gt;
&lt;p&gt;This past Thursday, March 27th, Garmin announced a premium plan called &lt;strong&gt;Garmin Connect+&lt;/strong&gt; in a press release titled &lt;a href="https://www.garmin.com/en-US/newsroom/press-release/wearables-health/elevate-your-health-and-fitness-goals-with-garmin-connect/"&gt;&amp;ldquo;Elevate your health and fitness goals with Garmin Connect+&amp;rdquo;&lt;/a&gt;. This new service promises AI-powered insights through their &amp;ldquo;Active Intelligence&amp;rdquo; feature, offering personalized suggestions based on health and activity data. Additionally, it includes a performance dashboard with custom charts, enhanced LiveTrack features, and exclusive app badges. I don&amp;rsquo;t care for any of that, but these additions may sound appealing to some. I can&amp;rsquo;t help but feel a sense of &lt;em&gt;déjà vu&lt;/em&gt;. Initially, companies introduce new features behind a paywall, assuring users that existing functionalities remain free. But as revenue targets loom, there&amp;rsquo;s a creeping tendency to lock previously free features behind that same paywall, degrading the user experience. This gradual decline, often referred to as &lt;em&gt;enshittification&lt;/em&gt;, has plagued many platforms.&lt;/p&gt;
&lt;p&gt;Garmin&amp;rsquo;s move mirrors trends seen with other fitness platforms like Strava, which have also introduced subscription models for premium features. While it&amp;rsquo;s understandable that publicly traded companies seek additional revenue streams, it&amp;rsquo;s disheartening when it comes at the expense of loyal users who have already invested heavily in their products. The general feeling of their customers is clear. You just need to look around the &lt;a href="https://reddit.com/r/garmin"&gt;Garmin subreddit&lt;/a&gt;, or the &lt;a href="https://www.reddit.com/r/Garmin/comments/1jl9mef/garmin_subscription_model_megathread/"&gt;Garmin Subscription Model megathread&lt;/a&gt; specifically, to know what I&amp;rsquo;m talking about.&lt;/p&gt;
&lt;p&gt;Of course, as these things usually go, Garmin assures us that all existing features will remain free and that Connect+ merely adds new functionalities. I don&amp;rsquo;t buy it. Forgive me for being skeptical. We&amp;rsquo;ve seen this play out before, and it often doesn&amp;rsquo;t end well for the consumer.​&lt;/p&gt;
&lt;p&gt;Garmin watches are already a significant investment. Introducing a subscription model feels like a cash grab, especially when the features offered seem like they could be standard inclusions. It&amp;rsquo;s a slippery slope, and I fear we&amp;rsquo;re at the top, ready to slide down. I hope Garmin proves me wrong and maintains the integrity of its free offerings. But history suggests otherwise, and I can&amp;rsquo;t help but brace for the inevitable &lt;em&gt;enshittification&lt;/em&gt; of a platform I once held in high regard.&lt;/p&gt;
&lt;p&gt;One thing is clear to me. My next sports watch will not be a Garmin watch. This move has annihilated my trust in the company. I&amp;rsquo;ve started researching other options, and I have found &lt;a href="https://polar.com"&gt;Polar&lt;/a&gt; and &lt;a href="https://suunto.com"&gt;Suunto&lt;/a&gt;, both based in Europe, to be excellent alternatives.&lt;/p&gt;</description></item></channel></rss>