// lesson: a-framebuffer-of-your-own

A Framebuffer of Your Own

When the Expose event arrives, you need pixels to show. Modern toolkits route everything through the GPU; we're going to render in software โ€” computing every pixel on the CPU into a plain array โ€” and there's nothing apologetic about that choice. A text editor's frame is mostly blank space and a few thousand small glyphs; software rendering handled this fine on 25 MHz machines, it handles 4K displays fine today, and it gives us something priceless for this course: rendering that is testable โ€” a std::vector you can assert on โ€” and identical on every platform. (Sublime Text shipped for years on a software rasterizer; VS Code's terminal used one; every 2D toolkit keeps a software fallback.)

The pixel array

A framebuffer is a width, a height, and w * h 32-bit pixels in row-major order: pixel (x, y) lives at index y * w + x. Rows are contiguous; walking x is cache-friendly, walking y strides by w. The 32-bit value packs the color. We'll use the byte order X11, Windows, and macOS all natively use on little-endian machines: 0xAARRGGBB โ€” blue in the low byte. White is 0xFFFFFFFF, a pleasant editor-background off-white is 0xFFFAF8F0, black text is 0xFF000000.

class Framebuffer {
public:
    Framebuffer(int w, int h) : w_(w), h_(h), px_(size_t(w) * h, 0) {}
    int width() const { return w_; }
    int height() const { return h_; }
    uint32_t at(int x, int y) const { return px_[size_t(y) * w_ + x]; }
    // drawing ops go here
private:
    int w_, h_;
    std::vector<uint32_t> px_;
};

std::vector rather than new uint32_t[] is not a style nicety: the vector makes Framebuffer copyable, movable, and leak-proof with zero special members written โ€” rule of zero again. Resizing the window? Build a new Framebuffer and move-assign it.

Clipping is the whole game

Every drawing routine in a real renderer spends its first lines on the same question: which part of this shape actually lands inside the buffer? Draw a rect at x = โˆ’5 or a glyph half off the right edge, and the naive loop writes outside the array โ€” instant heap corruption (or in our graded builds, an AddressSanitizer abort, which is the same bug caught politely). The discipline:

  • Clip, then loop. Intersect the requested rect with the buffer's bounds rect before touching memory, then loop over the clipped rect only. One intersect call โ€” the one you wrote in lesson 2 โ€” replaces four per-pixel ifs and is dramatically faster.
  • Clip rects compose. "Only draw inside the text area" (don't paint over the scrollbar) is just one more intersect against a caller- supplied clip rect. Renderers keep a clip stack; ours keeps one rect, which is all an editor needs.

Scrolling without repainting

Here's the trick that made editors feel instant on slow hardware, and still cuts your paint cost 20ร— today: when the user scrolls one line, the new frame is 95% the old frame, shifted. So don't re-render โ€” blit: copy the surviving band to its new position within the same buffer, then repaint only the newly revealed strip. (Lesson 12 computes exactly which strip.)

Copying a rect within one buffer has a classic trap: if source and destination overlap, the copy direction matters. Copy downward (dest below source) iterating rows top-to-bottom and you'll read rows you've already overwritten โ€” the visual result is one source row smeared down the screen. The fix is the memmove insight: iterate rows bottom-up when moving down, top-up when moving up, and within a row let std::memmove (not memcpy โ€” overlap within the row is possible when shifting horizontally) handle the bytes.

Getting pixels onto glass

The framebuffer is portable; the last hop isn't. Each backend implements PlatformWindow::blit in a few lines:

  • X11: xcb_put_image ships the array to the server over the socket. That's a full copy per frame โ€” fine at editor scale โ€” and the reason the MIT-SHM extension exists: place the pixels in a POSIX shared-memory segment, and xcb_shm_put_image makes the server read them directly, no copy. Same array, faster hop; the SHM segment ID is precisely the kind of resource your UniqueHandle from lesson 2 wraps.
  • Win32: in the WM_PAINT handler, StretchDIBits(hdc, ...) pushes a device-independent bitmap โ€” your array with a small BITMAPINFO header saying "32-bit, top-down" โ€” onto the window's device context.
  • Cocoa: wrap the bytes in a CGDataProvider โ†’ CGImageCreate โ†’ draw it in drawRect:. (Or set it as the contents of a CALayer.)

Note what stayed portable: everything except one function per platform. That's the seam doing its job.

โ€บ Fill, Clipped

12 pts

Implement the workhorse: fill_rect, plus the variant that also respects a caller-supplied clip rect. The starter includes the lesson-2 geometry kit, solved โ€” build on intersect rather than re-deriving edge cases.

Requirements:

  • fill_rect(r, color): set every pixel inside r โˆฉ bounds to color. Rects partially or fully outside the buffer must not touch out-of-bounds memory (the tests run under AddressSanitizer โ€” any stray write fails the run).
  • fill_rect_clipped(r, clip, color): same, but only inside r โˆฉ clip โˆฉ bounds.
  • Empty input rects paint nothing.

Log in to submit a solution and earn points.

โ€บ The Scroll Blit

15 pts

Implement copy_rect: copy the pixels of src so that src's top-left corner lands at (dst_x, dst_y) โ€” within the same buffer, overlap allowed. This is the routine your scroll handler calls before repainting the revealed strip.

Semantics, in order:

  1. Clip src against the buffer bounds. If clipping moves src's origin (e.g. src.x was negative), the destination origin shifts by the same amount โ€” clipping trims the shape, it doesn't slide the image.
  2. Clip the destination rect (now src.w ร— src.h at the shifted origin) against the bounds, trimming the source correspondingly.
  3. Copy whatever survives. Overlapping regions must copy correctly: choose row order (and use std::memmove, or equivalent care, within rows) so no pixel is read after being overwritten.
  4. If nothing survives clipping, do nothing.

The starter's Framebuffer arrives with fill_rect working, and the tests paint every pixel a unique value first โ€” any smear, off-by-one, or wrong-direction copy shows up as a mismatched pixel.

Log in to submit a solution and earn points.