Imaginarium

Devlog - Q2 2026

A Fun Hack for Simple Framebuffer Font Handling

April 18, 2026 — Khitiara

Text rendering is kinda f*cked.

The framebuffer terminal in Imaginarium only renders single-codepoint characters. This makes things a lot easier, but doesn’t remove all the problems. (For those wondering, imaginarium uses unicode box drawing characters and visible-control characters which are multibyte codepoints, so we can’t just iterate bytes.)

The interesting part of handling font rendering, at least here, is how to efficiently store and lookup glyphs. The font, zevv-peep, is preprocessed by a tool at build time so we can pick any efficient structure, but the structure must be buildable at comptime as the preprocess outputs valid zig code.

The solution I arrived at is to rely on enums of all things, using zig’s EnumArray (and underlying EnumIndexer) to convert a sparse enum into a compact index space.

Each glpyh in the font gets a Glyph struct as expected, placed in a compact array, but the glyph is also added as a member of an enum, with tag value equal to its integer encoding, and looking up a Glyph is done by using @enumFromInt and calling EnumArray.get. As the enum‘s structure and values can only be iterated at comptime, the EnumIndexer in zig’s std is already designed to be created at comptime, and the resulting EnumArray being compact is well-suited to storing the font glyphs.

Reworking IRQ Handling

April 02, 2026 — Khitiara

Interrupt handling infrastructure is kind of important for an OS to get right.

And the old way Imaginarium was doing it was definitely not right.

Before discussing the new system, a bit of context: It used to be that an interrupt number was a tagged union of different domains, such as Global System Interrupts (GSIs) as defined by ACPI and mirrored by GIC INTIDs, or Message Signalled Interrupts (MSIs), which have to route through an IOMMU if present and enabled and need to provide an (address, data) pair to the interrupt producing driver to configure itself, or Private Peripheral Interrupts (PPIs) that are given INTIDs on arm but are an entirely separate mechanism on x86 with the APIC and had to be shoehorned in using annoying hacks. Every stage of the IRQ handling had to be special cased per architecture and have switches over this tagged union.

The New Design

The new system trades some added indirection for a great deal of simplification. While interrupts are still represented by an Irq struct as before, their interrupt ids are now a (port, vector) pair and all Irq objects point to their Irq.Domain.

IrqDomains are responsible for reserving virtual interrupt lines, identified by the (port, vector) pair and masking and unmasking interrupts (if supported by the represented controller). Each architecture provides one IrqDomain for routing GSIs and one for routing Software Generated Interrupts (SGIs), which may be backed by internal IrqDomains representing hardware concepts.

As an example, the x86 architecture HAL has an IrqDomain for x86 vectors, which maps port to a cpu id and vector to an x86 interrupt vector. The GSI domain on x86 uses this “x86 vector” domain to reserve terminal vectors and maps the requested GSI to that terminal vector using the IOAPICs, whereas the SGI domain is a thin wrapper around x86 vectors.

MSIs Make Everything Harder

Those of you familiar with x86 may find the title of this section strange - after all the message and data fields for MSIs are easily derived from the terminal x86 vector (leaving IOMMUs aside for now, as Imaginarium does not yet implement support for them).

If that’s you, allow me to introduce you to the hell that is MSIs on the GIC (aarch64’s interrupt controller).

A system with a GIC (v3 or v4 - v2 is old to be worths supporting and v5 isnt yet supported by ACPI or emulators) that supports MSIs will have a component called an Interrupt Translation Service (ITS), which is responsible for dispatching MSIs as part of a range of INTIDs known as Locality-specific Peripheral Interrupts (LPIs).

The ITS has a 32-bit mmio register called GITS_TRANSLATER that devices write to to issue an MSI. The value written is arbitrary and known as the Event ID. Each possible source of MSIs is also associated with a 32-bit “Device ID” which is communicated to the ITS by some unspecified means associated with the bus it’s on.

The Device ID is related to PCI requester ids using the IORT ACPI table, and PCI requester ids have their own hell involving PCIe-PCI bridges that I wont get into detail about here but did cost me several hours of my life because PCI cant make a reasonable standard about anything.

