// 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
intersectcall โ the one you wrote in lesson 2 โ replaces four per-pixelifs and is dramatically faster. - Clip rects compose. "Only draw inside the text area" (don't paint
over the scrollbar) is just one more
intersectagainst 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_imageships 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, andxcb_shm_put_imagemakes the server read them directly, no copy. Same array, faster hop; the SHM segment ID is precisely the kind of resource yourUniqueHandlefrom lesson 2 wraps. - Win32: in the
WM_PAINThandler,StretchDIBits(hdc, ...)pushes a device-independent bitmap โ your array with a smallBITMAPINFOheader saying "32-bit, top-down" โ onto the window's device context. - Cocoa: wrap the bytes in a
CGDataProviderโCGImageCreateโ draw it indrawRect:. (Or set it as the contents of aCALayer.)
Note what stayed portable: everything except one function per platform. That's the seam doing its job.
โบ Fill, Clipped
12 ptsImplement 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 insider โฉ boundstocolor. 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 insider โฉ clip โฉ bounds.- Empty input rects paint nothing.
Log in to submit a solution and earn points.
โบ The Scroll Blit
15 ptsImplement 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:
- Clip
srcagainst the buffer bounds. If clipping movessrc's origin (e.g.src.xwas negative), the destination origin shifts by the same amount โ clipping trims the shape, it doesn't slide the image. - Clip the destination rect (now
src.w ร src.hat the shifted origin) against the bounds, trimming the source correspondingly. - 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. - 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.