Rendering like it's 1996 - Baby's first pixel

December 02, 2022

There's absolutely no chance we'll get to this level of quality.

In 1996, I was a teen without a gaming console. While my friends enjoyed their Crash Bandicoots, Tekens, and Turoks, I had a beige 486 DX 2 with a turbo button, 16Mb of RAM, a 256Mb hard disk, and a 2x CD-ROM drive running DOS. And then I got a copy of Quake. Did it run great? No. But it did run! And to my young eyes, it was the most beautiful thing I've ever seen on my computer screen. Ok, the most beautiful brown thing.

3D accelerator graphics cards were in their infancy. Most DOS PC games around that time would render their glorious pixels via the CPU to a dedicated area in RAM, e.g. starting at segment address 0xa000. The (pretty dumb) graphics card would then read and display the contents of that memory area on your bulky CRT. This is known as software rendering or software rasterization.

I did dabble in some graphics programming back then. I even managed to create a Wolfenstein style first person shooter in QBasic with some assembly before the end of the century.

Actually not a ray casting engine, but a polygonal 3D engine with terrible affine texture mapping.

But I never really dove into the depths of contemporary graphics technology. And while my subsequent professional career featured plenty of graphics programming, it was mostly the GPU accelerated kind, not the "worry about each cycle in your inner loops" software rasterizer kind of type.


I want to explore the ins and outs of software rasterization, starting from first principles, i.e. getting a pixel on screen. From there, I want to delve into topics like simple demo effects, primitive rasterization, ray casting, voxel terrain, maybe even Quake-style 3D rendering, and whatever else comes to mind.

Each blog post on a topic will lay out the theory the way I understand it in hopefully simple terms, discuss a naive practical implementation, and finally investigate ways to optimize the implementation until it is reasonably fast.

The end product(s) should work on Windows, Linux, macOS, and common browsers. Ideally, a little software rasterizer library and demos will fall out at the end, that can serve both as an example implementation of common techniques, or as the basis for other demos or games with DOS game aesthetics.

You'll be able to follow along both here, and by playing with the code on GitHub. For each blog post, there will be one tagged commit in the main branch you can check out. In addition to the render-y bits, I'll also demonstrate how I set up a cross-platform C project and show you how I structure, build, and debug C code in such a project. I love seeing and learning from other people's workflows. Maybe that's true for you too.

What I do not want to do is dabble in things like assembly or SIMD optimizations. While that can be fun too, it is unlikely to be necessary on today's hardware, given that I'll target common DOS resolutions like 320x240, or 640x480. I might however inspect and discuss the compiler's assembly output to identify areas that can be improved performance wise in the higher level code.

Tools of the trade

The weapon of choice will be C99 for aesthetic and practical reasons. I want all the code produced throughout this series to compile anywhere. It should also be easy to re-use the code in other languages through an FFI. C99 is a good choice for both objectives.

In terms of ompilers, I'll be using Clang with MinGW headers and standard libraries on Windows, Clang through Xcode on macOS, and GCC on Linux. Why Clang on Windows? Because Visual Studio is a multi-gigabyte download, and setting up builds for it is a terrible experience. Clang also generates better code.

I'll use CMake as the meta build tool, not because I love it, but because my favorite C/C++ IDE CLion has first class support for it. Other development environments understand CMake as well these days, including Visual Studio if that's your kink. For actually executing the builds, I'll use Ninja, which is wicked fast, especially compared to MSBuild and consorts.

The pixels we'll generate need to be thrown up on the display somehow. On Windows, Linux, and macOS we'll use MiniFB. In a few lines of code, we can open a window, process keyboard and mouse input, and give it a bunch of pixels to draw to the window. It can even upscale our low resolution output if needed. Since MiniFB does not have browser support, I've written a web backend myself and submitted it as a pull request to the upstream repo. In the meantime, we'll use my MiniFB fork, which has web support baked in.

To get the code running in the browser, we'll use Emscripten to compile the C code to WASM and a small .js file, which loads the .wasm file and exposes our C functions to JavaScript.

In terms of IDE, you are free to use whatever you want. You'll most likely want something that can ingest CMake builds. For this series, I choose VS Code, not because I love it, but because it's free. The project contains a bunch of VS Code specific settings that make working on the project super simple for all supported platforms.

Getting the source code and tools

That's a lot of tools! I've tried to make it as simple for you to follow along as possible. Here's what you need to install:

Once you've installed the above, clone the repository (on Windows, use Git Bash, which comes with Git for Windows):

git clone
cd r96

Next, checkout the tag for the blog post you want to follow along with, execute the tools/ script:

git checkout 01-babys-first-pixel

The script will download all remaining tools that are needed, like CMake, Ninja, Clang for Windows, Python, a small static file server, Emscripten, and Visual Studio Code extensions needed for C/C++ development. See the for details.

Note: we may add new tools in future blog posts. After checking out a tag for a blog post, make sure to run tools/ again.

The r96 project

These are the goals for the project scaffold:

Let's see how I tried to achieve the above. Open your clone of the r96 Git repository in VS Code and have a look what's inside.

Note: The first time you open the project in VS Code, you'll be asked to select a CMake configure preset.

Note: the first time you open a source file in VS Code, you will be asked if you want to install clangd. Click Yes.

File structure

Let's start in the root folder.

The .gitignore, LICENSE, and files are self-explanatory.

The CMakeLists.txt and CMakePresets.json define our build. We'll look into these in a later section.

The .clang-format file stores the formatting settings used to format the code via, you guessed it, clang-format. The VS Code C/C++ extension uses the settings in that file whenever you format a C/C++ source file. The file can also be used to format the entire code base from the command line.

The src/ folder contains our code. Re-usable code goes into src/r96/. Demo apps go into the root of the src/ folder. There are two demo apps so far called 00_basic_window.c and 01_drawing_a_pixel.c. Any demo apps we write in subsequent blog posts will also go into src/ and start with a sequential number, so we immediately see in which order they were written.

The src/web/ folder may be weird, even scary to seasoned C veterans. But we need it to run our demo apps on the web. A small price to pay. It contains one .html file per demo app. The purpose of that file is to:

For any demo app we write in the future, we'll add a source file to the src/ folder, and a corresponding .html file to the src/web/ folder.

The src/web/index.html file is just a plain listing linking to all the .html files of our demo apps. The src/web/r96.css file is a CSS style sheet used to make the elements in the demo app .html files a little prettier.

The .vscode/ folder contains settings and launch configurations so working on the project is a nice experience in VS Code.

Finally, the tools/ folder contains scripts to download the necessary tools as well as configuration files for a few of those tools. When executing the tools/ script, some of the tools actually get installed in the tools/ folder so they don't clog up your system. The folder also contains scripts and batch files used by the launch configurations to do their work.

The details of the .vscode and tools folder are all gory and duct tape-y. You can have a look if you must. For the remained of the series, their content doesn't matter much. Just know that they are setup in a way to make our lives easy.


The first time you open the project in VS Code, you're asked to select a configure preset.

A configure preset defines for what platform the code should be build and with what compiler and build flags that should happen. The presets are defined in CMakePresets.json.

For each platform the r96 project supports, there is a corresponding debug and release configure preset. To start, we'll select Desktop debug. You can also select the configure preset in the status bar at the bottom of VS Code.

To build the project for the selected platform and build type (debug or release), click the Build button in the status bar.

Alternatively, you can open the VS Code command palette (CTRL+SHIFT+P or CMD+SHIFT+P on macOS), type CMake: Build, and hit enter.

In both cases, the CMake Tools extension, which was installed as part of tools/, will configure the CMake build if necessary, then incrementally build the libraries and executables defined in CMakeLists.txt.

The resulting build output consisting of executables and assets can be found in build/<os>-<build-type>. E.g. for Desktop debug, the build output will be located in build/windows-debug, build/linux-debug, or build/macos-debug depending on what operating system you are on. For Web release the output will be in build/web-output, and so on.

Note: To learn more about how to use VS Code CMake integration, check out the documentation.

You can of course also build the project on the command line:

# Configure a Windows debug build and execute the build
cmake --preset windows-debug
cmake --build build/windows-debug

# Configure a web release build and execute the build
cmake --preset web-release
cmake --build build/web-release


The launch.json file in the .vscode/ folder defines launch configurations for each platform. Click the launch button in the status bar to select the launch configuration and start a debugging session.

After clicking this status bar entry, you'll be asked to select a launch configuration:

When you first start a debugging session, you'll be asked to select a launch target, aka the executable you want to launch:

You can also change the launch target in the status bar:

After selecting the launch target, the code is incrementally rebuild, and the debugging session starts.

