Learn Zig Series (#156) - Framebuffer Basics

Published on HivePostify by @scipio · Tue Sep 01 2026

Learn Zig Series (#156) - Framebuffer Basics

What will I learn? - What a framebuffer actually is -- why every pixel you have ever seen on a screen ultimately lives in a flat, one-dimensional slice of memory, and how we make that slice pretend to be two-dimensional; - How to lay out pixels in row-major order, compute the index(x, y) mapping by hand, and why that one line of arithmetic is the foundation of all 2D graphics; - How to build a Framebuffer from scratch in Zig -- a pixel type, an owned buffer, bounds-checked setPixel/getPixel, a clear, and a clipped fillRect; - How Zig's type system turns "did I go out of bounds?" from an undefined-behaviour crash into a first-class ?Rgba you cannot ignore, and how comptime lets one framebuffer serve grayscale, RGBA, or any pixel format you like; - How to test a renderer even though it draws nothing to a screen -- by reading pixels back out and asserting on them, and by exporting to the dead-simple PPM image format; - Why row-major access order matters for cache behaviour, and how @memset clears a whole buffer far faster than a per-pixel loop; - How the same design shows up in C, Rust and Go, and what each language does about the bounds check that Zig makes explicit.

Requirements - A working modern computer running macOS, Windows or Ubuntu; - An installed Zig 0.14+ distribution (download from ziglang.org) -- the code here is written and tested against Zig 0.16; - Allocators (the arena and alloc) from episode 7, slices from episode 5, and structs from episode 6 -- we lean on all three today; - comptime from episode 9 (and the generic-container pattern from episode 14) for the one section where we make the framebuffer generic over its pixel type; - The ambition to learn Zig programming.

Difficulty - Advanced

Curriculum (of the Learn Zig Series): - [Zig Programming Tutorial - ep001 - Intro](https://hive.blog/programming/@scipio/zig-programming-tutoroial-ep001-intro) - [Learn Zig Series (#2) - Hello Zig, Variables and Types](https://hive.blog/hive-196387/@scipio/learn-zig-series-2-hello-zig-variables-and-types) - [Learn Zig Series (#3) - Functions and Control Flow](https://hive.blog/hive-196387/@scipio/learn-zig-series-3-functions-and-control-flow) - [Learn Zig Series (#4) - Error Handling (Zig's Best Feature)](https://hive.blog/hive-196387/@scipio/learn-zig-series-4-error-handling-zigs-best-feature) - [Learn Zig Series (#5) - Arrays, Slices, and Strings](https://hive.blog/hive-196387/@scipio/learn-zig-series-5-arrays-slices-and-strings) - [Learn Zig Series (#6) - Structs, Enums, and Tagged Unions](https://hive.blog/hive-196387/@scipio/learn-zig-series-6-structs-enums-and-tagged-unions) - [Learn Zig Series (#7) - Memory Management and Allocators](https://hive.blog/hive-196387/@scipio/learn-zig-series-7-memory-management-and-allocators) - [Learn Zig Series (#8) - Pointers and Memory Layout](https://hive.blog/hive-196387/@scipio/learn-zig-series-8-pointers-and-memory-layout) - [Learn Zig Series (#9) - Comptime (Zig's Superpower)](https://hive.blog/hive-196387/@scipio/learn-zig-series-9-comptime-zigs-superpower) - [Learn Zig Series (#10) - Project Structure, Modules, and File I/O](https://hive.blog/hive-196387/@scipio/learn-zig-series-10-project-structure-modules-and-file-io) - [Learn Zig Series (#11) - Mini Project: Building a Step Sequencer](https://hive.blog/hive-196387/@scipio/learn-zig-series-11-mini-project-building-a-step-sequencer) - [Learn Zig Series (#12) - Testing and Test-Driven Development](https://hive.blog/hive-196387/@scipio/learn-zig-series-12-testing-and-test-driven-development) - [Learn Zig Series (#13) - Interfaces via Type Erasure](https://hive.blog/hive-196387/@scipio/learn-zig-series-13-interfaces-via-type-erasure) - [Learn Zig Series (#14) - Generics with Comptime Parameters](https://hive.blog/hive-196387/@scipio/learn-zig-series-14-generics-with-comptime-parameters) - [Learn Zig Series (#15) - The Build System (build.zig)](https://hive.blog/hive-196387/@scipio/learn-zig-series-15-the-build-system-buildzig) - [Learn Zig Series (#16) - Sentinel-Terminated Types and C Strings](https://hive.blog/hive-196387/@scipio/learn-zig-series-16-sentinel-terminated-types-and-c-strings) - [Learn Zig Series (#17) - Packed Structs and Bit Manipulation](https://hive.blog/hive-196387/@scipio/learn-zig-series-17-packed-structs-and-bit-manipulation) - [Learn Zig Series (#18b) - Addendum: Async Returns in Zig 0.16](https://hive.blog/hive-196387/@scipio/learn-zig-series-18b-addendum-async-returns-in-zig-016) - [Learn Zig Series (#19) - SIMD with @Vector](https://hive.blog/hive-196387/@scipio/learn-zig-series-19-simd-with-vector) - [Learn Zig Series (#20) - Working with JSON](https://hive.blog/hive-196387/@scipio/learn-zig-series-20-working-with-json) - [Learn Zig Series (#21) - Networking and TCP Sockets](https://hive.blog/hive-196387/@scipio/learn-zig-series-21-networking-and-tcp-sockets) - [Learn Zig Series (#22) - Hash Maps and Data Structures](https://hive.blog/hive-196387/@scipio/learn-zig-series-22-hash-maps-and-data-structures) - [Learn Zig Series (#23) - Iterators and Lazy Evaluation](https://hive.blog/hive-196387/@scipio/learn-zig-series-23-iterators-and-lazy-evaluation) - [Learn Zig Series (#24) - Logging, Formatting, and Debug Output](https://hive.blog/hive-196387/@scipio/learn-zig-series-24-logging-formatting-and-debug-output) - [Learn Zig Series (#25) - Mini Project: HTTP Status Checker](https://hive.blog/hive-196387/@scipio/learn-zig-series-25-mini-project-http-status-checker) - [Learn Zig Series (#26) - Writing a Custom Allocator](https://hive.blog/hive-196387/@scipio/learn-zig-series-26-writing-a-custom-allocator) - [Learn Zig Series (#27) - C Interop: Calling C from Zig](https://hive.blog/hive-196387/@scipio/learn-zig-series-27-c-interop-calling-c-from-zig) - [Learn Zig Series (#28) - C Interop: Exposing Zig to C](https://hive.blog/hive-196387/@scipio/learn-zig-series-28-c-interop-exposing-zig-to-c) - [Learn Zig Series (#29) - Inline Assembly and Low-Level Control](https://hive.blog/hive-196387/@scipio/learn-zig-series-29-inline-assembly-and-low-level-control) - [Learn Zig Series (#30) - Thread Safety and Atomics](https://hive.blog/hive-196387/@scipio/learn-zig-series-30-thread-safety-and-atomics) - [Learn Zig Series (#31) - Memory-Mapped I/O and Files](https://hive.blog/hive-196387/@scipio/learn-zig-series-31-memory-mapped-io-and-files) - [Learn Zig Series (#32) - Compile-Time Reflection with @typeInfo](https://hive.blog/hive-196387/@scipio/learn-zig-series-32-compile-time-reflection-with-typeinfo) - [Learn Zig Series (#33) - Building a State Machine with Tagged Unions](https://hive.blog/hive-196387/@scipio/learn-zig-series-33-building-a-state-machine-with-tagged-unions) - [Learn Zig Series (#34) - Performance Profiling and Optimization](https://hive.blog/hive-196387/@scipio/learn-zig-series-34-performance-profiling-and-optimization) - [Learn Zig Series (#35) - Cross-Compilation and Target Triples](https://hive.blog/hive-196387/@scipio/learn-zig-series-35-cross-compilation-and-target-triples) - [Learn Zig Series (#36) - Mini Project: CLI Task Runner](https://hive.blog/hive-196387/@scipio/learn-zig-series-36-mini-project-cli-task-runner) - [Learn Zig Series (#37) - Markdown to HTML: Tokenizer and Lexer](https://hive.blog/hive-196387/@scipio/learn-zig-series-37-markdown-to-html-tokenizer-and-lexer) - [Learn Zig Series (#38) - Markdown to HTML: Parser and AST](https://hive.blog/hive-196387/@scipio/learn-zig-series-38-markdown-to-html-parser-and-ast) - [Learn Zig Series (#39) - Markdown to HTML: Renderer and CLI](https://hive.blog/hive-196387/@scipio/learn-zig-series-39-markdown-to-html-renderer-and-cli) - [Learn Zig Series (#40) - Key-Value Store: In-Memory Store](https://hive.blog/hive-196387/@scipio/learn-zig-series-40-key-value-store-in-memory-store) - [Learn Zig Series (#41) - Key-Value Store: Write-Ahead Log](https://hive.blog/hive-196387/@scipio/learn-zig-series-41-key-value-store-write-ahead-log) - [Learn Zig Series (#42) - Key-Value Store: TCP Server](https://hive.blog/hive-196387/@scipio/learn-zig-series-42-key-value-store-tcp-server) - [Learn Zig Series (#43) - Key-Value Store: Client Library and Benchmarks](https://hive.blog/hive-196387/@scipio/learn-zig-series-43-key-value-store-client-library-and-benchmarks) - [Learn Zig Series (#44) - Image Tool: Reading and Writing PPM/BMP](https://hive.blog/hive-196387/@scipio/learn-zig-series-44-image-tool-reading-and-writing-ppmbmp) - [Learn Zig Series (#45) - Image Tool: Pixel Operations](https://hive.blog/hive-196387/@scipio/learn-zig-series-45-image-tool-pixel-operations) - [Learn Zig Series (#46) - Image Tool: CLI Pipeline](https://hive.blog/hive-196387/@scipio/learn-zig-series-46-image-tool-cli-pipeline) - [Learn Zig Series (#47) - Build a Shell: Parsing Commands](https://hive.blog/hive-196387/@scipio/learn-zig-series-47-build-a-shell-parsing-commands) - [Learn Zig Series (#48) - Build a Shell: Process Spawning](https://hive.blog/hive-196387/@scipio/learn-zig-series-48-build-a-shell-process-spawning) - [Learn Zig Series (#49) - Build a Shell: Built-in Commands](https://hive.blog/hive-196387/@scipio/learn-zig-series-49-build-a-shell-built-in-commands) - [Learn Zig Series (#50) - Build a Shell: Job Control and Signals](https://hive.blog/hive-196387/@scipio/learn-zig-series-50-build-a-shell-job-control-and-signals) - [Learn Zig Series (#51) - HTTP Server: Accept Loop and Parsing](https://hive.blog/hive-196387/@scipio/learn-zig-series-51-http-server-accept-loop-and-parsing) - [Learn Zig Series (#52) - HTTP Server: Router and Responses](https://hive.blog/hive-196387/@scipio/learn-zig-series-52-http-server-router-and-responses) - [Learn Zig Series (#53) - HTTP Server: Static Files and MIME](https://hive.blog/hive-196387/@scipio/learn-zig-series-53-http-server-static-files-and-mime) - [Learn Zig Series (#54) - HTTP Server: Middleware and Logging](https://hive.blog/hive-196387/@scipio/learn-zig-series-54-http-server-middleware-and-logging) - [Learn Zig Series (#55) - ECS Game Engine: Architecture](https://hive.blog/hive-196387/@scipio/learn-zig-series-55-ecs-game-engine-architecture) - [Learn Zig Series (#56) - ECS Game Engine: Component Storage](https://hive.blog/hive-196387/@scipio/learn-zig-series-56-ecs-game-engine-component-storage) - [Learn Zig Series (#57) - ECS Game Engine: Systems and Queries](https://hive.blog/hive-196387/@scipio/learn-zig-series-57-ecs-game-engine-systems-and-queries) - [Learn Zig Series (#58) - ECS Game Engine: Terminal Rendering](https://hive.blog/hive-196387/@scipio/learn-zig-series-58-ecs-game-engine-terminal-rendering) - [Learn Zig Series (#59) - Assembler: Instruction Encoding](https://hive.blog/hive-196387/@scipio/learn-zig-series-59-assembler-instruction-encoding) - [Learn Zig Series (#60) - Assembler: Two-Pass Assembly](https://hive.blog/hive-196387/@scipio/learn-zig-series-60-assembler-two-pass-assembly) - [Learn Zig Series (#61) - Assembler: Disassembler and Binary Inspector](https://hive.blog/hive-196387/@scipio/learn-zig-series-61-assembler-disassembler-and-binary-inspector) - [Learn Zig Series (#62) - File Systems: Reading Directories and Metadata](https://hive.blog/hive-196387/@scipio/learn-zig-series-62-file-systems-reading-directories-and-metadata) - [Learn Zig Series (#63) - File Watching: Detecting Changes](https://hive.blog/hive-196387/@scipio/learn-zig-series-63-file-watching-detecting-changes) - [Learn Zig Series (#64) - Process Management: Fork, Exec, Wait](https://hive.blog/hive-196387/@scipio/learn-zig-series-64-process-management-fork-exec-wait) - [Learn Zig Series (#65) - Pipes and Inter-Process Communication](https://hive.blog/hive-196387/@scipio/learn-zig-series-65-pipes-and-inter-process-communication) - [Learn Zig Series (#66) - Shared Memory and Semaphores](https://hive.blog/hive-196387/@scipio/learn-zig-series-66-shared-memory-and-semaphores) - [Learn Zig Series (#67) - Signal Handling Deep Dive](https://hive.blog/hive-196387/@scipio/learn-zig-series-67-signal-handling-deep-dive) - [Learn Zig Series (#68) - Unix Domain Sockets](https://hive.blog/hive-196387/@scipio/learn-zig-series-68-unix-domain-sockets) - [Learn Zig Series (#69) - Daemonization: Background Services](https://hive.blog/hive-196387/@scipio/learn-zig-series-69-daemonization-background-services) - [Learn Zig Series (#70) - Timers and Scheduling](https://hive.blog/hive-196387/@scipio/learn-zig-series-70-timers-and-scheduling) - [Learn Zig Series (#71) - Resource Limits and Capabilities](https://hive.blog/hive-196387/@scipio/learn-zig-series-71-resource-limits-and-capabilities) - [Learn Zig Series (#72) - System Call Wrappers](https://hive.blog/hive-196387/@scipio/learn-zig-series-72-system-call-wrappers) - [Learn Zig Series (#73) - seccomp and Sandboxing](https://hive.blog/hive-196387/@scipio/learn-zig-series-73-seccomp-and-sandboxing) - [Learn Zig Series (#74) - ptrace: Process Tracing](https://hive.blog/hive-196387/@scipio/learn-zig-series-74-ptrace-process-tracing) - [Learn Zig Series (#75) - Reading Kernel State from /proc and /sys](https://hive.blog/hive-196387/@scipio/learn-zig-series-75-reading-kernel-state-from-proc-and-sys) - [Learn Zig Series (#76) - Mini Project: Process Monitor](https://hive.blog/hive-196387/@scipio/learn-zig-series-76-mini-project-process-monitor) - [Learn Zig Series (#77) - Mini Project: File Sync Tool - Part 1](https://hive.blog/hive-196387/@scipio/learn-zig-series-77-mini-project-file-sync-tool-part-1) - [Learn Zig Series (#78) - Mini Project: File Sync Tool - Part 2: Delta Transfer](https://hive.blog/hive-196387/@scipio/learn-zig-series-78-mini-project-file-sync-tool-part-2-delta-transfer) - [Learn Zig Series (#79) - Mini Project: File Sync Tool - Part 3: Network Protocol](https://hive.blog/hive-196387/@scipio/learn-zig-series-79-mini-project-file-sync-tool-part-3-network-protocol) - [Learn Zig Series (#80) - Mini Project: File Sync Tool - Part 4: Polish](https://hive.blog/hive-196387/@scipio/learn-zig-series-80-mini-project-file-sync-tool-part-4-polish) - [Learn Zig Series (#81) - UDP Sockets and Datagrams](https://hive.blog/hive-196387/@scipio/learn-zig-series-81-udp-sockets-and-datagrams) - [Learn Zig Series (#82) - DNS Resolver from Scratch](https://hive.blog/hive-196387/@scipio/learn-zig-series-82-dns-resolver-from-scratch) - [Learn Zig Series (#83) - DNS Server Implementation](https://hive.blog/hive-196387/@scipio/learn-zig-series-83-dns-server-implementation) - [Learn Zig Series (#84) - HTTP/1.1 Deep Dive](https://hive.blog/hive-196387/@scipio/learn-zig-series-84-http11-deep-dive) - [Learn Zig Series (#85) - HTTP/2 Frames and Streams](https://hive.blog/hive-196387/@scipio/learn-zig-series-85-http2-frames-and-streams) - [Learn Zig Series (#86) - TLS via C Interop](https://hive.blog/hive-196387/@scipio/learn-zig-series-86-tls-via-c-interop) - [Learn Zig Series (#87) - WebSocket Protocol](https://hive.blog/hive-196387/@scipio/learn-zig-series-87-websocket-protocol) - [Learn Zig Series (#88) - WebSocket Server](https://hive.blog/hive-196387/@scipio/learn-zig-series-88-websocket-server) - [Learn Zig Series (#89) - MQTT Messaging Protocol](https://hive.blog/hive-196387/@scipio/learn-zig-series-89-mqtt-messaging-protocol) - [Learn Zig Series (#90) - Protocol Buffers Serialization](https://hive.blog/hive-196387/@scipio/learn-zig-series-90-protocol-buffers-serialization) - [Learn Zig Series (#91) - MessagePack Format](https://hive.blog/hive-196387/@scipio/learn-zig-series-91-messagepack-format) - [Learn Zig Series (#92) - gRPC Service in Zig](https://hive.blog/hive-196387/@scipio/learn-zig-series-92-grpc-service-in-zig) - [Learn Zig Series (#93) - SOCKS5 Proxy](https://hive.blog/hive-196387/@scipio/learn-zig-series-93-socks5-proxy) - [Learn Zig Series (#94) - NAT Traversal and Hole Punching](https://hive.blog/hive-196387/@scipio/learn-zig-series-94-nat-traversal-and-hole-punching) - [Learn Zig Series (#95) - Mini Project: Chat Server - Protocol Design](https://hive.blog/hive-196387/@scipio/learn-zig-series-95-mini-project-chat-server-protocol-design) - [Learn Zig Series (#96) - Mini Project: Chat Server - Server Core](https://hive.blog/hive-196387/@scipio/learn-zig-series-96-mini-project-chat-server-server-core) - [Learn Zig Series (#97) - Mini Project: Chat Server - Client TUI](https://hive.blog/hive-196387/@scipio/learn-zig-series-97-mini-project-chat-server-client-tui) - [Learn Zig Series (#98) - Mini Project: Chat Server - Rooms and History](https://hive.blog/hive-196387/@scipio/learn-zig-series-98-mini-project-chat-server-rooms-and-history) - [Learn Zig Series (#99) - Mini Project: DNS-over-HTTPS Proxy](https://hive.blog/hive-196387/@scipio/learn-zig-series-99-mini-project-dns-over-https-proxy) - [Learn Zig Series (#100) - Mini Project: Port Scanner](https://hive.blog/hive-196387/@scipio/learn-zig-series-100-mini-project-port-scanner) - [Learn Zig Series (#101) - Mini Project: HTTP Load Tester - Part 1](https://hive.blog/hive-196387/@scipio/learn-zig-series-101-mini-project-http-load-tester-part-1) - [Learn Zig Series (#102) - Mini Project: HTTP Load Tester - Part 2](https://hive.blog/hive-196387/@scipio/learn-zig-series-102-mini-project-http-load-tester-part-2) - [Learn Zig Series (#103) - Mini Project: Reverse Proxy - Routing](https://hive.blog/hive-196387/@scipio/learn-zig-series-103-mini-project-reverse-proxy-routing) - [Learn Zig Series (#104) - Mini Project: Reverse Proxy - Load Balancing](https://hive.blog/hive-196387/@scipio/learn-zig-series-104-mini-project-reverse-proxy-load-balancing) - [Learn Zig Series (#105) - Mini Project: Reverse Proxy - Health Checks](https://hive.blog/hive-196387/@scipio/learn-zig-series-105-mini-project-reverse-proxy-health-checks) - [Learn Zig Series (#106) - Linked Lists: Singly and Doubly](https://hive.blog/hive-196387/@scipio/learn-zig-series-106-linked-lists-singly-and-doubly) - [Learn Zig Series (#107) - Skip Lists](https://hive.blog/hive-196387/@scipio/learn-zig-series-107-skip-lists) - [Learn Zig Series (#108) - B-Trees](https://hive.blog/hive-196387/@scipio/learn-zig-series-108-b-trees) - [Learn Zig Series (#109) - Red-Black Trees](https://hive.blog/hive-196387/@scipio/learn-zig-series-109-red-black-trees) - [Learn Zig Series (#110) - Tries: Prefix Trees](https://hive.blog/hive-196387/@scipio/learn-zig-series-110-tries-prefix-trees) - [Learn Zig Series (#111) - Bloom Filters](https://hive.blog/hive-196387/@scipio/learn-zig-series-111-bloom-filters) - [Learn Zig Series (#112) - Cuckoo Filters](https://hive.blog/hive-196387/@scipio/learn-zig-series-112-cuckoo-filters) - [Learn Zig Series (#113) - Ring Buffers: Lock-Free](https://hive.blog/hive-196387/@scipio/learn-zig-series-113-ring-buffers-lock-free) - [Learn Zig Series (#114) - Memory Pools](https://hive.blog/hive-196387/@scipio/learn-zig-series-114-memory-pools) - [Learn Zig Series (#115) - Slab Allocators](https://hive.blog/hive-196387/@scipio/learn-zig-series-115-slab-allocators) - [Learn Zig Series (#116) - Sorting Algorithms in Zig](https://hive.blog/hive-196387/@scipio/learn-zig-series-116-sorting-algorithms-in-zig) - [Learn Zig Series (#117) - Binary Search Variations](https://hive.blog/hive-196387/@scipio/learn-zig-series-117-binary-search-variations) - [Learn Zig Series (#118) - Graph Representation](https://hive.blog/hive-196387/@scipio/learn-zig-series-118-graph-representation) - [Learn Zig Series (#119) - BFS and DFS](https://hive.blog/hive-196387/@scipio/learn-zig-series-119-bfs-and-dfs) - [Learn Zig Series (#120) - Dijkstra and A](https://hive.blog/hive-196387/@scipio/learn-zig-series-120-dijkstra-and-a) - [Learn Zig Series (#121) - Topological Sort](https://hive.blog/hive-196387/@scipio/learn-zig-series-121-topological-sort) - [Learn Zig Series (#122) - Union-Find](https://hive.blog/hive-196387/@scipio/learn-zig-series-122-union-find) - [Learn Zig Series (#123) - LRU Cache](https://hive.blog/hive-196387/@scipio/learn-zig-series-123-lru-cache) - [Learn Zig Series (#124) - Consistent Hashing](https://hive.blog/hive-196387/@scipio/learn-zig-series-124-consistent-hashing) - [Learn Zig Series (#125) - Mini Project: Search Engine - Inverted Index](https://hive.blog/hive-196387/@scipio/learn-zig-series-125-mini-project-search-engine-inverted-index) - [Learn Zig Series (#126) - Mini Project: Search Engine - TF-IDF](https://hive.blog/hive-196387/@scipio/learn-zig-series-126-mini-project-search-engine-tf-idf) - [Learn Zig Series (#127) - Mini Project: Search Engine - Query Parser](https://hive.blog/hive-196387/@scipio/learn-zig-series-127-mini-project-search-engine-query-parser) - [Learn Zig Series (#128) - Mini Project: Database Engine - Page Storage](https://hive.blog/hive-196387/@scipio/learn-zig-series-128-mini-project-database-engine-page-storage) - [Learn Zig Series (#129) - Mini Project: Database Engine - B-Tree Index](https://hive.blog/hive-196387/@scipio/learn-zig-series-129-mini-project-database-engine-b-tree-index) - [Learn Zig Series (#130) - Mini Project: Database Engine - SQL Parser](https://hive.blog/hive-196387/@scipio/learn-zig-series-130-mini-project-database-engine-sql-parser) - [Learn Zig Series (#131) - Lexing a Simple Language](https://hive.blog/hive-196387/@scipio/learn-zig-series-131-lexing-a-simple-language) - [Learn Zig Series (#132) - Recursive Descent Parsing](https://hive.blog/hive-196387/@scipio/learn-zig-series-132-recursive-descent-parsing) - [Learn Zig Series (#133) - AST Design and Traversal](https://hive.blog/hive-196387/@scipio/learn-zig-series-133-ast-design-and-traversal) - [Learn Zig Series (#134) - Type Checking](https://hive.blog/hive-196387/@scipio/learn-zig-series-134-type-checking) - [Learn Zig Series (#135) - Bytecode Design](https://hive.blog/hive-196387/@scipio/learn-zig-series-135-bytecode-design) - [Learn Zig Series (#136) - Stack-Based Virtual Machine](https://hive.blog/hive-196387/@scipio/learn-zig-series-136-stack-based-virtual-machine) - [Learn Zig Series (#137) - Closures and Upvalues](https://hive.blog/hive-196387/@scipio/learn-zig-series-137-closures-and-upvalues) - [Learn Zig Series (#138) - Garbage Collection: Mark and Sweep](https://hive.blog/hive-196387/@scipio/learn-zig-series-138-garbage-collection-mark-and-sweep) - [Learn Zig Series (#139) - Garbage Collection: Generational](https://hive.blog/hive-196387/@scipio/learn-zig-series-139-garbage-collection-generational) - [Learn Zig Series (#140) - JIT Compilation Basics](https://hive.blog/hive-196387/@scipio/learn-zig-series-140-jit-compilation-basics) - [Learn Zig Series (#141) - Regex: Thompson NFA](https://hive.blog/hive-196387/@scipio/learn-zig-series-141-regex-thompson-nfa) - [Learn Zig Series (#142) - Regex: NFA to DFA](https://hive.blog/hive-196387/@scipio/learn-zig-series-142-regex-nfa-to-dfa) - [Learn Zig Series (#143) - Regex: Matching Engine](https://hive.blog/hive-196387/@scipio/learn-zig-series-143-regex-matching-engine) - [Learn Zig Series (#144) - Code Generation: AST to Machine Code](https://hive.blog/hive-196387/@scipio/learn-zig-series-144-code-generation-ast-to-machine-code) - [Learn Zig Series (#145) - Register Allocation](https://hive.blog/hive-196387/@scipio/learn-zig-series-145-register-allocation) - [Learn Zig Series (#146) - Mini Project: Calculator - Lexer/Parser](https://hive.blog/hive-196387/@scipio/learn-zig-series-146-mini-project-calculator-lexerparser) - [Learn Zig Series (#147) - Mini Project: Calculator - Interpreter](https://hive.blog/hive-196387/@scipio/learn-zig-series-147-mini-project-calculator-interpreter) - [Learn Zig Series (#148) - Mini Project: Calculator - Bytecode Compiler](https://hive.blog/hive-196387/@scipio/learn-zig-series-148-mini-project-calculator-bytecode-compiler) - [Learn Zig Series (#149) - Mini Project: Calculator - VM with Debugger](https://hive.blog/hive-196387/@scipio/learn-zig-series-149-mini-project-calculator-vm-with-debugger) - [Learn Zig Series (#150) - Mini Project: Lisp - Reader](https://hive.blog/hive-196387/@scipio/learn-zig-series-150-mini-project-lisp-reader) - [Learn Zig Series (#151) - Mini Project: Lisp - Evaluator](https://hive.blog/hive-196387/@scipio/learn-zig-series-151-mini-project-lisp-evaluator) - [Learn Zig Series (#152) - Mini Project: Lisp - Special Forms and Macros](https://hive.blog/hive-196387/@scipio/learn-zig-series-152-mini-project-lisp-special-forms-and-macros) - [Learn Zig Series (#153) - Mini Project: Lisp - Standard Library](https://hive.blog/hive-196387/@scipio/learn-zig-series-153-mini-project-lisp-standard-library) - [Learn Zig Series (#154) - Mini Project: Regex Engine - NFA](https://hive.blog/hive-196387/@scipio/learn-zig-series-154-mini-project-regex-engine-nfa) - [Learn Zig Series (#155) - Mini Project: Regex Engine - Matching](https://hive.blog/hive-196387/@scipio/learn-zig-series-155-mini-project-regex-engine-matching) - [Learn Zig Series (#156) - Framebuffer Basics](https://hive.blog/hive-196387/@scipio/learn-zig-series-156-framebuffer-basics) (this post)

Learn Zig Series (#156) - Framebuffer Basics

For the last two episodes we lived inside a regex engine -- parsing, wiring an NFA, simulating it in linear time. Today we make a hard turn and start something new: we are going to draw. Not with a library, not with a game framework, not with SDL or raylib hiding the details -- we are going to build the thing all of those sit on top of, the humble framebuffer, from a bare slice of bytes. Everything a screen shows you -- this text, that photo, the cursor blinking at you -- is, at the very bottom, a rectangle of numbers in memory that something copies out to a display. Understand that rectangle and you understand computer graphics from the ground up. Here we go!

The core concept: a 1D slice pretending to be 2D

A screen is two-dimensional. Memory is one-dimensional -- a slice is just ptr and len, a straight line of bytes with no notion of "up" or "left". So the first and most important idea in all of graphics is a convention for cramming a 2D grid into a 1D line. The overwhelmingly common one is row-major order: you store row 0 left-to-right, then row 1 right after it, then row 2, and so on. The pixel at column x, row y lives at a single computable offset.

That offset is the one formula you must burn into memory, because literally every drawing routine we write from here on is built on it:

zig const std = @import("std");

test "row-major index math" { // A 4-wide, 3-tall image is a flat run of 12 pixels. // Row 0 occupies slots 0..3, row 1 slots 4..7, row 2 slots 8..11. // The pixel at (x, y) therefore lives at: y width + x const width: usize = 4; const x: usize = 2; const y: usize = 1; const slot = y width + x; // second row, third column try std.testing.expectEqual(@as(usize, 6), slot); }

The width in that formula is what graphics people call the stride (sometimes "pitch") -- the number of pixels you skip to move straight down one row. In our simple case the stride equals the width, but hold on to the distinction: real hardware framebuffers often pad each row up to a nice alignment, so the stride is bigger than the visible width, and mixing them up is a classic bug that makes your image lean diagonally like a drunk. We will keep stride equal to width for now, but I am naming it so the idea is already in your head.

Having said that, let us decide what a single pixel is before we allocate a grid of them.

A pixel is just a small struct

A colour, in the model your monitor speaks, is three intensities -- red, green, blue -- plus optionally an alpha channel that says how opaque the pixel is (we will not blend with it today, but every serious framebuffer carries it, so we will too). Each channel is one byte, 0..255. That is exactly a four-byte value, and Zig lets us say so precisely:

zig pub const Rgba = packed struct { r: u8, g: u8, b: u8, a: u8 = 255, // default: fully opaque };

I made it a packed struct deliberately (recall episode 17). A packed struct has a guaranteed, gap-free memory layout -- these four bytes sit back to back with no padding, so an Rgba is bit-for-bit a u32, and a []Rgba is bit-for-bit the raw pixel bytes a real display or an image file expects. That is the whole point of a framebuffer: the in-memory representation is the wire format. The a: u8 = 255 default is a small ergonomic gift -- you can write .{ .r = 255, .g = 0, .b = 0 } for opaque red and not think about alpha until the day you need to.

Building the Framebuffer

Now the container. A framebuffer owns three things: the slice of pixels, its dimensions, and the allocator it was born from (so it can give the memory back). This is the same ownership discipline we have used since episode 7 -- whoever allocates, deallocates, and the type carries what it needs to clean up after itself:

zig pub const Framebuffer = struct { pixels: []Rgba, width: usize, height: usize, allocator: std.mem.Allocator,

pub fn init(allocator: std.mem.Allocator, width: usize, height: usize) !Framebuffer { const pixels = try allocator.alloc(Rgba, width height); return .{ .pixels = pixels, .width = width, .height = height, .allocator = allocator, }; }

pub fn deinit(self: Framebuffer) void { self.allocator.free(self.pixels); self. = undefined; // poison the struct -- use-after-free becomes an obvious crash }

fn index(self: Framebuffer, x: usize, y: usize) usize { return y self.width + x; }

pub fn inBounds(self: Framebuffer, x: usize, y: usize) bool { return x = self.width or y0 >= self.height) return; // wholly off-canvas const xend = @min(x0 + w, self.width); // clip right edge const yend = @min(y0 + h, self.height); // clip bottom edge var y = y0; while (y = self.width or y >= self.height) return null; return self.pixels[y self.width + x]; }

pub fn set(self: Self, x: usize, y: usize, p: Pixel) void { if (x >= self.width or y >= self.height) return; self.pixels[y self.width + x] = p; }

pub fn clear(self: Self, p: Pixel) void { @memset(self.pixels, p); } }; }

Image(Rgba) gives you back essentially the colour framebuffer we just wrote; Image(u8) gives you a grayscale one; Image(f32) gives you a depth buffer. And here is the beautiful part -- there is zero runtime cost for this abstraction. Image(u8) and Image(Rgba) are two entirely separate, fully specialised types generated at compile time, each with its own optimally-laid-out @memset. This is not the "boxed, virtual-dispatch, one-size-fits-all" generics of a managed language; it is monomorphisation with no apology, the same machinery C++ templates and Rust generics use, but spelled out in plain Zig you can read. A part from that, because it is all comptime, an illegal pixel type fails to compile in stead of blowing up at runtime.

Testing a renderer that draws nothing you can see

Here is the awkward truth about graphics code: it produces pictures, and pictures are exactly the kind of output that is miserable to assert on in a unit test. But our framebuffer is not a black box -- it is a slice of numbers we can read straight back. So we test drawing the same way we tested the regex matcher last episode: perform an operation, then read the state back out and assert on it. No screen required.

zig test "setPixel and getPixel round-trip a colour" { var fb = try Framebuffer.init(std.testing.allocator, 4, 3); defer fb.deinit(); fb.clear(.{ .r = 0, .g = 0, .b = 0 }); fb.setPixel(1, 2, .{ .r = 255, .g = 128, .b = 0 }); // opaque orange const got = fb.getPixel(1, 2).?; try std.testing.expectEqual(@as(u8, 255), got.r); try std.testing.expectEqual(@as(u8, 128), got.g); try std.testing.expectEqual(@as(u8, 0), got.b); try std.testing.expectEqual(@as(u8, 255), got.a); // the default alpha came through }

That test pins down the round-trip and the alpha default in one go. Now the boundary behaviour -- the part that would be undefined behaviour in C and is instead a boring, testable no-op here:

zig test "out-of-bounds writes clip and reads return null" { var fb = try Framebuffer.init(std.testing.allocator, 2, 2); defer fb.deinit(); fb.clear(.{ .r = 10, .g = 20, .b = 30 }); fb.setPixel(99, 99, .{ .r = 255, .g = 255, .b = 255 }); // way off canvas: no crash, no effect try std.testing.expect(fb.getPixel(99, 99) == null); try std.testing.expectEqual(@as(u8, 10), fb.getPixel(0, 0).?.r); // untouched }

test "fillRect clips to the framebuffer edge" { var fb = try Framebuffer.init(std.testing.allocator, 4, 4); defer fb.deinit(); fb.clear(.{ .r = 0, .g = 0, .b = 0 }); const white = Rgba{ .r = 255, .g = 255, .b = 255 }; fb.fillRect(2, 2, 10, 10, white); // spills far past the bottom-right corner try std.testing.expectEqual(@as(u8, 255), fb.getPixel(3, 3).?.r); // inside: painted try std.testing.expectEqual(@as(u8, 0), fb.getPixel(1, 1).?.r); // outside the rect: untouched }

The fillRect test is the one I care about most, because it exercises the clipping logic on a rectangle that deliberately runs off the edge -- the exact case that a careless index computation would turn into a buffer overflow. If our @min clamps are wrong, this test either crashes or paints a pixel it should not, and zig test tells us immediately. And the generic Image deserves a test too, proving one definition really does serve two pixel formats:

zig test "generic Image serves both grayscale and rgba" { var gray = try Image(u8).init(std.testing.allocator, 3, 3); defer gray.deinit(); gray.clear(0); gray.set(1, 1, 200); try std.testing.expectEqual(@as(?u8, 200), gray.at(1, 1));

var color = try Image(Rgba).init(std.testing.allocator, 2, 2); defer color.deinit(); color.clear(.{ .r = 0, .g = 0, .b = 0 }); color.set(0, 0, .{ .r = 5, .g = 6, .b = 7 }); try std.testing.expectEqual(@as(u8, 5), color.at(0, 0).?.r); }

Getting the picture out: the PPM format

Reading pixels back in a test is satiesfying, but at some point you want to look at what you drew. The friendliest possible target is PPM (Portable Pixmap), a format so simple you can write an encoder in a dozen lines: a tiny text header -- the magic bytes P6, the width and height, the max channel value 255 -- followed by the raw RGB bytes, three per pixel. No compression, no chunks, no checksums. Every image viewer worth the name opens it, and zig test can verify the bytes exactly:

zig pub fn toPpm(self: Framebuffer, allocator: std.mem.Allocator) ![]u8 { const header = try std.fmt.allocPrint(allocator, "P6\n{d} {d}\n255\n", .{ self.width, self.height }); defer allocator.free(header);

var out = try allocator.alloc(u8, header.len + self.pixels.len 3); @memcpy(out[0..header.len], header);

var i: usize = header.len; for (self.pixels) |p| { out[i] = p.r; out[i + 1] = p.g; out[i + 2] = p.b; // note: PPM has no alpha channel, so we drop it i += 3; } return out; }

zig test "toPpm emits a correct P6 header and body length" { var fb = try Framebuffer.init(std.testing.allocator, 2, 1); defer fb.deinit(); fb.clear(.{ .r = 1, .g = 2, .b = 3 }); const ppm = try fb.toPpm(std.testing.allocator); defer std.testing.allocator.free(ppm); try std.testing.expect(std.mem.startsWith(u8, ppm, "P6\n2 1\n255\n")); // header + exactly 3 bytes per pixel (2 pixels -> 6 body bytes) try std.testing.expectEqual("P6\n2 1\n255\n".len + 6, ppm.len); }

Once you have these bytes, writing them to output.ppm with the file I/O from episode 10 is a one-liner, and you can open the result and see your rectangle. I have deliberately returned the bytes rather than writing the file inside the function -- that keeps the encoder pure and trivially testable, and it lets the caller decide whether the pixels go to disk, down a socket, or into another buffer. Separating "compute the bytes" from "perform the I/O" is a habit that pays off far beyond graphics.

Performance: draw with the grain of memory

I keep saying "respect the layout", so let me make it concrete with the single most important performance idea for framebuffers: traverse in row-major order, the same order the pixels are stored. Consider two ways to clear a buffer to one colour. The obvious-looking nested loop, and the one-shot builtin:

zig // SLOW-ish: correct, but a per-pixel bounds check and a column-outer loop pub fn clearSlow(self: Framebuffer, color: Rgba) void { var x: usize = 0; while (x (or a [u8] for the raw bytes) and gives you two doors: indexing with buf[y w + x] panics on overflow, exactly like Zig's default safe build, while buf.get(y w + x) returns an Option -- the direct analogue of our ?Rgba. The image crate wraps all this in an ImageBuffer generic over the pixel type P, which is precisely the Image(comptime Pixel: type) move we just made, only with traits doing what our comptime does. Go leans on its standard library: image.RGBA holds a Pix []uint8 byte slice plus a Stride field -- and there is that stride again, first-class in the standard type, because Go's designers knew row padding matters. Go's slice accesses are bounds-checked at runtime and panic out of range, so it lands in the same safety neighbourhood as Zig's safe build, trading a little speed for not corrupting memory.

The through-line: everyone agrees a framebuffer is a flat buffer plus a stride and a pixel format. Where they differ is what happens when you index out of bounds -- silent corruption (C), a panic (Rust/Go by default), or Zig's choice of a checked crash in safe builds that you can drop to raw speed in ReleaseFast once you have proven your indices with tests like the ones above. Zig hands you the C-level buffer and the high-level safety in the same language, and lets you pick per build.

Exercises

1. Horizontal and vertical lines. Add hLine(self, x0, x1, y, color) and vLine(self, x, y0, y1, color) methods that draw straight runs, clipping to the canvas. The horizontal one should be a single @memset per call (contiguous memory), the vertical one a while loop stepping by width. Write tests that draw a line running off each edge and assert only the on-canvas pixels changed.

Tags: #stem#stemsocial#steemstem#zig#programming

View full post on HivePostify →

Join HivePostify — Pakistan's First Web3 Platform →