When the ITS receives a write to the GITS_TRANSLATER register, it looks up the source’s Device ID in its Device Table, which are a two-level table (the first level is managed by software and the second level is entirely opaque, but both have to be allocated by software and communicated to the ITS during init). If the Device Table has a valid entry for the Device ID, that entry will point to an Interrupt Translation Table (ITT, also opaque but allocated by software) to find an entry for the Event ID which was written to the register.

The ITT entry for a given (Device ID, Event ID) pair has a 16-bit Interrupt Collection ID (ICID) and a 24-bit physical interrupt id (pINTID). The ITS then looks up in another table (again allocated by software), the Collection Table, for the given ICID. The Collection Table entry has either the physical address of a GIC Redistributor (GICR) or the GICR’s associated processor number (which depends on the PTA bit in the read-only GITS_TYPER register associated with the ITS).

Once it has a pINTID and a reference to a GICR, the ITS issues that INTID to the GICR.

The GICR then has to look up the INTID in its own tables (allocated by software) to determine the interrupt priority and masking. If the interrupt is unmasked in this final table it is either dispatched to the CPU or has a pending bit set in yet another software-allocated table.

And then software has to reverse all the lookups somehow to figure out what MSI vector on what device corresponds to the given INTID it received.

Oh and did I mention that you have to allocate a ring buffer to use to set up everything on the ITS and add/remove/change entries in its tables? Not the GICR LPI tables though, those have a fixed binary representation so you manage those yourself. (but dont forget to put an INV command in the ITS’s command ring buffer to make sure the GICRs flush their caches!)

MSIs: The Solution

The solution I arrived on for Imaginarium for the GIC MSI hell is shockingly simple: Instead of providing a single IrqDomain for MSIs, an architecture HAL provides an MsiDomain instead. An MsiDomain has one function: to create per-device-id IrqDomains for routing MSIs.

The Irq ID for an MSI uses the (port, vector) pair to store the (address, data) pair for the MSI, and uses the domain to hold the Device ID if relevant for the architecture.

On x86, the MsiDomain just returns a singleton IrqDomain. (this will likely need to change once IOMMU support happens)

On aarch64 with the GIC ITS, the MsiDomain returns a refcounted per-device-id IrqDomain which uses its internal stored Device ID to request vectors from the LPI domain using (Device ID, Event ID) as the requested interrupt ID, and then wraps the result with an Irq object that has (GITS_TRANSLATER, Event ID) as its (port, vector) ID pair.

PCI Device-Local Masking: The Final Wrinkle

The PCI(e) enumeration/bus driver wraps the per-device IrqDomain for MSIs with its own. This PCI wrapper domain overrides the mask and unmask functions to use the masking functionality of the MSI and MSI-X PCI features (PVM for MSI if supported; MSI-X always supports per-vector masking). In addition, the PCI wrapper domain sets the MSI(-X) address and data registers on reservation of interrupts.

This way, PCI MSI handling can be transparent to the device driver - it just needs to provide the prefilter_resources IRP dispatch to lower the number of requested MSIs.

Getting Going

March 10, 2026 — Khitiara

And so it begins.

Welcome to the docs/devlog site for Imaginarium, my attempt at making an OS in the Zig programming language. The design of Imaginarium is generally inspired by the NT Kernel (better known as the inner part of Windows that actually works and is good), albeit with my own spin on things – Imaginarium is inspired by NT, not a clone of NT.

Work on Imaginarium has been ongoing since January 17, 2024, and the source code is available on codeberg under the terms of the Mozilla Public License Version 2.0.

As of the time of this post, Imaginarium is capable of booting under UEFI with QEMU on the “q35” x86_64 virtual machine and the “virt,acpi=on” aarch64 virtual machine, has an initial non-preemptive prioritized scheduler implementation with kernel threads, can enumerate the ACPI Namepsace and PCI Buses, and receives timer interrupts on both supported platforms.

For more information on how Imaginarium works, check the docs!