PHASE II · SURFACE / MODULE 03 ·

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 03 — walkthrough
module introcoming soon

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.

Lesson 3.1

3.1 Write-combining the framebuffer

the single biggest performance lever in the OS
3.1 — MTRR / PAT write-combining
~18 mincoming soon

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.

mtrr.c — mark the framebuffer WCcopy
// 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
}
Test this ON HARDWARE, always

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.

Lesson 3.2

3.2 Double buffering

draw off-screen, show all at once
3.2 — the back buffer
~14 mincoming soon

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.

compositor.ccopy
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);
}
Lesson 3.3

3.3 Dirty rectangles

copy only what changed
3.3 — tracking dirty regions
~20 mincoming soon

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.

compositor.c — dirty trackingcopy
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();
}
The dirty-rect bug you WILL hit

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.

Lesson 3.4

3.4 Rounded rectangles and text

the primitives windows are made of
3.4 — rounded rects, fonts, the whole toolkit
~22 mincoming soon

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.

draw.c — rounded rectcopy
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);
    }
}
Never stroke a rounded fill with a square border

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.)

Module 03 checkpoint
  • 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_rect and a working text blitter
  • A rounded-border primitive that follows the corners
next module

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 →
or see all plans →