<?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>Wed, 04 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tonisagrista.com/index.xml" rel="self" type="application/rss+xml"/><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 Sagrista Selles</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 quantization, which trims down the bit count per parameter to achieve lower memory usage and (sometimes) faster inference. 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;/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;Q4_K_M&lt;/strong&gt; seems to be the community sweet spot, as it offers ~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;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;ul&gt;
&lt;li&gt;Best balance \(\rightarrow\) Q4_K_M (default recommendation)&lt;/li&gt;
&lt;li&gt;Maximum quality, still compressed \(\rightarrow\) Q5_K_M or Q6_K&lt;/li&gt;
&lt;li&gt;Tight VRAM, acceptable quality loss \(\rightarrow\) Q4_K_S or IQ4_XS&lt;/li&gt;
&lt;li&gt;Absolute smallest file \(\rightarrow\) IQ3_XS / Q3_K_S (test quality first)&lt;/li&gt;
&lt;li&gt;Near-original accuracy \(\rightarrow\) Q8_0 or keep FP16/BF16&lt;/li&gt;
&lt;li&gt;Running on CPU \(\rightarrow\) Q4_K_M or IQ4_NL for better decode speed&lt;/li&gt;
&lt;li&gt;Fitting a 70B model on 24GB VRAM \(\rightarrow\) IQ3_S or Q3_K_M + CPU offload&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusions"&gt;Conclusions&lt;/h2&gt;
&lt;p&gt;As you see, this looks like the wild west at first glance, but this mess is not without order. Most importantly, there are 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 (Q4_K_M, IQ4_XS) 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;</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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;@BenchmarkMode&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Mode&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;AverageTime&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;@OutputTimeUnit&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;TimeUnit&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;NANOSECONDS&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;@Fork&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;value&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;@Warmup&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;iterations&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;1&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;time&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;@Measurement&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;iterations&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;3&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;time&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&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;public&lt;/span&gt; &lt;span style="color:#ff636f"&gt;abstract&lt;/span&gt; &lt;span style="color:#ff636f"&gt;class&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;BaseBenchmark&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;@State&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Scope&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;Thread&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;public&lt;/span&gt; &lt;span style="color:#ff636f"&gt;static&lt;/span&gt; &lt;span style="color:#ff636f"&gt;class&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;BenchmarkState&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;MathContext&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;mc&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;BigDecimal&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;aBD&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;bBD&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Apfloat&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;aAF&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;bAF&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;@Param&lt;/span&gt;({ &lt;span style="color:#a6be9d"&gt;&amp;#34;25&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;50&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;500&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;1000&amp;#34;&lt;/span&gt; }) &lt;span style="color:#828b96;font-style:italic"&gt;// Add different precision levels here&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;int&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;precision&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;@Setup&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;Level&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;Trial&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;public&lt;/span&gt; &lt;span style="color:#ff636f"&gt;void&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;setUp&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;mc&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;new&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;MathContext&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;precision&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;aBD&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;new&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;BigDecimal&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;12345.678901234567890123456789012345678934343434343434343434343434343434&amp;#34;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;mc&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;aAF&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;new&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Apfloat&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;12345.678901234567890123456789012345678934343434343434343434343434343434&amp;#34;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;precision&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;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;public&lt;/span&gt; &lt;span style="color:#ff636f"&gt;class&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Sin&lt;/span&gt; &lt;span style="color:#ff636f"&gt;extends&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;BaseBenchmark&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;@Benchmark&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;public&lt;/span&gt; &lt;span style="color:#ff636f"&gt;void&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;BigDecimalSin&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;BenchmarkState&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;state&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;Blackhole&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bh&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;var&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;BigDecimalMath&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;sin&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;state&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;aBD&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;state&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;mc&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;bh&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;consume&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;result&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;@Benchmark&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;public&lt;/span&gt; &lt;span style="color:#ff636f"&gt;void&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ApfloatSin&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;BenchmarkState&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;state&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;Blackhole&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;bh&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;var&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;ApfloatMath&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;sin&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;state&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;aAF&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;bh&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;consume&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;result&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;/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><item><title>Building a local AI assistant with user context</title><link>https://tonisagrista.com/blog/2025/gaiasky-ai-assistant/</link><pubDate>Wed, 26 Mar 2025 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2025/gaiasky-ai-assistant/</guid><description>&lt;p&gt;In my last &lt;a href="https://tonisagrista.com/blog/2025/local-llm-rag"&gt;post&lt;/a&gt;, I explored the concept of Retrieval-Augmented Generation (RAG) to enable a locally running generative AI model to access and incorporate new information. To achieve this, I used hardcoded documents as context, which were then embedded as vectors and persisted into Chroma DB. These vectors are used during inference to use as context for a local LLM chatbot.
But using a few hardcoded sentences is hardly elegant or particularly exciting. It&amp;rsquo;s alright for educational purposes, but that&amp;rsquo;s as far as it goes. However, if we need to build a &lt;em&gt;minimally useful system&lt;/em&gt;, we need to be more sophisticated than this. In this new post, I set out to create a local Gaia Sky assistant by using the &lt;a href="http://docs.gaiasky.space"&gt;Gaia Sky documentation site&lt;/a&gt; and the &lt;a href="https://gaiasky.space"&gt;Gaia Sky homepage&lt;/a&gt; as supplementary information, and leveraging Ollama to generate context-aware responses. So, let’s dive into the topic and explain how it all works.&lt;/p&gt;
&lt;p&gt;The source code used in this post is available &lt;a href="https://codeberg.org/langurmonkey/gaiasky-ai"&gt;in this repository&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="scraping-the-websites"&gt;Scraping the websites&lt;/h2&gt;
&lt;p&gt;The first thing we need to do is extract useful content from our sources. Since we use websites, this is where web scraping comes into play:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;We’re using the &lt;code&gt;requests&lt;/code&gt; library to fetch the HTML content from a given URL.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;First, we get all the internal links for every URL by recursively scraping the content (with &lt;code&gt;BeautifulSoup&lt;/code&gt;) and looking for all &lt;code&gt;a&lt;/code&gt; tags (anchors) that don&amp;rsquo;t point to internal anchors. This happens in &lt;code&gt;get_all_doc_links(base_url)&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;del&gt;Then, we extract the text from every page by extracting the tags &lt;code&gt;h1&lt;/code&gt;, &lt;code&gt;h2&lt;/code&gt;, &lt;code&gt;h3&lt;/code&gt;, &lt;code&gt;h4&lt;/code&gt;, &lt;code&gt;h5&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;li&lt;/code&gt;, &lt;code&gt;td&lt;/code&gt;, and &lt;code&gt;article&lt;/code&gt;. This step required a little bit of trial and error. Once we have the text for each page, we concatenate everything and return it. This is implemented mostly in &lt;code&gt;extract_text_from_page(url)&lt;/code&gt;&lt;/del&gt;. At first, I wrote my own code to retrieve content from a URL and convert it to text. Of course, this code was less than impressive, and even though it worked, the results were not optimal. Since then I experimented with other methods to extract text from websites, like the libraries &lt;code&gt;newspaper3K&lt;/code&gt;, &lt;code&gt;goose3&lt;/code&gt;, and &lt;code&gt;html2text&lt;/code&gt;. I settled for the latter, as it produces Markdown-formatted documents which are particularly well suited for LLMs. You can see the very different results from the previous method by inspecting the &lt;a href="https://codeberg.org/langurmonkey/website-source/commits/branch/master/content/blog/2025/gaiasky-ai-assistant.md"&gt;history of this post&lt;/a&gt;. The initial scraping behavior can still be accessed in the application with the flag &lt;code&gt;--local-scraper&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;As we mentioned earlier, we make sure to avoid non-HTML files and internal page anchors (those pesky # URLs that only point to a specific section). This ensures that we only scrape actual webpages that contain relevant data.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this method, we capture all the relevant documentation&amp;mdash;every corner of the site. Since the process is recursive, we don’t miss any pages (unless they’re blocked or have weird redirect loops, but that&amp;rsquo;s a discussion for another time).&lt;/p&gt;
&lt;p&gt;The code box below shows the link gathering, scraping, and text extraction.&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;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;os&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 user agent enviornment variable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;user_agent&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36&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;os&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;environ&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#34;USER_AGENT&amp;#34;&lt;/span&gt;] &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;user_agent&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;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;requests&lt;/span&gt;&lt;span style="color:#ff636f"&gt;,&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ollama&lt;/span&gt;&lt;span style="color:#ff636f"&gt;,&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;argparse&lt;/span&gt;&lt;span style="color:#ff636f"&gt;,&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;readline&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;termcolor&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;colored&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;bs4&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;BeautifulSoup&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;urllib.parse&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;urljoin&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;urlparse&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;langchain_community.document_loaders&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;WebBaseLoader&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;langchain_chroma&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Chroma&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;langchain_ollama&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;OllamaLLM&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;langchain.text_splitter&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;RecursiveCharacterTextSplitter&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;langchain.chains&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;RetrievalQA&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;langchain_huggingface&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;HuggingFaceEmbeddings&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;headers&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;User-Agent&amp;#34;&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;user_agent&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;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;get_all_doc_links&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;base_url&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;Finds all internal links within the documentation site.&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;visited&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;set&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;to_visit&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; {&lt;span style="color:#58a1dd"&gt;base_url&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;while&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to_visit&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;url&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to_visit&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&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;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;Scraping &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;colored&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;url&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;blue&amp;#39;&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:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;url&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;visited&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;visited&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;add&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;url&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;response&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;requests&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;get&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;url&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;headers&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;headers&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;response&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;status_code&lt;/span&gt; &lt;span style="color:#ff636f"&gt;!=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;200&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;continue&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;soup&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;BeautifulSoup&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;response&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;text&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;html.parser&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;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;link&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;soup&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;find_all&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;a&amp;#34;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;href&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;full_url&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;urljoin&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;base_url&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;link&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#34;href&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;parsed&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;urlparse&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;full_url&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;# Ignore anchors (#...) and non-HTML files&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;parsed&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;fragment&lt;/span&gt; &lt;span style="color:#ff636f"&gt;or&lt;/span&gt; &lt;span style="color:#ff636f"&gt;not&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;parsed&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;endswith&lt;/span&gt;((&lt;span style="color:#a6be9d"&gt;&amp;#34;.html&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&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;continue&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;base_url&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;full_url&lt;/span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;full_url&lt;/span&gt; &lt;span style="color:#ff636f"&gt;not&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;visited&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;to_visit&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;add&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;full_url&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;visited&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;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;extract_text_from_page&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;url&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 meaningful text from a given URL using html2text.&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;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;html2text&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;response&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;requests&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;get&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;url&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 style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;html2text&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;HTML2Text&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 style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;ignore_links&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;False&lt;/span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Set this to True to ignore links&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;h&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;handle&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;response&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;text&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;except&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:#ff636f"&gt;None&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;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;scrape_urls&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;base_urls&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;local_scraper&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;&amp;#34;&amp;#34;&amp;#34;Scrape and extract text content from multiple URLs.&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;all_text&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&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;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;base_url&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;base_urls&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;all_links&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;get_all_doc_links&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;base_url&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;Found &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;len&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;all_links&lt;/span&gt;)&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt; pages to scrape...&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;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;Extracting text from pages...&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;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;url&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;all_links&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;page_text&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;extract_text_from_page&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;url&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;page_text&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;all_text&lt;/span&gt; &lt;span style="color:#ff636f"&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;\n\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;### &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;url&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;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;page_text&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;all_text&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;# Main body&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;urls&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; [&lt;span style="color:#a6be9d"&gt;&amp;#34;https://gaia.ari.uni-heidelberg.de/gaiasky/docs/master/&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;https://gaiasky.space&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:#58a1dd"&gt;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Starting scraping...&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;doc_text&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;scrape_urls&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;urls&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="storing-embeddings-with-chromadb"&gt;Storing embeddings with ChromaDB&lt;/h2&gt;
&lt;p&gt;Once we&amp;rsquo;ve scraped the content, it’s time to turn that raw text into something that the machine can understand. This is where embeddings come into play.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;We use the &lt;code&gt;langchain&lt;/code&gt; library to split the scraped text into manageable chunks. This is done using the &lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt;, which splits the content into chunks of a predefined size (to avoid overloading the model). I tuned this by trial and error, and ended up with a chunk size of 1000, and a chunk overlap of 200.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The text is then passed through an embedding model, specifically the &amp;ldquo;sentence-transformers/all-MiniLM-L6-v2&amp;rdquo; model from &lt;a href="https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2"&gt;HuggingFace&lt;/a&gt;, which transforms it into a high-dimensional vector representation. It is super compact and performs well. I have tried other embed models (see commented out lines at the top of &lt;a href="#code-embed"&gt;this code block&lt;/a&gt;), but in my experience they did not perform better than &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally, we store these vectors in a vector database, Chroma DB. The vectors are indexed so that when we query the database, it can efficiently retrieve the most relevant pieces of text. For the retrieval, I&amp;rsquo;m using a default of 6 results (&lt;code&gt;search_kwargs&lt;/code&gt; in &lt;code&gt;as_retriever()&lt;/code&gt; method). I tried with 2 and 3, but it seemed that most models didn&amp;rsquo;t get enough context.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where the magic happens. By converting text into vectors, we enable the chatbot to compare and understand the semantic meaning of different pieces of text. The advantage here is that embeddings can capture the relationships between words and concepts, even if they’re not an exact match — meaning the chatbot can intelligently pull out relevant context to answer your questions.&lt;/p&gt;
&lt;p&gt;The relevant code consists of the concatenated texts we produced in the previous step (we called it &lt;code&gt;doc_text&lt;/code&gt;), and the location for our disk database:&lt;/p&gt;
&lt;p&gt;&lt;a name="code-embed"&gt;&lt;/a&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:#58a1dd"&gt;embed_model&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;sentence-transformers/all-MiniLM-L6-v2&amp;#34;&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;# embed_model = &amp;#34;BAAI/bge-base-en-v1.5&amp;#34;&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;# embed_model = &amp;#34;HIT-TMG/KaLM-embedding-multilingual-mini-v1&amp;#34;&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;# embed_model = &amp;#34;Linq-AI-Research/Linq-Embed-Mistral&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;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;store_embeddings&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;texts&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;db_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;Tokenizes, embeds, and stores the texts in a ChromaDB vector store.&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;text_splitter&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;chunk_size&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;1000&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;chunk_overlap&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;200&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;documents&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;text_splitter&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;create_documents&lt;/span&gt;([&lt;span style="color:#58a1dd"&gt;texts&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;documents&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;raise&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ValueError&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;No documents found to embed. Check the scraper output.&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:#58a1dd"&gt;embeddings&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;HuggingFaceEmbeddings&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;model_name&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;embed_model&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;vector_store&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Chroma&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;from_documents&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;documents&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;embeddings&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;persist_directory&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;db_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;return&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;vector_store&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;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Storing embeddings in ChromaDB...&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;db_path&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;chroma_db&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;vector_store&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;store_embeddings&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;doc_text&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;db_path&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="querying-with-ollama"&gt;Querying with Ollama&lt;/h2&gt;
&lt;p&gt;Now that we’ve got our indexed data, it’s time to query it. But instead of just returning raw search results, we want to use an LLM (Large Language Model) to generate smart, context-aware answers. Enter Ollama.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;We use &lt;code&gt;langchain&lt;/code&gt;’s &lt;code&gt;create_stuff_documents_chain&lt;/code&gt; and &lt;code&gt;create_retrieval_chain&lt;/code&gt; to first create a chain to retrieve documents from our vector store and pass them on to the model.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The LLM we’re using is locally hosted through Ollama. The model takes the relevant text retrieved from Chroma and uses that context to generate responses to user queries. The actual model is user-selected at the beginning of the program.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We use a simple interface where the user can ask questions, and the chatbot pulls from the stored content to generate and stream an answer. If it doesn’t have enough context to answer, it will graciously let you know, but more often than not, it’s ready to impress with its knowledge.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By combining Chroma with an LLM, we ensure that the chatbot not only returns raw text but generates contextually appropriate answers. The power of an LLM with RAG lies in its ability to take a set of relevant documents and provide a coherent, informative response. So, when you ask, “What’s the internal reference system of Gaia Sky?” the chatbot can retrieve the most relevant content and synthesize it into a helpful response.&lt;/p&gt;
&lt;h3 id="system-prompt"&gt;System prompt&lt;/h3&gt;
&lt;p&gt;One of the key elements in ensuring a Retrieval-Augmented Generation (RAG) system behaves as expected is the &lt;strong&gt;system prompt&lt;/strong&gt;—a set of initial instructions that guide how the language model responds to queries. In our application, the system prompt is designed to enforce a structured and helpful interaction:&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;system_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;&amp;#34;You are a customer support agent, helping users by following directives and answering questions. &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;Use the given context to answer the question. &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;If you don&amp;#39;t know the answer, say you don&amp;#39;t know. &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;Keep the answer concise and to the point. &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;Context: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{context}&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;The prompt explicitly states that the LLM is a &lt;em&gt;customer support agent&lt;/em&gt; to steer its behavior towards professional responses. It also includes a sentence about context awareness so that it actually uses information given in the context. Then, we ask it to not invent something it does not find in the context, and we demand short and concise answers.&lt;/p&gt;
&lt;p&gt;By fine-tuning this system prompt, we set clear boundaries for how the model should behave, ensuring it delivers accurate, relevant, and trustworthy responses based on the retrieved knowledge.&lt;/p&gt;
&lt;h3 id="other-parameters"&gt;Other parameters&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Temperature&lt;/strong&gt; &amp;ndash; The temperature controls the randomness of the model&amp;rsquo;s responses, with lower values making answers more deterministic and factual, while higher values increase creativity and variability. We use a default low temperature value of 0.3 to get more correct and less creative responses. The temperature can be set with the argument &lt;code&gt;-t&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Number of results&lt;/strong&gt; &amp;ndash; The number of results parameter (&lt;code&gt;n_results&lt;/code&gt; or &lt;code&gt;k&lt;/code&gt;) determines how many relevant documents are retrieved from the vector database to provide context for the model&amp;rsquo;s response, with higher values increasing the available information but potentially adding noise. We use the default of 6 results, but this can be changed with the &lt;code&gt;-k&lt;/code&gt; argument.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The relevant code that does the actual querying lives in the &lt;code&gt;query_with_ollama(vector_store, llm_model)&lt;/code&gt; method. The code below includes the relevant parameters at the top.&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;system_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;&amp;#34;You are a customer support agent, helping users by following directives and answering questions. &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;Use the given context to answer the question. &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;If you don&amp;#39;t know the answer, say you don&amp;#39;t know. &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;Keep the answer concise and to the point. &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;Context: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{context}&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;# In the real program, both n_results and temp can be set with program arguments.&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;# Number of results to retrieve from the vector store.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;n_results&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:#828b96;font-style:italic"&gt;# Low temperature to get less creative and more correct responses&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;temp&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.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:#ff636f"&gt;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;query_with_ollama&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;vector_store&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;model_name&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;Uses an Ollama model to retrieve and answer user queries based on stored embeddings.&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;retriever&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;vector_store&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;as_retriever&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;search_kwargs&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;{&lt;span style="color:#a6be9d"&gt;&amp;#34;k&amp;#34;&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;n_results&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 style="color:#58a1dd"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;from_messages&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;&amp;#34;system&amp;#34;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;system_prompt&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;human&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{input}&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:#58a1dd"&gt;qa_chain&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;create_stuff_documents_chain&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;OllamaLLM&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;model&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;model_name&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;temperature&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;temp&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;stream&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#ff636f"&gt;True&lt;/span&gt;), &lt;span style="color:#58a1dd"&gt;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;chain&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;create_retrieval_chain&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;retriever&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;qa_chain&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;while&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;query&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;input&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;colored&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Ask a question (type &amp;#39;exit&amp;#39; to quit): &amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;yellow&amp;#34;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;attrs&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#34;bold&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:#58a1dd"&gt;query&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;lower&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; [&lt;span style="color:#a6be9d"&gt;&amp;#34;exit&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;quit&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;bye&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;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 style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;chunk&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;chain&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;stream&lt;/span&gt;({&lt;span style="color:#a6be9d"&gt;&amp;#34;input&amp;#34;&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;query&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:#a6be9d"&gt;&amp;#34;answer&amp;#34;&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;chunk&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;chunk&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#34;answer&amp;#34;&lt;/span&gt;], &lt;span style="color:#58a1dd"&gt;end&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:#58a1dd"&gt;flush&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&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;&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This method contains the context retrieval code and the question-answer loop. The actual &lt;strong&gt;LLM model&lt;/strong&gt; to use, stored in &lt;code&gt;llm_model&lt;/code&gt;, is &lt;strong&gt;user-defined&lt;/strong&gt; (code not shown here). After starting, the program presents the available models, and the user needs to select one of them. This happens in the &lt;code&gt;chatbot()&lt;/code&gt; function, which is the final piece of the puzzle. It gets the vector store object&amp;mdash;either we have just ingested it from scraping (if &lt;code&gt;--scrape&lt;/code&gt; is used) or we have retrieved it from the database, if the program is called with no arguments. Then, it prompts the user for the model to use, and calls &lt;code&gt;query_with_ollama(...)&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;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;chatbot&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;vector_store&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;# List available models&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;models&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ollama&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;list&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;model_names&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&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;model&lt;/span&gt; &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;m&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;models&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;models&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;except&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;&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;colored&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#39;ERROR&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;red&amp;#39;&lt;/span&gt;)&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;: Ollama service is not running.&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;exit&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;# Print the available models with index numbers&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;&amp;#34;Available models:&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;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;i&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;name&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;enumerate&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;model_names&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; [&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;colored&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;i&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;green&amp;#39;&lt;/span&gt;)&lt;span style="color:#a6be9d"&gt;}&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;] &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;name&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:#828b96;font-style:italic"&gt;# Loop until a valid selection is made&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:#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;choice&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;input&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;\n&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;Select model (default &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;colored&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#39;0&amp;#39;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;green&amp;#39;&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;.&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:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;choice&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:#828b96;font-style:italic"&gt;# Default to 0 if empty input&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;llm_model&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;model_names&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;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 style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;choice&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;isdigit&lt;/span&gt;(): &lt;span style="color:#828b96;font-style:italic"&gt;# Check if input is a number&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;index&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;int&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;choice&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:#a6be9d"&gt;0&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;index&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;len&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;model_names&lt;/span&gt;): &lt;span style="color:#828b96;font-style:italic"&gt;# Check if index is valid&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;llm_model&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;model_names&lt;/span&gt;[&lt;span style="color:#58a1dd"&gt;index&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 style="color:#58a1dd"&gt;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Invalid selection. Please enter a valid number.&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:#58a1dd"&gt;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;LLM model: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;colored&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;llm_model&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;yellow&amp;#39;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;attrs&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#39;bold&amp;#39;&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:#58a1dd"&gt;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;Embed model: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;colored&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;embed_model&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#39;yellow&amp;#39;&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;attrs&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#39;bold&amp;#39;&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:#58a1dd"&gt;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Connecting to Ollama for queries...&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;query_with_ollama&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;vector_store&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;llm_model&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="implementation-details"&gt;Implementation details&lt;/h2&gt;
&lt;p&gt;The scraping function is hidden behind a flag (&lt;code&gt;--scrape&lt;/code&gt;). When it is used, the websites are scraped, their information is extracted, embedded into vectors and ingested in the database. When no arguments are used, the program starts the chatbot. Of course, &lt;strong&gt;you need to run at least once with &lt;code&gt;--scrape&lt;/code&gt; before using the chatbot&lt;/strong&gt;. But if you’ve already scraped the site and generated embeddings, the system will load the existing embeddings from disk, saving you time and resources. It’s efficient, and you don’t have to redo the work every time you launch the chatbot.&lt;/p&gt;
&lt;p&gt;If you change the embed model, remember to nuke the database (&lt;code&gt;rm -rf chroma_db&lt;/code&gt;) and re-do the scraping!&lt;/p&gt;
&lt;p&gt;Finally, by using &lt;code&gt;stream=True&lt;/code&gt; in the creation of &lt;code&gt;OllamaLLM&lt;/code&gt;, and calling &lt;code&gt;chain.stream()&lt;/code&gt; instead of &lt;code&gt;chain.query()&lt;/code&gt;, we can stream the response as it is generated, token-by-token. This is the style most web chatbots use.&lt;/p&gt;
&lt;h2 id="evaluation"&gt;Evaluation&lt;/h2&gt;
&lt;p&gt;In this section we test the system with &lt;a href="https://ollama.com/library/mistral-small:22b-instruct-2409-q4_K_M"&gt;&lt;code&gt;mistral-small:22b-instruct-2409-q4_K_M&lt;/code&gt;&lt;/a&gt;, a small instruct LLM with capabilities comparable to larger models, and with &lt;a href="https://ollama.com/library/llama3.1:8b-instruct-q5_K_M"&gt;&lt;code&gt;llama3.1:8b-instruct-q5_K_M&lt;/code&gt;&lt;/a&gt;, a much smaller but better quantized model(Q5-K-M vs Q4-K-M), also specifically trained for instruct.&lt;/p&gt;
&lt;p&gt;I ask each model 3 questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The usages of Gaia Sky.&lt;/li&gt;
&lt;li&gt;How to install Gaia Sky on Linux.&lt;/li&gt;
&lt;li&gt;How are cubemap textures used in Gaia Sky.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here are links to the relevant sections.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#mistral-small"&gt;Mistral Small 3&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#mistral-small-usages"&gt;Usages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#mistral-small-linux"&gt;Linux installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#mistral-small-cubemaps"&gt;Cubemaps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#llama31"&gt;Llama 3.1&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#llama31-usages"&gt;Usages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#llama31-linux"&gt;Linux installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#llama31-cubemaps"&gt;Cubemaps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&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;Note&lt;/strong&gt;&lt;/div&gt;
&lt;div class="hint-body"&gt;
&lt;p&gt;You can install these models directly with Ollama:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ollama pull mistral-small:22b-instruct-2409-q4_K_M llama3.1:8b-instruct-q5_K_M
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Remember, if you want to try out the chatbot yourself, download it from &lt;a href="https://codeberg.org/langurmonkey/gaiasky-ai"&gt;this repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a name="mistral-small"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="mistral-small-3-instruct-22b-q4-k-m"&gt;Mistral Small 3 instruct, 22B Q4-K-M&lt;/h3&gt;
&lt;p&gt;Here we use the general &lt;a href="https://ollama.com/library/mistral-small:22b-instruct-2409-q4_K_M"&gt;Mistral Small 3 instruct (22B, Q4-K-M)&lt;/a&gt; model. It uses Q4_K_M quantization and features 22.2B parameters. I think this is around the limit of what my computer can handle (see &lt;a href="#conclusion"&gt;conclusion&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;On a first pass, we run the program with the &lt;code&gt;--scrape&lt;/code&gt; flag to gather the information from the websites and create the embeddings. This corresponds to the output shown below, where all the URLs are fetched and scraped.&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;$ gsai.py --scrape
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Welcome to the Gaia Sky AI assistant! We connect to Ollama to use a &lt;span style="color:#58a1dd"&gt;local&lt;/span&gt; LLM.
&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;Available models:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;[&lt;/span&gt;0&lt;span style="color:#ff636f"&gt;]&lt;/span&gt; mistral-small:22b-instruct-2409-q4_K_M
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;[&lt;/span&gt;1&lt;span style="color:#ff636f"&gt;]&lt;/span&gt; llama3.1:8b-instruct-q5_K_M
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;[&lt;/span&gt;2&lt;span style="color:#ff636f"&gt;]&lt;/span&gt; gemma3:12b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;[&lt;/span&gt;3&lt;span style="color:#ff636f"&gt;]&lt;/span&gt; qwen2.5:7b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;[&lt;/span&gt;4&lt;span style="color:#ff636f"&gt;]&lt;/span&gt; deepseek-r1:14b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;[&lt;/span&gt;5&lt;span style="color:#ff636f"&gt;]&lt;/span&gt; llama3.1:8b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;[&lt;/span&gt;6&lt;span style="color:#ff636f"&gt;]&lt;/span&gt; mistral-small:24b
&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;Select model &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;default 0&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;Using model: mistral-small:22b-instruct-2409-q4_K_M
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Embed model: sentence-transformers/all-MiniLM-L6-v2
&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;Starting scraping...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaia.ari.uni-heidelberg.de/gaiasky/docs/master/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaia.ari.uni-heidelberg.de/gaiasky/docs/master/Location-log.html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaia.ari.uni-heidelberg.de/gaiasky/docs/master/Virtual-textures.html
&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:#ff636f"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaia.ari.uni-heidelberg.de/gaiasky/docs/master/Cubemaps.html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaia.ari.uni-heidelberg.de/gaiasky/docs/master/Archetypes.html
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Found &lt;span style="color:#a6be9d"&gt;69&lt;/span&gt; pages to scrape...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Extracting text from pages...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaiasky.space
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaiasky.space/privacypolicy/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaiasky.space/news/
&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:#ff636f"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaiasky.space/news/2023/gaiasky_3.4.0/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaiasky.space/news/2023/gaiasky_3.5.3/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaiasky.space/news/page/5/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Scraping https://gaiasky.space/news/page/6/
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Found &lt;span style="color:#a6be9d"&gt;64&lt;/span&gt; pages to scrape...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Extracting text from pages...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Storing embeddings in ChromaDB...
&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;Done! Run the script without arguments to start the chatbot.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, we run &lt;code&gt;gsai.py&lt;/code&gt; without the flag to access the chatbot directly and start asking questions. These are documented in the quoted blocks below.&lt;/p&gt;
&lt;p&gt;First, I asked about what can Gaia Sky be used for. This is a general type of question, so let&amp;rsquo;s see how it fares.&lt;/p&gt;
&lt;p&gt;&lt;a name="mistral-small-usages"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q: State the possible usages of Gaia Sky.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Gaia Sky is a powerful and versatile software tool that can be used for a variety of purposes, especially in the fields of astronomy, education, and scientific research. Here are some of its potential usages:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Educational Tool&lt;/strong&gt;: Gaia Sky can be used to teach students about astronomy, celestial mechanics, and the universe. It provides an interactive and visually engaging way to learn about different astronomical objects and phenomena.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Research in Astronomy&lt;/strong&gt;: Scientists and researchers can use Gaia Sky to visualize and analyze complex datasets from space missions like Gaia, allowing them to gain insights into stellar evolution, galactic structure, and other astrophysical processes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data Visualization&lt;/strong&gt;: The software offers advanced data visualization capabilities, enabling users to create detailed 3D models of celestial objects and phenomena. This is particularly useful for presenting research findings or educational content in a compelling manner.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Virtual Tours&lt;/strong&gt;: Gaia Sky can be used to create virtual tours of the universe, allowing users to explore different regions of space, from our solar system to distant galaxies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Public Outreach&lt;/strong&gt;: Astronomy enthusiasts and science communicators can use Gaia Sky to create engaging presentations and demonstrations for public events, such as star parties or science festivals.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Simulations&lt;/strong&gt;: Researchers can use the software to run simulations of various astronomical scenarios, helping them understand how different factors influence celestial phenomena.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Planetarium Software&lt;/strong&gt;: Gaia Sky can be used in planetariums to provide immersive and interactive experiences for visitors, enhancing their understanding of astronomy through visual storytelling.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scientific Collaboration&lt;/strong&gt;: The software facilitates collaboration among researchers by providing a common platform to share and discuss data, simulations, and findings.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Artistic Visualizations&lt;/strong&gt;: Artists can use Gaia Sky to create stunning visual representations of astronomical objects and scenes for artistic purposes or multimedia projects.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Space Mission Support&lt;/strong&gt;: Gaia Sky can be used by teams involved in space missions to plan observations, analyze data, and support mission operations.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These uses highlight the flexibility and power of Gaia Sky as a tool for both educational and professional applications in astronomy and related fields.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This answer is not concise, but it is quite alright. It captures all important features of Gaia Sky and gives a short description for each. However, I think it might be overselling Gaia Sky a tiny bit, but hey. Also, this model is quite slow on my computer, more on that later.&lt;/p&gt;
&lt;p&gt;Next, I ask how to install Gaia Sky on Linux. We have several options to install the software on Linux, including APT, DEB and AUR packages, Flatpak, AppImage, and tarballs. Let&amp;rsquo;s see if the model captures these well.&lt;/p&gt;
&lt;p&gt;&lt;a name="mistral-small-linux"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q: How do I install Gaia Sky on Linux?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To install Gaia Sky on a Linux system, you have several options depending on your distribution and preferences. Here are the most common methods:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using Flatpak&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install Flatpak&lt;/strong&gt;: If you don&amp;rsquo;t already have Flatpak installed, you can install it using your package manager. For example, on Debian-based systems (like Ubuntu), you would use:&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install flatpak
&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;Add the Flathub 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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
&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;Install Gaia Sky&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;flatpak install flathub space.gaiasky.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;Run Gaia Sky&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;flatpak run space.gaiasky.GaiaSky
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Using AppImage&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Download the AppImage&lt;/strong&gt;: Go to the &lt;a href="https://gaiasky.space/downloads"&gt;Gaia Sky downloads page&lt;/a&gt; and download the latest &lt;code&gt;.AppImage&lt;/code&gt; file for Gaia Sky.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make the AppImage executable&lt;/strong&gt;: Open a terminal, navigate to the directory where you downloaded the AppImage, 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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod +x GaiaSky-*.AppImage
&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;Run the AppImage&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;./GaiaSky-*.AppImage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Using Unix Installer&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Download the installer&lt;/strong&gt;: Go to the &lt;a href="https://gaiasky.space/downloads"&gt;Gaia Sky downloads page&lt;/a&gt; and download the &lt;code&gt;.run&lt;/code&gt; file for the Unix installer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Make the installer executable&lt;/strong&gt;: Open a terminal, navigate to the directory where you downloaded the installer, 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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;chmod +x GaiaSky-*.run
&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;Run the installer&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;./GaiaSky-*.run
&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;Follow the on-screen instructions&lt;/strong&gt; to complete the installation.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Using TAR.GZ Package&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Download the TAR.GZ package&lt;/strong&gt;: Go to the &lt;a href="https://gaiasky.space/downloads"&gt;Gaia Sky downloads page&lt;/a&gt; and download the &lt;code&gt;.tar.gz&lt;/code&gt; file for Gaia Sky.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extract the package&lt;/strong&gt;: Open a terminal, navigate to the directory where you downloaded the TAR.GZ file, 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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tar -xzf GaiaSky-*.tar.gz
&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;Move the extracted folder&lt;/strong&gt; to your desired installation location (e.g., &lt;code&gt;/opt&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo mv GaiaSky /opt/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;Create a symbolic link&lt;/strong&gt; in &lt;code&gt;/usr/local/bin&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo ln -s /opt/GaiaSky/gaiasky /usr/local/bin/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;Run Gaia Sky&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-sh" data-lang="sh"&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;p&gt;&lt;strong&gt;Using Distro-Specific Packages (DEB, RPM, AUR)&lt;/strong&gt;
For distro-specific packages, you can usually install them directly using your package manager:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Debian/Ubuntu&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo apt install gaia-sky
&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;RedHat/Fedora&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo dnf install gaia-sky
&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;Arch Linux&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-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;yay -S gaia-sky
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Choose the method that best suits your needs and preferences.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Wow, lots of awesome info and formatting here. It gets most of the information right, but it misses the mark on the installation for Debian and RedHat (we have no packages in their repositories), and Arch Linux (the package is actually called &lt;code&gt;gaiasky&lt;/code&gt;, not &lt;code&gt;gaia-sky&lt;/code&gt;). Other than that, I think this is a good answer.&lt;/p&gt;
&lt;p&gt;To be fair, this was a rather open question, so it would be surprising that such a small model were able to capture all the info perfectly.&lt;/p&gt;
&lt;p&gt;Finally, I asked for something much more technical and specific&amp;mdash;I asked it to explain how cubemaps are used in Gaia Sky.&lt;/p&gt;
&lt;p&gt;&lt;a name="mistral-small-cubemaps"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q: Explain how cubemap textures are used in Gaia Sky.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Cubemaps are a type of texture that maps an entire environment onto a cube&amp;rsquo;s six faces, providing a full 360-degree view. In Gaia Sky, cubemaps are primarily used to create realistic and immersive environments for planets and other celestial bodies. Here’s how they are utilized:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Environment Mapping:&lt;/strong&gt;
Cubemaps are employed for environment mapping, which involves projecting the surrounding scene onto a sphere or cube. This technique is particularly useful for rendering reflections on surfaces, such as water or metallic objects, giving them a more realistic appearance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Skyboxes:&lt;/strong&gt;
One common use of cubemaps in Gaia Sky is to create skyboxes. A skybox is a cube that surrounds the scene and displays an environment map on its faces. This technique is used to simulate distant scenery, such as the sky or outer space, without needing to render far-off objects in detail.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Planetary Surfaces:&lt;/strong&gt;
Cubemaps can also be applied to planetary surfaces to create detailed and varied textures. By mapping different environments (like oceans, mountains, or forests) onto the faces of a cube, Gaia Sky can generate diverse and realistic planetary landscapes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lighting and Shadows:&lt;/strong&gt;
Cubemaps are used in lighting calculations to simulate how light interacts with the environment. This includes creating realistic shadows and highlights based on the surrounding scene, enhancing the overall visual quality of the rendered objects.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Implementation Steps:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Creation of Cubemap Textures:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developers create cubemap textures using specialized software that can render 360-degree environments. These tools often allow for the creation of high-quality, seamless textures.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Loading Cubemaps in Gaia Sky:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The created cubemaps are then loaded into Gaia Sky. This process involves specifying which texture should be applied to which face of the cube (positive X, negative X, positive Y, negative Y, positive Z, and negative Z).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Application in Shaders:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The shaders in Gaia Sky are programmed to use these cubemaps for various effects, such as reflections, environment lighting, and skybox rendering. This is done by sampling the cubemap texture within the shader code based on the viewing direction or other parameters.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Realism:&lt;/strong&gt; Cubemaps help create highly realistic environments by providing detailed textures and reflections.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Instead of rendering distant objects, which can be computationally expensive, cubemaps allow for quick and efficient simulation of the environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility:&lt;/strong&gt; They can be easily swapped out to change the appearance of an environment without altering the underlying geometry.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In summary, cubemap textures play a crucial role in Gaia Sky by enabling the creation of immersive, realistic environments through techniques like environment mapping and skybox rendering.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is perhaps too detailed, and sources information from outside of the context, but it is mostly correct, if a little verbose. I&amp;rsquo;m happy with this answer.&lt;/p&gt;
&lt;p&gt;All in all, I think this Mistral model is a little too verbose. You can see specks of larger models, like the good formatting, or the effort to produce complete and exhaustive answers. However, the accuracy is not always on point. I wonder if using a model with better quantization (Q6?) would solve this.&lt;/p&gt;
&lt;p&gt;&lt;a name="llama31"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="llama-31-instruct-8b-q5-k-m"&gt;Llama 3.1 instruct, 8B Q5-K-M&lt;/h3&gt;
&lt;p&gt;Now we test the &lt;a href="https://ollama.com/library/llama3.1:8b-instruct-q5_K_M"&gt;Llama 3.1 instruct (8B, Q5-K-M)&lt;/a&gt; model, an instruct-specific model that is much smaller than Mistral Small 3 (24B). However, this time around I stepped up the quantization a bit by using Q5_K_M, which should improve accuracy over the basic Q4_0.&lt;/p&gt;
&lt;p&gt;First, let&amp;rsquo;s ask about the usages:&lt;/p&gt;
&lt;p&gt;&lt;a name="llama31-usages"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q: State the possible usages of Gaia Sky.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Gaia Sky is a free, open-source planetarium software that can be used for various purposes, including:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Educational purposes&lt;/strong&gt;: Gaia Sky can be used in educational settings to teach astronomy and astrophysics concepts to students.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Research and scientific visualization&lt;/strong&gt;: The software&amp;rsquo;s advanced features make it suitable for researchers who need to visualize complex astronomical data, such as star catalogs, galaxy distributions, or planetary systems.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Public outreach and engagement&lt;/strong&gt;: Gaia Sky can be used in public events, exhibitions, or planetariums to showcase the wonders of the universe and promote interest in astronomy among the general public.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Personal entertainment&lt;/strong&gt;: Users can simply enjoy exploring the night sky, viewing celestial objects, and experiencing the thrill of space travel within the software&amp;rsquo;s virtual environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Artistic and creative projects&lt;/strong&gt;: Gaia Sky&amp;rsquo;s capabilities make it an attractive tool for artists, designers, and filmmakers who need to create realistic astronomical environments or simulate complex astrophysical phenomena.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Professional training and development&lt;/strong&gt;: The software can be used by professionals in fields like astronomy, space exploration, or related industries to improve their knowledge and skills through interactive simulations and visualizations.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These are just a few examples of the many possible usages of Gaia Sky. Its versatility and flexibility make it an excellent tool for various applications across different domains.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a good response. Not too long, and very well organized. Everything in it is correct, too. Also, Llama 3.1 was very fast in generating it.&lt;/p&gt;
&lt;p&gt;The next question is about the Linux installation.&lt;/p&gt;
&lt;p&gt;&lt;a name="llama31-linux"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q: How do I install Gaia Sky on Linux?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To install Gaia Sky on Linux, you can follow these steps for the different package types:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flatpak&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install Flatpak if you haven&amp;rsquo;t already:
&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 flatpak
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Add the Flathub repository:
&lt;pre tabindex="0"&gt;&lt;code&gt;flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;Install Gaia Sky from Flathub:
&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;flatpak install flathub space.gaiasky.GaiaSky
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Run Gaia Sky:
&lt;pre tabindex="0"&gt;&lt;code&gt;flatpak run space.gaiasky.GaiaSky
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;AppImage&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the AppImage file from the official website: &lt;a href="https://gaiasky.space/downloads/"&gt;https://gaiasky.space/downloads/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Make the AppImage executable:
&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;chmod +x gaiasky_x86_64.AppImage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Run Gaia Sky:
&lt;pre tabindex="0"&gt;&lt;code&gt;./gaiasky_x86_64.AppImage
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Unix Installer (tarball)&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the tarball from the official website: &lt;a href="https://gaiasky.space/downloads/"&gt;https://gaiasky.space/downloads/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Extract the contents of the tarball to a directory of your choice:
&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;tar -xvf gaiasky.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Run Gaia Sky:
&lt;pre tabindex="0"&gt;&lt;code&gt;./gaiasky.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;DEB (Debian and derivatives)&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the DEB package from the official website: &lt;a href="https://gaiasky.space/downloads/"&gt;https://gaiasky.space/downloads/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Install the package using dpkg:
&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_3.6.7_amd64.deb
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Run Gaia Sky:
&lt;pre tabindex="0"&gt;&lt;code&gt;gaiasky
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;RPM (RedHat and derivatives)&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download the RPM package from the official website: &lt;a href="https://gaiasky.space/downloads/"&gt;https://gaiasky.space/downloads/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Install the package using rpm:
&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 rpm -i gaiasky-3.6.7-x86_64.rpm
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Run Gaia Sky:
&lt;pre tabindex="0"&gt;&lt;code&gt;gaiasky
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;AUR (Arch Linux and derivatives)&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install the AUR helper of your choice (e.g., yay, yay-bin).&lt;/li&gt;
&lt;li&gt;Clone the Gaia Sky AUR package repository:
&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://aur.archlinux.org/gaiasky.git
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Navigate to the cloned repository directory:
&lt;pre tabindex="0"&gt;&lt;code&gt;cd gaiasky
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;li&gt;Build and install the package:
&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;makepkg -si
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Run Gaia Sky:
&lt;pre tabindex="0"&gt;&lt;code&gt;gaiasky
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;This answer is &lt;strong&gt;fracking excellent&lt;/strong&gt;. It nails everything. It includes all Linux packages we offer, and the instructions for each are on point. I&amp;rsquo;m super impressed. The quality of this response is much better than the one generated by the (much larger) Mistral Small 3 22B.&lt;/p&gt;
&lt;p&gt;Finally, we ask it what cubemaps are used for:&lt;/p&gt;
&lt;p&gt;&lt;a name="llama31-cubemaps"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Q: How are cubemap textures used in Gaia Sky?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In Gaia Sky, cubemap textures are used to display planetary surfaces and other 3D models with high detail and realism. Here&amp;rsquo;s an overview of how they work:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is a cubemap?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A cubemap is a type of texture that represents the surface of a 3D object from all six sides (top, bottom, left, right, front, and back). It&amp;rsquo;s called a &amp;ldquo;cubemap&amp;rdquo; because it&amp;rsquo;s essentially a cube with six faces, each containing a different view of the object.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How are cubemaps used in Gaia Sky?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In Gaia Sky, cubemaps are used to display planetary surfaces, such as Earth or other planets. When you load a planet into the game, Gaia Sky generates a cubemap texture that represents the surface of the planet from all six sides. This allows for more detailed and realistic rendering of the planet&amp;rsquo;s surface.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do cubemaps improve graphics in Gaia Sky?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Cubemaps offer several benefits over traditional textures:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Higher detail&lt;/strong&gt;: Cubemaps can display much higher levels of detail than traditional textures, making planetary surfaces look more realistic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Realistic lighting&lt;/strong&gt;: Cubemaps take into account the way light interacts with the surface of the planet, creating a more realistic and immersive experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improved performance&lt;/strong&gt;: Because cubemaps are pre-computed and stored in memory, they can be rendered much faster than traditional textures, reducing the load on your graphics card.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;How to use cubemap textures in Gaia Sky&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To use cubemap textures in Gaia Sky, you&amp;rsquo;ll need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load a planet or other 3D model into the game.&lt;/li&gt;
&lt;li&gt;Adjust the graphics settings to enable cubemaps (this may require updating to a newer version of Gaia Sky).&lt;/li&gt;
&lt;li&gt;Select the desired level of detail and lighting for the cubemap texture.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By using cubemap textures, Gaia Sky can create stunningly realistic planetary surfaces that draw you in and make you feel like you&amp;rsquo;re exploring the cosmos itself!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The format and wording of this answer look like it comes from a larger model. However, I think the content misses the point a bit. It does not look like it sources this information from the &lt;a href="https://gaia.ari.uni-heidelberg.de/gaiasky/docs/master/Cubemaps.html"&gt;Cubemaps section&lt;/a&gt; in our documentation, which may be an issue with the embedding, the context size, or the context retrieval. I tried reducing the number of results from 10 to 5 before running the query again:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How are cubemaps used in Gaia Sky?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Cubemaps are used in Gaia Sky to texture planets, moons, and other spherical or semi-spherical objects. They help eliminate artifacts that happen at the poles with UV sphere models, providing a more seamless appearance. Cubemaps can be used instead of regular equirectangular (spherically projected) images.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This time around, the answer is much more concise and correct.&lt;/p&gt;
&lt;p&gt;In the global picture, I like the responses of Llama better than those provided by Mistral. Also, Mistral is almost 3 times larger than Llama (22B vs 8B), so it is much slower on my machine.&lt;/p&gt;
&lt;h2 id="performance"&gt;Performance&lt;/h2&gt;
&lt;p&gt;In my case, I&amp;rsquo;m pretty limited by my hardware, and responses tend to be slow with the larger model (Mistral). The speed is quite good with Llama (8B) though. True, I&amp;rsquo;m running this a very old computer for today&amp;rsquo;s standards (Intel(R) Core(TM) i7-7700 (8) @ 4.20 GHz, NVIDIA GeForce GTX 1070 8 GB, 32 GB RAM), so it is what it is.&lt;/p&gt;
&lt;p&gt;Here are the performance stats for both models. I asked them both to generate a short text (~100 words) about the meaning of life.&lt;/p&gt;
&lt;span style="font-size: small"&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Total Duration&lt;/th&gt;
&lt;th&gt;Prompt Eval Rate&lt;/th&gt;
&lt;th&gt;Eval Count&lt;/th&gt;
&lt;th&gt;Eval Duration&lt;/th&gt;
&lt;th&gt;Eval Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mistral-small:22b-instruct-2409-q4_K_M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;24.575599226s&lt;/td&gt;
&lt;td&gt;53.56 tokens/s&lt;/td&gt;
&lt;td&gt;67 token(s)&lt;/td&gt;
&lt;td&gt;23.194348749s&lt;/td&gt;
&lt;td&gt;2.89 tokens/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;llama3.1:8b-instruct-q5_K_M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7.825027246s&lt;/td&gt;
&lt;td&gt;109.12 tokens/s&lt;/td&gt;
&lt;td&gt;119 token(s)&lt;/td&gt;
&lt;td&gt;7.526260082s&lt;/td&gt;
&lt;td&gt;15.81 tokens/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/span&gt;
&lt;p&gt;I found the responses provided by smaller models (1B to 5B) to be mostly garbage. On larger instruct models with decent quantization, accuracy is greatly improved at the expense of your precious time.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post, we&amp;rsquo;ve expanded on our previous post and developed an AI chatbot able to answer questions on Gaia Sky. The information is sourced from the Gaia Sky homepage and documentation. With the power of web scraping, embeddings, and Ollama, we&amp;rsquo;ve brought the Gaia Sky website and docs to life with smart, context-aware interactions. No more endless searches for answers. Instead, we now have a chatbot that knows exactly where to look and can provide concise, relevant responses. So go ahead and try it out&amp;mdash;who knew scraping could be this useful?&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s pretty obvious to me that such small models are able to get most of the questions &lt;em&gt;almost&lt;/em&gt; right, but they are most definitely not suited for professional settings. In this case, larger models (at least 70B, I reckon) would be required. For our purposes here, Mistral Small 3 instruct (22B) and Llama 3.1 instruct (8B) are enough.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t want to finish without mentioning off-the-shelf options that accomplish the same goals. There exist no-code software packages like &lt;a href="https://anythingllm.com/"&gt;AnythingLLM&lt;/a&gt; and &lt;a href="https://openwebui.com/"&gt;Open WebUI&lt;/a&gt; that support local models via Ollama and integrate RAG. I actually recommend using these if you are not too interested in the nitty-gritty details.&lt;/p&gt;</description></item><item><title>Local LLM with Retrieval-Augmented Generation</title><link>https://tonisagrista.com/blog/2025/local-llm-rag/</link><pubDate>Mon, 24 Mar 2025 00:00:00 +0000</pubDate><author>Toni Sagristà Sellés</author><guid>https://tonisagrista.com/blog/2025/local-llm-rag/</guid><description>&lt;p&gt;&lt;span class="orange"&gt;Edit (2025-03-26):&lt;/span&gt;
&lt;em&gt;Added some words about next steps in conclusion.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class="orange"&gt;Edit (2025-03-25):&lt;/span&gt;
&lt;em&gt;I re-ran the example with a clean database and the results are better. I also cleaned up the code a bit.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Over the past few months I have been running local &lt;abbr title="Large Language Model"&gt;LLMs&lt;/abbr&gt; on my computer with various results, ranging from &amp;lsquo;unusable&amp;rsquo; to &amp;lsquo;pretty good&amp;rsquo;.
Local LLMs are becoming more powerful, but they don&amp;rsquo;t inherently &amp;ldquo;know&amp;rdquo; everything. They&amp;rsquo;re trained on massive datasets, but those are typically static. To make LLMs truly useful for specific tasks, you often need to augment them with &lt;em&gt;your own&lt;/em&gt; data&amp;ndash;data that&amp;rsquo;s constantly changing, specific to your domain, or not included in the LLM&amp;rsquo;s original training. The technique known as &lt;abbr title="Retrieval Augmented Generation"&gt;RAG&lt;/abbr&gt; aims to bridge this problem by embedding context information into a vector database that is later used to provide context to the LLM, so that it can &lt;em&gt;expand&lt;/em&gt; its knowledge beyond the original training dataset. In this short article, we&amp;rsquo;ll see how to build a very &lt;em&gt;primitive&lt;/em&gt; local AI chatbot powered by Ollama with RAG capabilities.&lt;/p&gt;
&lt;p&gt;The source code used in this post is available &lt;a href="https://codeberg.org/langurmonkey/rag-llm"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="ingredients-and-set-up"&gt;Ingredients and set-up&lt;/h2&gt;
&lt;p&gt;In order to build our AI chatbot, we need the following ingredients:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ollama, to facilitate access to different LLMs.&lt;/li&gt;
&lt;li&gt;Chroma DB, our vector storage for our context.&lt;/li&gt;
&lt;li&gt;Langchain, to facilitate the integration of our RAG application with the LLM.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;First, we need to install Ollama. On Arch Linux, it is in the &lt;code&gt;extra&lt;/code&gt; repository. If you happen to have an NVIDIA GPU that supports CUDA, you&amp;rsquo;re in luck. You can speed-up your inference but a significant factor! Otherwise, you can still just use the CPU. I have a (rather old) NVIDIA GPU, so I&amp;rsquo;m installing &lt;code&gt;ollama-cuda&lt;/code&gt; as well.&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;pacman -S ollama ollama-cuda
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Next, we need some LLM models to run locally. First, start the Ollama 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;systemctl start ollama.service
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You can check whether the service is running with curl:&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;$ curl http://localhost:11434
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Ollama is running%
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then, you need to get some LLM models. I have some installed that I use regularly:&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;$ ollama list
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;NAME ID SIZE MODIFIED
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;llama3.1:8b 46e0c10c039e 4.9 GB &lt;span style="color:#a6be9d"&gt;2&lt;/span&gt; hours ago
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;gemma3:12b 6fd036cefda5 8.1 GB &lt;span style="color:#a6be9d"&gt;10&lt;/span&gt; days ago
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;phi4-mini:latest 78fad5d182a7 2.5 GB &lt;span style="color:#a6be9d"&gt;10&lt;/span&gt; days ago
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;deepseek-coder-v2:16b 63fb193b3a9b 8.9 GB &lt;span style="color:#a6be9d"&gt;10&lt;/span&gt; days ago
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;llama3.2:1b baf6a787fdff 1.3 GB &lt;span style="color:#a6be9d"&gt;4&lt;/span&gt; weeks ago
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mistral-small:24b 8039dd90c113 &lt;span style="color:#a6be9d"&gt;14&lt;/span&gt; GB &lt;span style="color:#a6be9d"&gt;7&lt;/span&gt; weeks ago
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;phi4:latest ac896e5b8b34 9.1 GB &lt;span style="color:#a6be9d"&gt;7&lt;/span&gt; weeks ago
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I would recommend using &lt;code&gt;llama3.1:8b&lt;/code&gt; (&lt;a href="https://ai.meta.com/blog/meta-llama-3-1/"&gt;link&lt;/a&gt;) for this experiment, especially if you run a machine with little RAM. It is quite compact and works reasonably well&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;. Additionally, some models like &lt;code&gt;gemma3:12b&lt;/code&gt; do not support embeddings, so you need to choose carefully.&lt;/p&gt;
&lt;p&gt;Get &lt;code&gt;llama3.1:8b&lt;/code&gt; 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;ollama pull llama3.1:8b
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it for our LLM. Now, let&amp;rsquo;s create a new directory for our RAG chatbot project, and let&amp;rsquo;s install the dependencies.&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 rag-chat &lt;span style="color:#ff636f"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cd&lt;/span&gt; rag-chat
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pipenv install ollama langchain langchain-ollama chromadb overrides
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This should install &lt;code&gt;python-ollama&lt;/code&gt;, Langchain together with the &lt;code&gt;-ollama&lt;/code&gt; hook, and Chroma DB.&lt;/p&gt;
&lt;h2 id="ollama-model-setup"&gt;Ollama model setup&lt;/h2&gt;
&lt;p&gt;The code we display below is available &lt;a href="https://codeberg.org/langurmonkey/rag-llm"&gt;in this repository&lt;/a&gt;. Now, let&amp;rsquo;s create a file, &lt;code&gt;rag-chat.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First, our chatbot will list all the available models and will ask for the model we want to use. This part is simple:&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;#!python&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;langchain_ollama&lt;/span&gt; &lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;OllamaEmbeddings&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;OllamaLLM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;import&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;chromadb&lt;/span&gt;&lt;span style="color:#ff636f"&gt;,&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;requests&lt;/span&gt;&lt;span style="color:#ff636f"&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;ollama&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;OLLAMA_URL&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;http://localhost:11434&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:#58a1dd"&gt;models&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ollama&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;list&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;model_names&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:#58a1dd"&gt;m&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;models&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;models&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;model_names&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;append&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;model&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;print&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Available models:&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:#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;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;join&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;model_names&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;# Ask the user the LLM model to use&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;llm_model&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;input&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;Model to use: &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="vector-storage"&gt;Vector storage&lt;/h2&gt;
&lt;p&gt;Chroma DB is a vector database designed and optimized for storing and searching &lt;strong&gt;vector embeddings&lt;/strong&gt;, which are crucial in RAG. Vector embeddings are numerical representations of data (text, images, audio, etc.). They capture the &lt;em&gt;meaning&lt;/em&gt; or &lt;em&gt;semantic&lt;/em&gt; information of the data. They are generated by machine learning models (often
transformer models like those used in LLMs). These models are trained to map similar data points to nearby points in a high-dimensional vector space. That&amp;rsquo;s precisely what we need to do with our context data for it to be useful to the LLM. And to that purpose we use Chroma DB.&lt;/p&gt;
&lt;p&gt;So, we need to initialize the Chroma DB client with a persistent storage, and get it ready to embed our context data. We decide to persist our storage to disk, in the directory &lt;code&gt;chroma_db/&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;# Configure ChromaDB&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;# Initialize the ChromaDB client with persistent storage in the current directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;chroma_client&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;chromadb&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;PersistentClient&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;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;join&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;getcwd&lt;/span&gt;(), &lt;span style="color:#a6be9d"&gt;&amp;#34;chroma_db&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;# Custom embedding function for ChromaDB (using Ollama)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;class&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ChromaDBEmbeddingFunction&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;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; Custom embedding function for ChromaDB using embeddings from Ollama.
&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;&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;__init__&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;langchain_embeddings&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 style="color:#58a1dd"&gt;langchain_embeddings&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;langchain_embeddings&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;def&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;__call__&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;self&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;input&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 the input is in a list format for processing&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;isinstance&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;input&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;str&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;input&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; [&lt;span style="color:#58a1dd"&gt;input&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;self&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;langchain_embeddings&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;embed_documents&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;input&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;# Initialize the embedding using Ollama embeddings, with the chosen model&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;embedding&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ChromaDBEmbeddingFunction&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;OllamaEmbeddings&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;model&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;llm_model&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_url&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;OLLAMA_URL&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;# Define a collection for the RAG workflow&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;collection_name&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;rag_collection_demo&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;collection&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;chroma_client&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;get_or_create_collection&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;name&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;collection_name&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;metadata&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;{&lt;span style="color:#a6be9d"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: &lt;span style="color:#a6be9d"&gt;&amp;#34;A collection for RAG with Ollama&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;embedding_function&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;embedding&lt;/span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;# Embedding function we defined before&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="provide-context-for-retrieval"&gt;Provide context for retrieval&lt;/h2&gt;
&lt;p&gt;Once we have Chroma DB set up, with our collection and embedding function, we need to populate it. Here, we&amp;rsquo;ll just use an array with text, but you could fetch this information from files easily.&lt;/p&gt;
&lt;p&gt;In my example, I use a totally made up text (1), the abstract to a paper about characterizing atmospheres with JWST (2), and some information about the new Gaia Sky website. The first is made up, so the model can&amp;rsquo;t know it. The second is a paper which came out much later than the model was published. The third is about the new Gaia Sky website, which was also created after the model.&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;# Function to add documents to the ChromaDB collection&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;add_documents_to_collection&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;documents&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;ids&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;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; Add documents to the ChromaDB collection.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;&lt;span style="color:#a6be9d"&gt; Args:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; documents (list of str): The documents to add.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; ids (list of str): Unique IDs for the documents.
&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;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;collection&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;add&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;documents&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;documents&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;ids&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;ids&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;# Example: Add sample documents to the collection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;documents&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;The Mittius is a sphere of radius 2 that is used to disperse light in all&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;directions&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;The&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Mittius&lt;/span&gt; &lt;span style="color:#ff636f"&gt;is&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;very&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;powerful&lt;/span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sometimes&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;emits&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;light&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;various&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;wavelengths&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;on&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;its&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;own&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;It&lt;/span&gt; &lt;span style="color:#ff636f"&gt;is&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;completely&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;fictional&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;object&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;whose&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;only&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;purpose&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;is&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;testing&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;RAG&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;local&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;LLM&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&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;&amp;#34;The newly accessible mid-infrared (MIR) window offered by the James Webb Space&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Telescope&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;JWST&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;exoplanet&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;imaging&lt;/span&gt; &lt;span style="color:#ff636f"&gt;is&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;expected&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;provide&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;valuable&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;information&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;characterize&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;their&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atmospheres&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;In&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;particular&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;coronagraphs&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;on&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;board&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;JWST&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;Mid&lt;/span&gt;&lt;span style="color:#ff636f"&gt;-&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;InfraRed&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;instrument&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;MIRI&lt;/span&gt;) &lt;span style="color:#58a1dd"&gt;are&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;capable&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;imaging&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;coldest&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;directly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;imaged&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;giant&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;planets&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;at&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;wavelengths&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;where&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;they&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;emit&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;most&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;their&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;flux&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;The&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;MIRI&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;coronagraphs&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;have&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;been&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;specially&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;designed&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;detect&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;NH3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;absorption&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;around&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;10.5&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;microns&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;which&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;has&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;been&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;predicted&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;by&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atmospheric&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;models&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;We&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;aim&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;assess&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;presence&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;NH3&lt;/span&gt; &lt;span style="color:#ff636f"&gt;while&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;refining&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atmospheric&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;parameters&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;one&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;coldest&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;companions&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;detected&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;by&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;directly&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;imaging&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;GJ&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;504&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;b&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Its&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;mass&lt;/span&gt; &lt;span style="color:#ff636f"&gt;is&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;still&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;matter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;debate&lt;/span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;depending&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;on&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;host&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;star&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;age&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;estimate&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;companion&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;could&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;either&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;be&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;placed&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;brown&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;dwarf&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;regime&lt;/span&gt; &lt;span style="color:#ff636f"&gt;or&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;young&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Jovian&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;planet&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;regime&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;We&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 style="color:#58a1dd"&gt;an&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;analysis&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;MIRI&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;coronagraphic&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;observations&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;GJ&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;504&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;system&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;We&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;took&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;advantage&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;previous&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;observations&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;reference&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;stars&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;build&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;library&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;images&lt;/span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;perform&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;more&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;efficient&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;subtraction&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;stellar&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;diffraction&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;We&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;detected&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;presence&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;NH3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;at&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;12.5&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sigma&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atmosphere&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;line&lt;/span&gt; &lt;span style="color:#ff636f"&gt;with&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atmospheric&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;model&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;expectations&lt;/span&gt; &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;planetary&lt;/span&gt;&lt;span style="color:#ff636f"&gt;-&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;mass&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;object&lt;/span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;observed&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;brown&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;dwarfs&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;within&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;similar&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;temperature&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;range&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;The&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;best&lt;/span&gt;&lt;span style="color:#ff636f"&gt;-&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;fit&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;model&lt;/span&gt; &lt;span style="color:#ff636f"&gt;with&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Exo&lt;/span&gt;&lt;span style="color:#ff636f"&gt;-&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;REM&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;provides&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;updated&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;values&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;its&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atmospheric&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;parameters&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;yielding&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;temperature&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Teff&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;512&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;K&lt;/span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;radius&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&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:#a6be9d"&gt;1.08&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;RJup&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;These&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;observations&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;demonstrate&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;capability&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;MIRI&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;coronagraphs&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;detect&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;NH3&lt;/span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;to&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;provide&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;first&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;MIR&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;observations&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;one&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;coldest&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;directly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;imaged&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;companions&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;Overall&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;NH3&lt;/span&gt; &lt;span style="color:#ff636f"&gt;is&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;key&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;molecule&lt;/span&gt; &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;characterizing&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atmospheres&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;cold&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;planets&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;offering&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;valuable&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;insights&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;into&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;their&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;surface&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;gravity&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;These&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;observations&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;provide&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;valuable&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;information&lt;/span&gt; &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;spectroscopic&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;observations&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;planned&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;JWST&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&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;&amp;#34;Gaia Sky has a new website built with Hugo. It contains download pages for all new&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;old&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;versions&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;software&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;full&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;listing&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;of&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;all&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;catalogs&lt;/span&gt; &lt;span style="color:#ff636f"&gt;and&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;datasets&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;offered&lt;/span&gt; &lt;span style="color:#ff636f"&gt;with&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;software&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;The&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;datasets&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;can&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;be&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;downloaded&lt;/span&gt; &lt;span style="color:#ff636f"&gt;in&lt;/span&gt;&lt;span style="color:#ff636f"&gt;-&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;app&lt;/span&gt; &lt;span style="color:#ff636f"&gt;with&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;the&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;provided&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;dataset&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;manager&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&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;doc_ids&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; [&lt;span style="color:#a6be9d"&gt;&amp;#34;doc_mittius&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;doc_paper_jwst&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;&amp;#34;doc_gaiasky_web&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;# Documents only need to be added once or whenever an update is required. &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 of code is included for demonstration purposes:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;add_documents_to_collection&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;documents&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;doc_ids&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="finally-the-chatbot-logic"&gt;Finally, the chatbot logic&lt;/h2&gt;
&lt;p&gt;Now we have our vector storage set up. Let&amp;rsquo;s build the chat logic!&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;# Function to query the ChromaDB collection&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;query_chromadb&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;query_text&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;n_results&lt;/span&gt;&lt;span style="color:#ff636f"&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 style="color:#a6be9d"&gt;&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:#a6be9d"&gt; Query the ChromaDB collection for relevant documents.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;&lt;span style="color:#a6be9d"&gt; Args:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; query_text (str): The input query.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; n_results (int): The number of top results to return.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;&lt;span style="color:#a6be9d"&gt; Returns:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; list of dict: The top matching documents and their metadata.
&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;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;results&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;collection&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;query&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;query_texts&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;[&lt;span style="color:#58a1dd"&gt;query_text&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;n_results&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;n_results&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;results&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#34;documents&amp;#34;&lt;/span&gt;], &lt;span style="color:#58a1dd"&gt;results&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;&amp;#34;metadatas&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;# Function to interact with the Ollama LLM&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;query_ollama&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;prompt&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;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; Send a query to Ollama and retrieve the response.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;&lt;span style="color:#a6be9d"&gt; Args:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; prompt (str): The input prompt for Ollama.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;&lt;span style="color:#a6be9d"&gt; Returns:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; str: The response from Ollama.
&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;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;llm&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;OllamaLLM&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;model&lt;/span&gt;&lt;span style="color:#ff636f"&gt;=&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;llm_model&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;llm&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;invoke&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;prompt&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;# RAG pipeline: Combine ChromaDB and Ollama for Retrieval-Augmented Generation&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;rag_pipeline&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;query_text&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;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; Perform Retrieval-Augmented Generation (RAG) by combining ChromaDB and Ollama.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;&lt;span style="color:#a6be9d"&gt; Args:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; query_text (str): The input query.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&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;&lt;span style="color:#a6be9d"&gt; Returns:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt; str: The generated response from Ollama augmented with retrieved context.
&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;&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;# Step 1: Retrieve relevant documents from ChromaDB&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;retrieved_docs&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;metadata&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;query_chromadb&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;query_text&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;context&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;retrieved_docs&lt;/span&gt;[&lt;span style="color:#a6be9d"&gt;0&lt;/span&gt;]) &lt;span style="color:#ff636f"&gt;if&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;retrieved_docs&lt;/span&gt; &lt;span style="color:#ff636f"&gt;else&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;No relevant documents found.&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;# Step 2: Send the query along with the context to Ollama&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;augmented_prompt&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;f&lt;/span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;Context: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;context&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;Question: &lt;/span&gt;&lt;span style="color:#a6be9d"&gt;{&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;query_text&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;Answer: &amp;#34;&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;# Uncomment next line if you want to see the context&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;# print(augmented_prompt)&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;response&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;query_ollama&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;augmented_prompt&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;response&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;# User query loop&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:#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;query&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;input&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;&amp;#34;Ask a question (or type &amp;#39;exit&amp;#39; to quit): &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:#58a1dd"&gt;query&lt;/span&gt;&lt;span style="color:#ff636f"&gt;.&lt;/span&gt;&lt;span style="color:#58a1dd"&gt;lower&lt;/span&gt;() &lt;span style="color:#ff636f"&gt;==&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#34;exit&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;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;response&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;rag_pipeline&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;query&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;response&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it! In this part, we ask the user to provide a query. Then, the script uses this query to fetch some context from our local Chroma DB, adds the context to the prompt (in &lt;code&gt;augmented_prompt&lt;/code&gt;), and sends the query to Ollama. Then, we wait for the response and print it out.&lt;/p&gt;
&lt;p&gt;Here is an example output. I ask it about JWST and exoplanets, the imaginary object &amp;lsquo;Mittius&amp;rsquo;, and the new Gaia Sky website. I have highlighted the response lines:&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;display:grid;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ rag-chat.py
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Available models:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - llama3.1:8b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - gemma3:12b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - phi4-mini:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - deepseek-coder-v2:16b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - llama3.2:1b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - mistral-small:24b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - phi4:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Model to use: llama3.1:8b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Insert of existing embedding ID: doc_mittius
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Insert of existing embedding ID: doc_paper_jwst
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Insert of existing embedding ID: doc_gaiasky_web
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Add of existing embedding ID: doc_mittius
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Add of existing embedding ID: doc_paper_jwst
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Add of existing embedding ID: doc_gaiasky_web
&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;Ask a question &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;or &lt;span style="color:#58a1dd"&gt;type&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#39;exit&amp;#39;&lt;/span&gt; to quit&lt;span style="color:#ff636f"&gt;)&lt;/span&gt;: JWST and exoplanets.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;The James Webb Space Telescope &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;JWST&lt;span style="color:#ff636f"&gt;)&lt;/span&gt; will play a significant role in exoplanet
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;research by providing new capabilities &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; characterizing their atmospheres.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;The telescope&lt;span style="color:#a6be9d"&gt;\&amp;#39;&lt;/span&gt;s Mid-InfraRed instrument &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;MIRI&lt;span style="color:#ff636f"&gt;)&lt;/span&gt; coronagraphs are capable of
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;imaging cold directly imaged giant planets and detecting specific molecules,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;such as ammonia &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;NH3&lt;span style="color:#ff636f"&gt;)&lt;/span&gt;, which is a key indicator of surface gravity.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;In the provided text, JWST is mentioned in the context of:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;1. The newly accessible mid-infrared &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;MIR&lt;span style="color:#ff636f"&gt;)&lt;/span&gt; window offered by JWST &lt;span style="color:#ff636f"&gt;for&lt;/span&gt; exoplanet
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt; imaging.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;2. The MIRI coronagraphs on board JWST are capable of detecting NH3 around
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt; 10.5 microns.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;3. Future spectroscopic observations planned with JWST will be guided by the
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt; valuable information obtained from these observations.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;Overall, JWST is expected to provide new insights into exoplanet atmospheres
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;and &lt;span style="color:#58a1dd"&gt;help&lt;/span&gt; scientists refine their understanding of these distant worlds.
&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;Ask a question &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;or &lt;span style="color:#58a1dd"&gt;type&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#39;exit&amp;#39;&lt;/span&gt; to quit&lt;span style="color:#ff636f"&gt;)&lt;/span&gt;: What is the mittius?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;A completely fictional object used to &lt;span style="color:#58a1dd"&gt;test&lt;/span&gt; Reasoning and Argument Generation
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;(&lt;/span&gt;RAG&lt;span style="color:#ff636f"&gt;)&lt;/span&gt; in a &lt;span style="color:#58a1dd"&gt;local&lt;/span&gt; Large Language Model &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;LLM&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;Ask a question &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;or &lt;span style="color:#58a1dd"&gt;type&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#39;exit&amp;#39;&lt;/span&gt; to quit&lt;span style="color:#ff636f"&gt;)&lt;/span&gt;: What is the radius of the Mittius?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;The answer can be found in the first sentence of the text:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;&lt;span style="color:#a6be9d"&gt;&amp;#34;The Mittius is a sphere of radius 2 that is used to disperse light in all directions.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;So, the radius of the Mittius is 2.
&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;Ask a question &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;or &lt;span style="color:#58a1dd"&gt;type&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#39;exit&amp;#39;&lt;/span&gt; to quit&lt;span style="color:#ff636f"&gt;)&lt;/span&gt;: How was the new Gaia Sky website built?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex; background-color:#e5e5e5"&gt;&lt;span&gt;The new Gaia Sky website was built with Hugo.
&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;Ask a question &lt;span style="color:#ff636f"&gt;(&lt;/span&gt;or &lt;span style="color:#58a1dd"&gt;type&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;&amp;#39;exit&amp;#39;&lt;/span&gt; to quit&lt;span style="color:#ff636f"&gt;)&lt;/span&gt;: &lt;span style="color:#58a1dd"&gt;exit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see, even this small 8B parameter model pretty much nails all three answer. In the third question (radius of the Mittius), it even provides the exact sentence that it sourced the subsequent answer from. Good work, Llama3.1!&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As we&amp;rsquo;ve seen, with very little effort we can build a rudimentary RAG system on top of Ollama. This enables us to use context information in our queries in an automated manner, with the help of Chroma DB. This post only highlights the very basic notions required to get RAG to work in a very bare-bones manner. Further concepts to explore include different embedding models, quantization, system prompts, and more.&lt;/p&gt;
&lt;p&gt;In our small test, we&amp;rsquo;ve used the Llama3.1 8B model, which is rather small. Using a larger model, like Gemma3 (12B), DeepSeek-R1 (14B), or even Mistral-small (24B), should improve the results at the expense of performance.&lt;/p&gt;
&lt;p&gt;The code in this post is partially based on &lt;a href="https://medium.com/@arunpatidar26/rag-chromadb-ollama-python-guide-for-beginners-30857499d0a0"&gt;this medium article&lt;/a&gt;.&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://artificialanalysis.ai/models/llama-3-1-instruct-8b"&gt;https://artificialanalysis.ai/models/llama-3-1-instruct-8b&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;/ol&gt;
&lt;/div&gt;</description></item><item><title>Gaia Sky gets new website</title><link>https://tonisagrista.com/blog/2025/phd-successfully-defended/</link><pubDate>Wed, 19 Feb 2025 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2025/phd-successfully-defended/</guid><description>&lt;p&gt;Over the past few weeks I&amp;rsquo;ve been working on a new standalone website for Gaia Sky. It uses the same technology stack as my personal website (essentially &lt;a href="https://gohugo.io"&gt;Hugo&lt;/a&gt;), so it is a static website generated from templates and content. This is enough for Gaia Sky. I am no graphic designer or UX person, but I tried my best to make it look &lt;em&gt;potable&lt;/em&gt;.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2025/02/gaiasky-website.jpg" width="75%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:75%;"&gt;
&lt;h4&gt;The new Gaia Sky website, &lt;a href="https://gaiasky.space"&gt;gaiasky.space&lt;/a&gt;&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Why did I create a standalone website instead of keeping the old section in the &lt;a href="https://zah.uni-heidelberg.de"&gt;ZAH&lt;/a&gt; site? A few reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It is &lt;strong&gt;much easier&lt;/strong&gt; to update and maintain.&lt;/li&gt;
&lt;li&gt;We now have a &lt;a href="https://gaiasky.space/news"&gt;&lt;strong&gt;news section&lt;/strong&gt;&lt;/a&gt; to make announcements of new releases, datasets, and whatnot. People can follow it with &lt;a href="https://gaiasky.space/index.xml"&gt;RSS&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;We can &lt;strong&gt;use our own domain&lt;/strong&gt;, &lt;a href="https://gaiasky.space"&gt;&lt;code&gt;gaiasky.space&lt;/code&gt;&lt;/a&gt;. This was in the back of my mind for a long time.&lt;/li&gt;
&lt;li&gt;We can &lt;strong&gt;verify our Flathub package&lt;/strong&gt;. The &lt;a href="https://github.com/flathub/de.uni_heidelberg.zah.GaiaSky/issues/57"&gt;impossibility&lt;/a&gt; to get Gaia Sky &lt;a href="https://github.com/flathub-infra/website/issues/3844"&gt;verified&lt;/a&gt; in Flathub is what prompted it all, as we obviously do not control the domain &lt;a href="https://uni-heidelberg.de"&gt;&lt;code&gt;uni-heidelberg.de&lt;/code&gt;&lt;/a&gt; associated with the old app ID &lt;code&gt;de.uni_heidelberg.zah.GaiaSky&lt;/code&gt;. The new ID &lt;code&gt;space.gaiasky.GaiaSky&lt;/code&gt; is already &lt;a href="https://flathub.org/apps/space.gaiasky.GaiaSky"&gt;verified&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As an extra perk, the new site allowed us to integrate things that were previously scattered in several places. I have written scripts to generate pages for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gaiasky.space/downloads/releases"&gt;gaiasky.space/downloads/releases&lt;/a&gt; &amp;ndash; old releases, together with their packages and release notes.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gaiasky.space/resources/datasets"&gt;gaiasky.space/resources/datasets&lt;/a&gt; &amp;ndash; a full listing of the datasets available in Gaia Sky, with their descriptions and additional metadata.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The site is responsive and should render fine on PC and mobile. If you are interested, give it a visit: &lt;a href="https://gaiasky.space"&gt;gaiasky.space&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Rendering volume aurorae and nebulae</title><link>https://tonisagrista.com/blog/2024/rendering-aurorae-nebulae/</link><pubDate>Wed, 18 Dec 2024 00:00:00 +0000</pubDate><author>Toni Sagrista Selles</author><guid>https://tonisagrista.com/blog/2024/rendering-aurorae-nebulae/</guid><description>&lt;p&gt;A few weeks ago someone created an &lt;a href="https://codeberg.org/gaiasky/gaiasky/issues/784#issuecomment-2512129"&gt;issue&lt;/a&gt; in the Gaia Sky Codeberg repository requesting the addition of aurorae to the Earth. They used as an example the aurora add-on in Cosmographia&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;, which, at the time, looked to me like it was using some kind of billboard particle system to generate the effect. I never thought it looked particularly good for an aurora; I thought Gaia Sky could do better. So I set on a quest to implement a &lt;em&gt;better looking&lt;/em&gt; aurora object in Gaia Sky. This wee quest would involve implementing three very different approaches to aurora rendering over the span of a few weeks, plus more than half a dozen 3D volume nebulae.&lt;/p&gt;
&lt;p&gt;In this post, I present the three different methods I implemented to render aurorae. But before anything, I need to take a small detour and talk about nebulae.&lt;/p&gt;
&lt;h2 id="lil-nebulae-detour"&gt;Li&amp;rsquo;l nebulae detour&lt;/h2&gt;
&lt;p&gt;If every story should start at the beginning and be told in full, I must first relate how this little quest took a little detour, at the beginning, to explore the rendering of volume nebulae. The OP mentioned in the Codeberg issue that he also gets requests to fly into nebulae during his tours, and Gaia Sky only contained billboards so far. Since Gaia Sky already had the necessary infrastructure to render &lt;strong&gt;ray-marched volumes&lt;/strong&gt; using a screen quad in a &lt;strong&gt;post-processing effect&lt;/strong&gt;, I decided that addressing this issue first made sense, as it seemed to require a &lt;em&gt;much&lt;/em&gt; lower effort and friction.&lt;/p&gt;
&lt;p&gt;Typically, volumes are rendered using an in-scene model (normally a box) that encapsulates the volume fully. The fragments that hit the model faces are sent to through the pipeline, and the volume ray-marching or ray-tracing is implemented in the respective fragment shader. In our approach, we&amp;rsquo;ll use a post-processing stage, which means that we create a full screen quad and we run a fragment shader for every pixel in our screen or window. The main challenges of this approach are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ground the model in the scene world. Since we are essentially painting on an empty, flat canvas, we need to ground our 3D volume in our 3D world. This step is taken care of by carefully selecting the coordinate system and constructing the ray direction for each fragment and camera and object positions correctly.&lt;/li&gt;
&lt;li&gt;Interaction with the depth buffer. We need our volume to be occluded by objects closer to the camera, and the opaque portions of our volume need to occlude further away parts of the scene. To fix this, we pass in the current depth buffer as a texture so that we can sample it at every pixel. Additionally, we also pass in the current scene frame buffer so that we have full control over the blending of our volume with the rest of the scene.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To get started, I browsed through &lt;a href="https://shadertoy.com"&gt;shadertoy&lt;/a&gt; in search for a good implementation of a ray-marched shader that I could use as a base for my nebulae. I found several, like the &lt;a href="https://www.shadertoy.com/view/MsVXWW"&gt;Dusty Nebula 4&lt;/a&gt; by Duke, and the &lt;a href="https://www.shadertoy.com/view/MdKXzc"&gt;Supernova Remnant&lt;/a&gt;, also by Duke. This &lt;a href="https://www.shadertoy.com/user/Duke"&gt;Duke&lt;/a&gt; guy seems to be the most prolific shadertoy author when it comes to large-scale dusty objects, and by a large margin. It&amp;rsquo;s not even close, so props to him and thank you very much for your work. All of his shaders seem to be licensed under &lt;a href="https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en"&gt;CC-BY-NC-SA 3.0&lt;/a&gt;, which is perfect for modification and reuse in Gaia Sky.&lt;/p&gt;
&lt;p&gt;So, taking some of these shaders as a baseline, I created new ones for the &lt;em&gt;Cat&amp;rsquo;s Eye&lt;/em&gt; nebula, the &lt;em&gt;Hourglass&lt;/em&gt; nebula, the &lt;em&gt;Trifid&lt;/em&gt; nebula, the &lt;em&gt;Butterfly&lt;/em&gt; nebula, and the &lt;em&gt;Crab&lt;/em&gt; nebula, among others. You can see some of them &lt;a href="https://www.shadertoy.com/user/toninoni/sort=newest"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2024/12/crab-nebula.jpg" width="60%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:60%;"&gt;
&lt;h4&gt;My implementation of the Crab nebula as a volume. See it live on &lt;a href="https://www.shadertoy.com/view/MfcBzH"&gt;Shadertoy&lt;/a&gt;.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Once the Shadertoy shaders were ready, I did a little more work on the post-processing ray-marching infrastructure to accommodate the new members. So far, I had only ever used it for the black holes. The final step was translating the shaders from Shadertoy to Gaia Sky. This was not too difficult, as they were already created with that step in mind.&lt;/p&gt;
&lt;h3 id="anatomy-of-a-nebula-shader"&gt;Anatomy of a nebula shader&lt;/h3&gt;
&lt;p&gt;As we mentioned, currently the nebulae are implemented in the post-processor. This means that they are rendered in &lt;em&gt;image space&lt;/em&gt; after the main rendering stage, which renders the geometry, has finished.Post-processors get as input data the current frame buffer, the depth buffer, and whatever additional textures are needed for the particular effect (such as one or more noise textures). Additionally, they get positions and orientations as uniforms or varyings computed in the vertex stage.&lt;/p&gt;
&lt;p&gt;All nebula shaders have the same main parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Set up&lt;/strong&gt; &amp;ndash; here we prepare the data for the raymarching step. The ray direction, the camera position and the object position are all computed/fetched here. We also recover the depth for the current fragment from the depth buffer. The set-up is typically done in the &lt;code&gt;main()&lt;/code&gt; function. In the following code we have an example of how the set-up stage is done.
&lt;details class="collapsedcode"&gt;
&lt;summary&gt;
&lt;span class="code"&gt;Code: &lt;/span&gt;&lt;span class="summary"&gt;set-up example&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-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#version 330 core&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;// Color buffer.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;uniform&lt;/span&gt; &lt;span style="color:#ff636f"&gt;sampler2D&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;u_texture0&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;// Depth buffer (log).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;uniform&lt;/span&gt; &lt;span style="color:#ff636f"&gt;sampler2D&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;u_texture1&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;// Position of our camera with respect to the object to render.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;uniform&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;u_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:#828b96;font-style:italic"&gt;// Texture coordinates.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec2&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;v_texCoords&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;// Ray from camera to fragment.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;in&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;v_ray&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;// Output color.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#58a1dd"&gt;layout&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;location&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;out&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec4&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;fragColor&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;void&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;main&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;// Ray direction.&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;rayDir&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;v_ray&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;// Camera 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;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;camPos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;u_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;// Position of object. We always put the position of the object at the origin.&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;objPos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;0.0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0.0&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:#828b96;font-style:italic"&gt;// Sample depth buffer.&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;depth&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;texture&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;u_texture1&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;v_texCoords&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:#58a1dd"&gt;depth&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;rayDir&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;// Color of pre-existing scene.&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;col&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;texture&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;u_texture0&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;v_texCoords&lt;/span&gt;).&lt;span style="color:#58a1dd"&gt;rgb&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;// Here would go the call to ray-marching.&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;// Finally, the blending would go here.&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;/details&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ray marching&lt;/strong&gt; &amp;ndash; here is where the actual ray-marching happens. In ray marching, we shoot a ray for every pixel, divide it into samples, and gather information at every step. This information is usually the density and color of the medium we are rendering.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Intersection check&lt;/strong&gt; &amp;ndash; since we implement this in image space (we shoot a ray for each pixel in our window), we first need to check whether our ray intersects the bounding volume containing the object or not. In nebulae, this volume is typically a sphere. Simply enough, this is done with a straightforward ray-sphere intersection function, which returns the intersection near and far distances if there is an intersection at all, or nothing if the ray does not hit the sphere. The code below shows how to do the ray-sphere intersection.
&lt;details class="collapsedcode"&gt;
&lt;summary&gt;
&lt;span class="code"&gt;Code: &lt;/span&gt;&lt;span class="summary"&gt;ray-sphere intersection example&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-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;bool&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;RaySphereIntersect&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;org&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;dir&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;out&lt;/span&gt; &lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;near&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;out&lt;/span&gt; &lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;far&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;b&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;dir&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;org&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;c&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;org&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;org&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;8.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;float&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;b&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:#ff636f"&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:#ff636f"&gt;if&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;delta&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&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;return&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;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;deltasqrt&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sqrt&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:#58a1dd"&gt;near&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&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:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;deltasqrt&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;far&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&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:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;deltasqrt&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;far&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Main loop&lt;/strong&gt; &amp;ndash; here we loop over a maximum number of iterations and accumulate the density. Every cycle advances our position through the ray by a given step distance. The code below illustrates a rather typical main loop. The following code completes the &lt;code&gt;main()&lt;/code&gt; function to include the call to the main loop (&lt;code&gt;raymarching()&lt;/code&gt;) and the alpha blending at the very bottom.
&lt;details class="collapsedcode"&gt;
&lt;summary&gt;
&lt;span class="code"&gt;Code: &lt;/span&gt;&lt;span class="summary"&gt;main loop example&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-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#828b96;font-style:italic"&gt;#define ITERATIONS 50&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;raymarch&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;camPos&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;rayDir&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;objPos&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;depth&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;// Ray direction.&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;rd&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;rayDir&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;// Ray origin.&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;ro&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;camPos&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, td: local, total density&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;// w: weighting factor&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;ld&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 style="color:#58a1dd"&gt;td&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 style="color:#58a1dd"&gt;w&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// t: length of the ray&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;// d: distance function&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;d&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:#58a1dd"&gt;t&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff636f"&gt;const&lt;/span&gt; &lt;span style="color:#ff636f"&gt;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;h&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.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;vec4&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sum&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:#a6be9d"&gt;0.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;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;min_dist&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 style="color:#58a1dd"&gt;max_dist&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// If our ray intersects the sphere...&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;RaySphereIntersect&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;ro&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;rd&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;min_dist&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;max_dist&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&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;min_dist&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;step&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;t&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;min_dist&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;// Raymarch loop&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;int&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:#a6be9d"&gt;0&lt;/span&gt;; &lt;span style="color:#58a1dd"&gt;i&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ITERATIONS&lt;/span&gt;; &lt;span style="color:#58a1dd"&gt;i&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;vec3&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;ro&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;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;rd&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;// Loop break conditions.&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;// Here we check the current distance agains the depth and the&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;// maximum distance.&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;t&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;depth&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;&amp;gt;&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;max_dist&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 style="color:#828b96;font-style:italic"&gt;// Evaluate distance function.&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;// The map function returns the density at a given 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;d&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;map&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;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:#828b96;font-style:italic"&gt;// Point light calculations.&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;ldst&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;0.0&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;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:#828b96;font-style:italic"&gt;// Here, the color of light depends on the distance from the center&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;// of the object. Typically, this is more complicated than what&amp;#39;s here.&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;lightColor&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;mix&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;colorA&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;colorB&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;ldst&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;d&lt;/span&gt; &lt;span style="color:#ff636f"&gt;&amp;lt;&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:#828b96;font-style:italic"&gt;// Compute local density.&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:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;h&lt;/span&gt; &lt;span style="color:#ff636f"&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#828b96;font-style:italic"&gt;// Compute weighting factor.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;w&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;td&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;ld&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;// Accumulate density.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;td&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;w&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:#a6be9d"&gt;200.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;vec4&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;col&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:#a6be9d"&gt;0.3&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0.3&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0.3&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;td&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;// Emission.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;sum&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sum&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;a&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;sum&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;rgb&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0.0&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.2&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;// Uniform scale density.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;col&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;a&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*=&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.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;// Colour by alpha.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;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;col&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:#828b96;font-style:italic"&gt;// Alpha blend in contribution.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;sum&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sum&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;col&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;sum&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;td&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:#a6be9d"&gt;90.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;// Optimize step size near the 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;t&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;max&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;d&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.1&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;max&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;min&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;length&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;ldst&lt;/span&gt;), &lt;span style="color:#58a1dd"&gt;length&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;ro&lt;/span&gt;)), &lt;span style="color:#a6be9d"&gt;1.0&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&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;// Simple scattering.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;sum&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:#58a1dd"&gt;exp&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;ld&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.2&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.4&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;sum&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;sum&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 style="color:#58a1dd"&gt;sum&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;xyz&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sum&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;xyz&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;sum&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;xyz&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; (&lt;span style="color:#a6be9d"&gt;3.0&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:#58a1dd"&gt;sum&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&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:#ff636f"&gt;vec4&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;sum&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;xyz&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;td&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;/details&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Blending&lt;/strong&gt; &amp;ndash; finally, we have the blending. Most nebulae are blended using regular alpha blending, but implementing additive blending is trivial.
&lt;details class="collapsedcode"&gt;
&lt;summary&gt;
&lt;span class="code"&gt;Code: &lt;/span&gt;&lt;span class="summary"&gt;alpha blending example&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-glsl" data-lang="glsl"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff636f"&gt;void&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;main&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;rayDir&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;v_ray&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;camPos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;u_pos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;*&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;2.0e-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;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;objPos&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;0.0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;0.0&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;float&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;depth&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;texture&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;u_texture1&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;v_texCoords&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:#58a1dd"&gt;depth&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;rayDir&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;col&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;texture&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;u_texture0&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;v_texCoords&lt;/span&gt;).&lt;span style="color:#58a1dd"&gt;rgb&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;// Ray marching.&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;rmcol&lt;/span&gt; &lt;span style="color:#ff636f"&gt;=&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;raymarch&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;camPos&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;rayDir&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;objPos&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;depth&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;// Alpha blending.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;fragColor&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;col&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;rmcol&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;a&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;rmcol&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;rmcol&lt;/span&gt;.&lt;span style="color:#58a1dd"&gt;a&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;/details&gt;
&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This covers the basics of our nebulae shaders. The video below shows some of the nebulae that I have adapted for Gaia Sky and that will be available in the NGC2000 catalog when the next version is out, soon™.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;video width="75%" poster="/img/2024/12/nebulae-raymarch.jpg" preload="none"controls="controls"
&gt;
&lt;source src="https://tonisagrista.com/img/2024/12/nebulae-raymarch.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;Six of the new nebulae implemented in Gaia Sky.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Now, back to the aurora.&lt;/p&gt;
&lt;h2 id="approaching-aurora-rendering"&gt;Approaching aurora rendering&lt;/h2&gt;
&lt;p&gt;Aurorae are natural light displays caused by interactions between charged particles from the Sun (solar wind) and the Earth&amp;rsquo;s magnetosphere and atmosphere. At the poles, magnetic field lines converge and funnel these particles into the upper atmosphere (thermosphere), where they collide with oxygen and nitrogen molecules. The collisions excite the molecules, causing electrons to jump to higher energy states. The excited atoms are unstable, so as the electrons return to their normal states, they release packets of energy in the form of photons. The color (frequency) of these photons depends on the type of gas and altitude: green from oxygen (~100-150 km), red from oxygen higher up, where lower density causes slower de-excitation (~200+ km), and blue or purple from nitrogen. The shifting, curtain-like shapes result from changes in the solar wind and Earth&amp;rsquo;s magnetic field.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;
&lt;source srcset="https://tonisagrista.com/img/2024/12/aurora-picture.jxl" type="image/jxl" /&gt;
&lt;source srcset="https://tonisagrista.com/img/2024/12/aurora-picture.avif" type="image/avif" /&gt;
&lt;img src="https://tonisagrista.com/img/2024/12/aurora-picture.jpg" width="75%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:75%;"&gt;
&lt;h4&gt;A picture of an aurora borealis in Greenland. The green, red and blue colors can be seen in different parts of the curtains. Photo by &lt;a href="https://pixnio.com/nature-landscapes/night/aurora-borealis-astronomy-atmosphere-phenomenon-planet-majestic-sky-night"&gt;&lt;em&gt;Visit Greenland&lt;/em&gt;&lt;/a&gt; on &lt;a href="https://pixnio.com/"&gt;Pixnio&lt;/a&gt;, CC0.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Aurorare are, then, purely volumetric and perfectly emissive phenomena. This fact strongly points to a volume ray-caster being the best method for rendering aurorare, but implementing volume rendering shaders is hard, so I explored some alternative options first. I have very creatively labeled them 1, 2 and 3.&lt;/p&gt;
&lt;h3 id="1-mesh-w-custom-shader"&gt;1. Mesh w/ custom shader&lt;/h3&gt;
&lt;p&gt;I like to simplify problems to their very basics. Even though aurora curtains have volume (width), their extent is limited when compared to their height or their footprint length. So, obviously, my first thought was to implement aurorae as a curved mesh with a special shader to render the curtain elements. I knew it wouldn&amp;rsquo;t look super good, but maybe it was &lt;strong&gt;good enough&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I set up the infrastructure and created the object. At first, I used an uncapped cylinder mesh. The faces are unculled, since they need to be visible from both sides. Once the mesh was in place, I created the fragment shader which would render the actual emissive elements. This shader is available on Shadertoy: &lt;a href="https://www.shadertoy.com/view/MfdBW4"&gt;Base Aurora Shader&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I loaded the shader and applied it to the mesh. The results were&amp;hellip; underwhelming. The shader itself is ok, but when applied to the flat surface of the cylinder, it just does not cut it. When the camera moves the effect is totally shattered and you can tell right away that this is a flat object.&lt;/p&gt;
&lt;p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;
&lt;source srcset="https://tonisagrista.com/img/2024/12/aurora-simple-2.jxl" type="image/jxl" /&gt;
&lt;source srcset="https://tonisagrista.com/img/2024/12/aurora-simple-2.avif" type="image/avif" /&gt;
&lt;img src="https://tonisagrista.com/img/2024/12/aurora-simple-2.jpg" width="60%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:60%;"&gt;
&lt;h4&gt;The aurora shader on a cylindrical mesh, seen from afar.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;
&lt;source srcset="https://tonisagrista.com/img/2024/12/aurora-simple-1.jxl" type="image/jxl" /&gt;
&lt;source srcset="https://tonisagrista.com/img/2024/12/aurora-simple-1.avif" type="image/avif" /&gt;
&lt;img src="https://tonisagrista.com/img/2024/12/aurora-simple-1.jpg" width="60%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:60%;"&gt;
&lt;h4&gt;A close-up. Meh.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;It was clear to me that this wasn&amp;rsquo;t good enough. So I went browsing on shadertoy again.&lt;/p&gt;
&lt;h3 id="2-isolated-noise-aurora"&gt;2. Isolated noise aurora&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ll start by saying that this second effort went nowhere, but had the pleasant side effect of having implemented the true in-scene, bounding box-based volume infrastructure.&lt;/p&gt;
&lt;p&gt;The base idea was to adapt a pre-existing ray-marching shader to be rendered as a volume in Gaia Sky. The shader in question is &lt;a href="https://www.shadertoy.com/view/MfdfR2"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;&lt;img src="https://tonisagrista.com/img/2024/12/isolated-volume-aurora.jpg" width="60%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:60%;"&gt;
&lt;h4&gt;The isolated aurora shader ray-marches an aurora using fractal brownian motion noise as footprints. Looks good in shadertoy, not so much in Gaia Sky. See it live on &lt;a href="https://www.shadertoy.com/view/MfdfR2"&gt;Shadertoy&lt;/a&gt;.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;That shader contains some code to to intersect the ray with an axis-aligned bounding box (AABB), so naturally I implemented the necessary infrastructure to be able to ship external volume rendering shaders on &lt;em&gt;bounding meshes&lt;/em&gt;. And it kind of worked, but I could never make it behave exactly the way I wanted. I had several problems with the integration in Gaia Sky, so instead of spending lots of time and effort into solving them only to end up with a half-assed, bad-looking aurora, I decided to try the next approach.&lt;/p&gt;
&lt;h3 id="3-true-volume-aurora"&gt;3. True volume aurora&lt;/h3&gt;
&lt;p&gt;This is the good one. It is based on Lawlor and Genetti&amp;rsquo;s 2011 paper &lt;em&gt;&amp;ldquo;Interactive Volume Rendering Aurora on the GPU&amp;rdquo;&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;/em&gt;. Key innovations include a height-based aurora model, a GPU-friendly atmosphere model, efficient ray marching with distance fields, and interactive frame rates (20–80 FPS). The aurora footprints are initially represented as splines, which are then fed into a stem-like fluid simulator to generate the true footprint textures. Then, they sample the rays and stretch the footprints in the vertical direction using a physically-based vertical electron deposition function. They also accelerate the ray-marching step using a 2D signed distance field (SDF) texture generated from the footprint.&lt;/p&gt;
&lt;p&gt;I implemented all of this and the results were quite good. I drew the footprints myself using Gimp. I use cubemaps for the footprints and the signed distance function texture, as they eliminate the artifacts at the poles typical of regular UV mapping on spheres. The aurora volume itself is modeled with a &lt;strong&gt;flipped sphere&lt;/strong&gt; that wraps around the Earth. I generate it programmatically.&lt;/p&gt;
&lt;p&gt;I generated the SDF textures with &lt;a href="https://github.com/j-norberg/sdfer"&gt;&lt;code&gt;sdfer&lt;/code&gt;&lt;/a&gt;, a small CLI utility that generates distance fields from raster images.&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;picture&gt;
&lt;source srcset="https://tonisagrista.com/img/2024/12/aurora-sidebyside.jxl" type="image/jxl" /&gt;
&lt;source srcset="https://tonisagrista.com/img/2024/12/aurora-sidebyside.avif" type="image/avif" /&gt;
&lt;img src="https://tonisagrista.com/img/2024/12/aurora-sidebyside.jpg" width="90%" loading="lazy"decoding="async" /&gt;
&lt;/picture&gt;&lt;figcaption style="margin: 0 auto; width:90%;"&gt;
&lt;h4&gt;Left: the footprint texture, created with Gimp. Center: the signed distance function texture. Right: the final render.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;By far, the largest problem I faced was in the UV coordinate mapping. In order to sample the 2D textures from the 3D samples in the ray marching steps, I implemented a projection function that maps 3D points to 2D UV coordinates.&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;vec2&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;uv&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;p&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;rayDir&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;camPos&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;p&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;p&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:#ff636f"&gt;vec2&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;0.5&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atan&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:#58a1dd"&gt;z&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:#58a1dd"&gt;x&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;PI2&lt;/span&gt;), &lt;span style="color:#58a1dd"&gt;asin&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:#58a1dd"&gt;y&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;PI&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.5&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;However, when the camera was inside the volume, this gave me mirrored ghosting aurorae in their opposite direction relative to the camera. I struggled to solve this issue for a while, until I realized that I just had to enforce that the sampling point had to be on the &lt;em&gt;right&lt;/em&gt; side of the ray:&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;vec2&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;uv&lt;/span&gt;(&lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;p&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;rayDir&lt;/span&gt;, &lt;span style="color:#ff636f"&gt;vec3&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;camPos&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;dot&lt;/span&gt;(&lt;span style="color:#58a1dd"&gt;rayDir&lt;/span&gt;, &lt;span style="color:#58a1dd"&gt;p&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.000&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;// Ok.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#58a1dd"&gt;p&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;p&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:#ff636f"&gt;vec2&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;0.5&lt;/span&gt; &lt;span style="color:#ff636f"&gt;-&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;atan&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:#58a1dd"&gt;z&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:#58a1dd"&gt;x&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; (&lt;span style="color:#58a1dd"&gt;PI2&lt;/span&gt;), &lt;span style="color:#58a1dd"&gt;asin&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:#58a1dd"&gt;y&lt;/span&gt;) &lt;span style="color:#ff636f"&gt;/&lt;/span&gt; &lt;span style="color:#58a1dd"&gt;PI&lt;/span&gt; &lt;span style="color:#ff636f"&gt;+&lt;/span&gt; &lt;span style="color:#a6be9d"&gt;0.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;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;// Invalid result.&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:#ff636f"&gt;vec2&lt;/span&gt;(&lt;span style="color:#a6be9d"&gt;5.0&lt;/span&gt;, &lt;span style="color:#a6be9d"&gt;5.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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This solved the artifacts. I then implemented some additional suggestions by O. Lawlor (the paper&amp;rsquo;s author), and I got it to a state where I really liked it. See the video below:&lt;/p&gt;
&lt;figure class="fig-center"&gt;&lt;video width="75%" poster="/img/2024/12/final-aurora.jpg" preload="none"controls="controls"
&gt;
&lt;source src="https://tonisagrista.com/img/2024/12/final-aurora.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;The final volume aurora in Gaia Sky.&lt;/h4&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="future-work"&gt;Future work&lt;/h2&gt;
&lt;p&gt;A few things can be improved in the current aurora:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Render the aurorae to an off-screen buffer. This would unlock a series of benefits:
&lt;ul&gt;
&lt;li&gt;Render at lower resolution to increase performance.&lt;/li&gt;
&lt;li&gt;Apply blur and glow to mitigate jittering artificts.&lt;/li&gt;
&lt;li&gt;Full control over blending with scene.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Proper color modeling. The base and high colors are now passed in from a JSON descriptor file. This could be improved by adopting a physical model of the upper atmosphere and generating the colors accordingly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post, I have shown how the new volume nebulae and aurorae in Gaia Sky came about. I have given some implementation details, and discussed their pros and cons. In the end, I think the time spent on this was well spent, as the results were quite satisfactory.&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;SPICE-Enhanced Comosgraphia Mission Visualization Tool, &lt;em&gt;NAIF&lt;/em&gt;: &lt;a href="https://naif.jpl.nasa.gov/naif/cosmographia.html"&gt;https://naif.jpl.nasa.gov/naif/cosmographia.html&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;Interactive Volume Rendering Aurora on the GPU, &lt;em&gt;Journal of WSCG&lt;/em&gt;: &lt;a href="https://www.researchgate.net/publication/220200705_Interactive_Volume_Rendering_Aurora_on_the_GPU"&gt;https://www.researchgate.net/publication/220200705_Interactive_Volume_Rendering_Aurora_on_the_GPU&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;/ol&gt;
&lt;/div&gt;</description></item></channel></rss>