A Software Compositor
A raw framebuffer flickers and crawls. A compositor fixes both: draw into an off-screen buffer, copy only what changed, and mark the framebuffer write-combining so a 1440p fill stops being a slideshow.
Module 01 ended on a cliffhanger: the full-screen fill that was instant in QEMU crawled on real hardware. This module solves it, then builds the drawing primitives — off-screen buffering, change tracking, and the rounded-window look — that everything visual sits on.
3.1 Write-combining the framebuffer
Firmware often leaves the framebuffer mapped uncached (UC): every 4-byte pixel write is its own bus transaction, so a 2560×1440 fill is ~3.7 million round-trips. Marking the range write-combining (WC) lets the CPU batch those into full cache-line bursts. This is the difference between 5 fps and 60 on real hardware, and it does nothing in QEMU — which is exactly why it's a "real metal" lesson.
// A variable-range MTRR: set the framebuffer physical range to
// memory type 0x01 (Write-Combining). base and mask are MSRs.
void mtrr_set_wc(UINT64 base, UINT64 size) {
UINT64 mask = ~(size - 1) & 0x0000FFFFFFFFF000;
wrmsr(IA32_MTRR_PHYSBASE0, (base & ~0xFFF) | 0x01); // 0x01 = WC
wrmsr(IA32_MTRR_PHYSMASK0, mask | 0x800); // 0x800 = valid
}This is the textbook case of "the emulator lies." In QEMU the framebuffer is host RAM, so UC vs WC is invisible — your fill is instant either way. On a real panel the difference is night and day. If you only ever test in QEMU you will never know your OS feels like a slideshow to everyone else. Flash the stick.
3.2 Double buffering
Drawing directly to the framebuffer means the user sees every intermediate step — a half-drawn window, flicker, tearing. The fix: draw everything into an off-screen back buffer in normal RAM (fast, cached), then copy the finished frame to the framebuffer in one pass.
static UINT32 *back; // off-screen buffer, cached RAM
static UINT32 *front; // the real framebuffer (WC)
void compositor_init(int w, int h) {
back = pmm_alloc_pages(w * h * 4 / 4096 + 1);
front = framebuffer;
}
// all drawing goes to `back`. present() copies to `front`.
void compositor_present(void) {
memcpy32(front, back, ScreenW * ScreenH);
}3.3 Dirty rectangles
Copying the whole back buffer every frame is wasteful — most of the screen didn't change. Track dirty rectangles: when something draws, record the region it touched, and at present time copy only those regions to the front buffer. This is what makes a hover highlight cost microseconds instead of a full-screen blit.
void compositor_mark_dirty_rect(int x, int y, int w, int h) {
// grow the frame's dirty bounds to include this rect
if (x < dirty.x0) dirty.x0 = x;
if (y < dirty.y0) dirty.y0 = y;
if (x+w > dirty.x1) dirty.x1 = x+w;
if (y+h > dirty.y1) dirty.y1 = y+h;
}
void compositor_present(void) {
for (int y = dirty.y0; y < dirty.y1; y++) // only dirty rows
memcpy32(front + y*Pitch + dirty.x0,
back + y*Pitch + dirty.x0, dirty.x1 - dirty.x0);
dirty_reset();
}If a hover highlight draws but you forget to mark the region it left as dirty, the old highlight never gets erased — you get trails following the cursor, and alpha overlays compound into dark boxes. Every visual state change must mark both where it drew and where it used to be. This exact bug bit ForgeOS; now you know the shape of it.
3.4 Rounded rectangles and text
Two primitives carry the whole UI: a rounded rectangle (every window, button, card) and a bitmap font blitter (every label). The rounded rect is a filled rect with the four corners clipped by a simple curve; text is copying glyph bitmaps pixel by pixel.
void draw_rounded_rect(int x, int y, int w, int h, int r, UINT32 col) {
if (r*2 > w) r = w/2;
if (r*2 > h) r = h/2;
fill_rect(x+r, y, w-2*r, h, col); // centre column
fill_rect(x, y+r, r, h-2*r, col); // left/right edges
fill_rect(x+w-r, y+r, r, h-2*r, col);
for (int row = 0; row < r; row++) { // the four corner arcs
int inset = r - isqrt(r*r - (r-row)*(r-row));
fill_rect(x+inset, y+row, w-2*inset, 1, col);
fill_rect(x+inset, y+h-row-1, w-2*inset, 1, col);
}
}The border of a rounded rect must follow the same curve. If you draw a rounded fill and then stroke a plain rectangular outline around it, the square corners poke past the rounded fill and frame it in a mismatched box — the highlight looks inset and wrong. Build a rounded-border primitive too, and use it. (This one framed every button in ForgeOS until it was fixed at the primitive level.)
- A write-combined framebuffer — fills are fast on real hardware
- A back buffer; nothing draws directly to the screen anymore
- Dirty-rectangle tracking so only changed regions are copied
draw_rounded_rectand a working text blitter- A rounded-border primitive that follows the corners
Module 04 — Windows & a Desktop
A window manager with draggable, resizable chrome, a dock, and a shared UI toolkit so every app looks consistent and themeable. The desktop starts to feel real.
Get lifetime access — $149 →