Replacing `Queue` With a `RingBuffer`, and Bumping Cosmos to .NET 10

There's a fork of Cosmos on my box that I push to a handful of times a month, and two of those pushes from the last month are worth writing down: a RingBuffer that replaced Queue in the keyboard driver and then got promoted into the stable HAL contract, and a runtime bump to .NET 10.0.12.

Both are on git.punksky.com/zarlo/Cosmos as their own branches rather than stacked on one long-lived fork branch. Neither is finished work. Both are the kind of change that's much cheaper to do deliberately than to do later under pressure.

Cosmos is a real kernel, not a toy

Cosmos is an operating system construction kit. You write C#, it compiles down through IL2CPU, and the result boots. It runs under QEMU and on real hardware.

That detail matters for everything below. A Queue<T> in an ASP.NET service is a performance question. The same Queue<T> sitting underneath a keyboard interrupt in an OS kernel is a correctness question with a much shorter fuse, because the producer is hardware and hardware does not wait. Keystrokes arrive on their own schedule regardless of whether anything is draining the buffer.

The KeyboardManager change

The branch is zarlo/ringbuffer/0, four commits:

Commit Files Change
Introduce RingBuffer and replace Queue in KeyboardManager 4 −108 / +198
Promote RingBuffer to stable HAL contract, and update the promotion guard 3 −62 / +72
Run format. 19 −105 / +110
revert small change. 1 −4 / +4

Twenty-seven files, net 279 insertions against 284 deletions. Almost exactly break-even, which is the shape you want to see in a refactor.

Queue<T> is backed by a ring buffer internally, so this isn't about discovering a data structure. It's about having a bounded, wrap-around, explicitly sized one at the point where the kernel needs it, instead of a general-purpose collection that will happily grow and reallocate on a path that shouldn't allocate at all.

The commit that actually mattered

The first commit introduced the type. The second one is the one I'd defend in review.

Cosmos ships its Hardware Abstraction Layer drivers independently of the OS core. A RingBuffer that lives inside KeyboardManager is an implementation detail of one driver. The same RingBuffer in the stable HAL contract is a promise: every driver written against Cosmos can rely on it, and it will not change shape underneath them.

Those are very different commitments, and the second one has a cost. It's a public API, which means it needs to be stable, which means every future change to it has to be weighed against people already depending on it. The HAL promotion guard that went up in the same commit is there to catch a driver trying to depend on something that hasn't been promoted.

The general lesson, which applies well outside kernel work: introducing a type and stabilising it are two separate decisions and they deserve to be two separate commits. Bundling them makes the review conversation about the refactor when it should be about the contract.

The Run format. commit touching nineteen files is the other half of that honesty. Reformatting is part of the change, not something to hide in a separate cleanup the week after.

The runtime bump

The other branch is zarlo/runtime_update/10_0_12/0, and it is a single commit: Update to 10.0.12 for runtime.

One file. Zero lines added, zero lines removed.

I'm not going to dress that up. That's genuinely what the commit looks like, because a runtime version bump in Cosmos isn't a dependency edit you can eyeball in a diff. It's the consequence of taking upstream, which is where the real work was sitting.

The branch forked from upstream 3.0.89, which had landed a run of substantial changes in the days before: a garbage collector fix for finding objects behind an interior root by walking its segment, three separate xHCI USB merges for keyboard, mass storage and hotplug, and a fix for hanging when QEMU exits before it connects its monitor.

Why a managed OS can't bump .NET like an application

An application bumps .NET by editing a TargetFramework and re-running dotnet restore.

A managed OS cannot do that, because Cosmos ships its own runtime and a re-linked, trimmed subset of the base class library. The runtime version isn't a package reference, it's a build input that has to line up with the OS's own abstractions over memory management, garbage collection and driver services. Moving the runtime underneath an operating system that owns the garbage collector is a different proposition from moving it underneath a web service that merely calls into one.

So the sequencing is the actual engineering content of this branch: take upstream first, land the version bump separately on its own branch, and keep the two changesets separable. When something breaks, knowing whether it was the runtime or the twenty commits underneath it is worth more than having a tidy single history.

Why branches, and not a fork

All three of these live as separate branches rather than one long-lived divergence from upstream. That keeps each change reviewable on its own, lets me drop one without losing the others, and means a rebase against upstream is a mechanical operation instead of a negotiation.

The third branch is zarlo/paging/0, and it's the one still in progress: a platform-agnostic paging API, separate from the ring buffer, for the memory side of the same problem. A ring buffer is the primitive you use when you know the bounds. Paging is what you reach for when you don't.

More on how far this can be pushed is in Building Bare-Metal .NET.