Instead of going through the status bar, you can also start a new debugging sessions by pressing `F5`. This will launch a session for the currently selected launch configuration, configure preset, and launch target.

Important: the launch configuration MUST match the preset you selected:

Debugging a desktop build is the standard experience you are used to. Set breakpoints and watches, interrupt the program at any time, and so on.

Debugging the C code compiled to WASM directly in VS Code is not possible. When you start a web debugging session, the respective launch configuration starts a local static file server (downloaded via tools/ and opens the .html file corresponding to the selected launch target in a browser tab.

When you are done "debugging" a web build, close the browser tab, and close the debugging session in VS Code by clicking the "Stop" button in the debugger controls.

If you feel adventurous: it is possible to debug the C and JavaScript code in Chrome.. We'll look into that below.

Dissecting the CMakeLists.txt and CMakePresets.json files

To understand how the build is setup, we need to understand the CMakeLists.txt and CMakePresets.json files.

We've already had a brief look at the CMakePresets.json file above. It defines a configure preset for each operating system and build type combination. Let's have a look at one of the presets, specifically, the one used for Windows debug builds.

	"name": "windows-debug",
	"displayName": "Desktop debug",
	"description": "",
	"generator": "Ninja",
	"binaryDir": "${sourceDir}/build/${presetName}",
	"cacheVariables": {
		"CMAKE_BUILD_TYPE": "Debug",
		"CMAKE_MAKE_PROGRAM": "${sourceDir}/tools/desktop/ninja/ninja"
	"toolchainFile": "${sourceDir}/tools/desktop/toolchain-clang-mingw.cmake",
	"condition": {
		"type": "equals",
		"lhs": "${hostSystemName}",
		"rhs": "Windows"

The important bits are:

The other configure presets are pretty similar and only differ in what operating system they should be available on, as well as the toolchain being used. On macOS and Linux, the default toolchain is used (GCC or Xcode's Clang). For the web, the Emscripten toolchain is used through a toolchain file that ships with Emscripten.

We could work without this presets file, but that would mean we'd have to specify all these parameters manually every time we configure a CMake build. With the presets, this becomes cmake --preset <preset-name>. Much nicer!

The CMakeLists.txt file defines the actual build itself, i.e. which source files make up which libraries and executables, and what compiler flags to use. Definitions of libraries and executables are called targets in CMake. Let's go through it section by section. We start out with this:

cmake_minimum_required(VERSION 3.21.1)


We define the minimum CMake version and project name, and enable (and require) C99 support. The final line makes CMake generate a compile_commands.json file in the build folder. This is also known as a compilation database and used by many IDEs to understand a CMake build. In our case, the file is used by the clangd VS Code extension to provide us with code completion and other niceities.

FetchContent_Declare(minifb GIT_REPOSITORY GIT_TAG dos-pr-master)

Next we pull in MiniFB via CMake's FetchContent mechanism. CMake veterans may sneer at this and rather use a Git submodule. But I like it that way, thank you very much. This magic incantation will clone my MiniFB fork with web support, disable the MiniFB example targets, and finally make the remaining MiniFB library target available to the targets defined in our own CMakeLists.txt. Nice.

add_compile_options(-Wall -Wextra -Wpedantic -Wno-implicit-fallthrough)

This section sets the "pedantic warnings are errors" compiler flags. We want the code to be reasonably clean and fail if a warning is generated.

add_library(r96 "src/r96/r96.c")

Next we add a library target called r96. It's compiled from the r96/r96.c source file. Any re-usable code we write during the course of this blog post series will go in there. Any of our demo app executable targets can then depend on the r96 library target to pull in its code.

add_executable(r96_00_basic_window "src/00_basic_window.c")
add_executable(r96_01_drawing_a_pixel "src/01_drawing_a_pixel.c")

We define two executable targets for the demos of this blog post.

    COMMAND ${CMAKE_COMMAND} -E copy_directory

We define a custom target that copies all the .html files from src/web/ to the output folder. This target is needed for web builds.

get_property(targets DIRECTORY "${_dir}" PROPERTY BUILDSYSTEM_TARGETS)
list(REMOVE_ITEM targets minifb r96 r96_assets r96_web_assets)
foreach(target IN LISTS targets)
    target_link_libraries(${target} LINK_PUBLIC minifb r96)    
        add_dependencies(${target} r96_web_assets)
        target_link_options(${target} PRIVATE

And then stuff gets crazy! The first two lines compiles a list of all demo executable targets. We then iterate through those executable targets and link the r96 library target to each of them. If we build for the web, we also add the custom r96_web_assets target to the executable as a dependency, so the .html files get copied over to the output folder.

Finally, we add a few Emscripten specific linker options. These are settings I arrived at after working with WASM for the last 2 years. They are all Emscripten specific and do things like allowing heap memory to grow. You can check out all the options in Emscripten's settings.js file. There's a lot.

The purpose of this evil incantation is to reduce the amount of CMake spaghetti needed when adding a new demo. All we need to do is add a single add_executable_target() line, specifiyng the demo name and source files its composed of. The evil incantation will then link the new demo up with all the necessary bits automatically. Nice!

The first demo app: 00_basic_window

Before we can get our hands dirty with programmatically creating the most beautiful pixels in the world, we need to understand how MiniFB works and how a demo app is structured in terms of code. With no further ado, here's src/00_basic_window.c:

#include <MiniFB.h>
#include <stdlib.h>

int main(void) {
	const int res_x = 320, res_y = 240;
	struct mfb_window *window = mfb_open("00_basic_window", res_x, res_y);
	uint32_t *pixels = (uint32_t *) malloc(sizeof(uint32_t) * res_x * res_y);
	do {
		mfb_update_ex(window, pixels, res_x, res_y);
	} while (mfb_wait_sync(window));
	return 0;

This is a minimal MiniFB app that opens a window with a drawing area of 320x240 pixels (line 6). It then allocates a buffer of 320x240 unit32_t elements (line 7). Each uint32_t element encodes the color of a pixel. Next, we keep drawing the contents of the buffer to the window via mfb_update_ex() (line 9) until mfb_wait_sync() returns false (line 10), e.g. because the user pressed the ESC key to quit the app. It can't get any simpler.

MiniFB has a super minimal API. You can learn more about it here.

Note: The mfb_update_ex() function also returns a status code which can be used to decide if the app should be exited. We're not using this above for brevity's sake.

Running the demo app on the desktop

To compile and run (or debug) our little demo app on the desktop, select the Desktop debug configure preset in the VS Code status bar, select the r96_00_basic_window target as the launch target, and the Desktop debug target launch configuration. Press F5 and you'll get this:

r96_00_basic_window on the desktop.

Most impressive. You can make changes to the code and just hit F5 again to incrementally rebuild and restart the demo app. You can also set breakpoints, inspect variables and call stacks, and so on.

Now how do we run the same demo app in the browser?

Running the demo app on the web

Select the Web debug configure preset, and the Web debug target launch configuraton and press F5. You'll see this:

r96_00_basic_window running in the browser.

The launch configuration starts a static file server (tools/web/static-server) which serves the files in build/web-debug/ on port 8123. The static server will also automatically open a browser tab with the URL corresponding to the demo's .html file.

When you're done being amazed by this, close the browser tab, and click the Stop button in the debugger controls in VS Code. If you want to be amazed again, just press F5

How the web version works

It's kind of magic. Here's how the 00_basic_window.html file for the 00_basic_window.c demo app looks like:

<!DOCTYPE html>
<html lang='en'>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <link rel='stylesheet' href='r96.css'>
    <script src='r96_00_basic_window.js'></script>
<body class='r96_content'>
    <h2>Basic window</h2>
    <canvas id='00_basic_window'></canvas>
    async function init() {
        await r96_00_basic_window()


Ignoring the boring HTML boilerplate, we see that the r96_00_basic_window.js file is loaded via a <script> tag. This file was generated as part of the build by Emscripten. It contains JavaScript code that loads the WebAssembly file that stores our compiled C code and exports functions with which we can interact with the WebAssembly code.

Next we define a <canvas> with the id 00_basic_window. Finally, a little JavaScript kicks of a call to r96_00_basic_window() (defined in r96_00_basic_window.js) in an asynchronous function. This call will load the r96_00_basic_window.wasm file and run its main() method.

How does MiniFB know to render to the canvas? In our C code we have this line:

struct mfb_window *window = mfb_open("00_basic_window", res_x, res_y);

Instead of opening a window with "00_basic_window" as the title, the MiniFB web backend uses the first argument passed to mfb_open() to search a canvas element with that string as its id. Any calls to mfb_update_ex() will then draw the contents of the provided buffer to this canvas.

Also of note: We didn't have to modify our C code at all, it just "works". If you've ever done any front-end development, that may be very weird to you. The app basically has an infinite loop! If you do that in JavaScript, the browser tab (or the whole browser) will freeze, because the browser engine's event loop will never get a chance to run and process events. How does this magic work?

The MiniFB web backend I wrote uses an Emscripten feature called Asyncify. In the implementation of mfb_wait_sync(), I call emscripten_sleep(0). This gives back control to the browser engine, so it can process any DOM events and not freeze. Our native C code will then resume again, without our C code ever knowing that it was actually put to sleep. The Asyncify feature rewrites our C code (or rather its WASM representation) to use continuations. That allows pausing and resuming the C code transparently. Super cool!

Can I debug the C code in the browser?

Yes, we can in Chrome. When we build using the Web debug configure preset, Emscripten will emit DWARF information in the resulting .wasm file. Chrome can use that information to provide native code debugging right in the developer tools. To get that working:

Launch the demo app using the Web debug target launch config in VS code, then open the dev tools in the browser, and click on the Sources tab. You can find all the .c files that make up our little demo app under the file:// node, including the MiniFB sources. Open up 00_basic_window.c and set a breakpoint inside the loop:

C/C++ debugging in Chrome

And there you have it: C/C++ debugging in Chrome! Since the C code runs the same on both the desktop and in the browser, we'll likely never need this functionality, unless we implement web specific features.

Speaking of features, let's add a second demo app and draw our first pixel! But first, some very practical "theory".

Of colors, pixels, and rasters

What's a pixel? Rumor has it that pixel is a stylized abbreviation of "(pic)ture (el)ement". A precise answer is actually quite involved and may even depend on the decade you are living in.

Here, we lazily and imprecisely define a pixel as the smallest "atomic" area within a raster for which we can define a color. A raster is a rectangular area made up of pixels. Each pixel in the raster is assumed to have the same size. The width of a raster equals the number of pixels in a row, the height equals the number of pixels in a column.

The below raster has a width of 23 pixels and a height of 20 pixels. To locate a pixel inside the raster, we use an integer coordinate system, with the x-axis pointing to the right, and the y-axis pointing down. The top left pixel in the raster is at coordinate (0, 0), the top right pixel is at coordinate (22, 0) (or (width - 1, 0)), the bottom right pixel is at coordinate (22, 19) (or (width - 1, height -1)), and so on.

A fishy raster. Source: Wikipedia

A raster can be a display device's output area, a piece of grid paper, etc. The rasters we'll work with are two-dimensional arrays in memory. Each array element stores the color of the pixel in some encoding.

Color encodings

We encode the color of a pixel using the RGBA color model, where a color is represented as an additive mix of its red, green, and blue components, and an additional alpha component specifying the pixel's opacity. The opacity comes into play when we blend pixels of one raster with pixels from another raster. That's a topic for another blog post.

More specifically, we use an ARGB8888 encoding that fits in a 32-bit unsigned integer (or uint32_t in C). Each color component is encoded as an 8-bit integer in the range 0 (no contribution) to 255 (highest contribution). For the alpha component, 0 means "fully transparent" and 255 means "fully opaque".

Here's how the components are stored in a 32-bit unsigned integer. The most significant byte stores the alpha component, then come the red, green, and blue bytes.

Storage layout of an ARGB8888 color in a 32-bit unsigned integer. Source: Wikipedia
Here are a few colors in C:
uint32_t red = 0xffff0000;
uint32_t green = 0xff00ff00;
uint32_t blue = 0xff0000ff;
uint32_t pink = 0xffff00ff;
uint32_t fifty_percent_transparent_white = 0x80ffffff;

More generally, we can compose a color by bit shifting and or'ing its individual components:

uint8_t alpha = 255; // fully opaque
uint8_t red = 20;    // a little red
uint8_t green = 200; // a lot of green
uint8_t blue = 0;    // no blue
uint32_t color = (alpha << 24) | (red << 16) | (green << 8) | blue;

That looks like a great candidate for a re-usable macro! Why a macro? Because C99 support in Microsoft's C++ compiler is still meh and who knows how it does with inlined functions defined in a header. The macro guarantees that the code is inlined at the use site. Let's put the following in src/r96/r96.h

#include <stdint.h>

#define R96_ARGB(alpha, red, green, blue) (uint32_t)(((uint8_t) (alpha) << 24) | ((uint8_t) (red) << 16) | ((uint8_t) (green) << 8) | (uint8_t) (blue))

Defining a color then becomes:

uint32_t color = R96_ARGB(255, 20, 200, 0);

Adressing a pixel in a raster

We now can define colors easily. But how do we work with rasters in code and manipulate the colors of its pixels? We already did! Remember this line from our 00_basic_window demo app?

const int res_x = 320, res_y = 240;
uint32_t *pixels = (uint32_t *)malloc(res_x * res_y * sizeof(uint32_t))

This allocates memory to store a 320x240 raster where each pixel is stored in a uint32_t. Each row of pixels is stored after the other. We can think of it as a one-dimensional array storing a two-dimensional raster.

This raster is passed to mfb_update_ex() to be drawn to the window. The reason the window content remains black is that the pixels all have the color 0x00000000 aka black (at least when building the debug variant or for Emscripten).

We can set the pixel in the top left corner at coordinate (0, 0) to the color red like this:

pixels[0] = R96_ARGB(255, 255, 0, 0);

OK, that was obvious. But how about a pixel at an arbitrary coordinate? Let's look at a smaller 4x3 pixel raster:

Our raster is a one dimensional block of memory. The pixel rows are stored one behind the other. The 4 pixels of the first pixel row with y=0 are stored in pixels[0] to pixels[3]. The index of a pixel in the first row is simply its x-coordinate. E.g. the pixel at coordinate (2, 0) is stored in pixels[2].

The pixels of the second row with y=1 are stored in pixels[4] to pixels[7]. The pixels of the third row with y=2 are stored in pixels[8] to pixels[11]. In general, the first pixel of a row at y-coordinate y is located at pixels[y * width]. And to address any pixel inside a row, we just add its x-coordinate! The general formula to go from a pixel's (x, y) coordinate to an index in the one dimensional array representing the raster is thus x + y * width!

Note: this is how C implements two-dimensional arrays under the hood as well. The principle also applies to higher dimensional arrays.

If we want to set the color of the pixel at (160, 120) to red in our 320x240 pixel raster, we can do it like this:

const int res_x = 320, res_y = 240;
pixels[160 + 120 * res_x] = R96_ARGB(255, 255, 0, 0);

Alright, time to draw some pixels!

Demo app: drawing a pixel

Drawing a single pixel is a bit boring, so how about we flood the screen with a gazillion pixels instead?

#include <MiniFB.h>
#include <stdlib.h>
#include <string.h>
#include "r96/r96.h"

int main(void) {
	const int res_x = 320, res_y = 240;
	struct mfb_window *window = mfb_open("01_drawing_a_pixel", res_x, res_y);
	uint32_t *pixels = (uint32_t *) malloc(sizeof(uint32_t) * res_x * res_y);
	do {
		for (int i = 0; i < 200; i++) {
			int32_t x = rand() % res_x;
			int32_t y = rand() % res_y;
			uint32_t color = R96_ARGB(255, rand() % 255, rand() % 255, rand() % 255);
			pixels[x + y * res_x] = color;

		if (mfb_get_mouse_button_buffer(window)[MOUSE_BTN_1]) {
			memset(pixels, 0, sizeof(uint32_t) * res_x * res_y);

		mfb_update_ex(window, pixels, res_x, res_y);
	} while (mfb_wait_sync(window));
	return 0;

The interesting bit happens in lines 11 to 15. Each frame, we generate 200 pixels at random coordinates with random colors. We ensure that the coordinates are within the raster bounds by % res_x and % res_y. We also clamp the color components to the range 0-255 via modulo.

We also introduce some light input handling by checking if the left mouse button is pressed. If so, we set all pixels to the color black, giving the "user" a way to restart the glorious demo.

Finally, we pass the pixels to mfb_update_ex(), which will draw them to the window.

And here it is live in your browser, because why would we spend so much time getting the WASM build to work so nicely. Click/touch to start!

I sure feel all this build up paid off, don't you?

Mario, WTF

Yeah, I'm sorry. I sometimes just drift off. But we learned a lot! Next time I likely won't be so wordy. We'll have a looksy at how to draw rectangles. Exciting!

Read the the next article in the series.

Discuss this post on Twitter or Mastodon.