Skip to main content

Drawing Basics

The PALETTE Register#

WASM-4 can only display 4 colors on screen at a time. This palette's RGB values are stored in the PALETTE memory register, and can be modified.

For example, to change the palette to Ice Cream GB:

w4.PALETTE.* = .{    0xfff6d3,    0xf9a875,    0xeb6b6f,    0x7c3f58,};

The palette colors are considered to be numbered 1-4, even though they may be accessed with indices 0-3.

The default Gameboy-ish palette looks like this:

Color 1
Color 2
Color 3
Color 4

The first color in the palette register is used as the screen background color.

The DRAW_COLORS Register#

DRAW_COLORS is a set of 4 indexes into PALETTE. Drawing functions use these indexes to decide which colors to use, and what to use them for.

DRAW_COLORS is a 16 bit value that holds 4 indexes. Bits 0-3 (the least significant bits) hold the first draw color, bits 4-7 hold the second draw color, and so on.

Setting a draw color to 1 means use PALETTE color 1 for that draw color. The same applies when setting a draw color to 2, 3, or 4.

For example, rect() uses the first draw color for the fill color, and the second draw color as the outline color. To draw a light-green (palette color 2) rectangle with a black (palette color 4) outline:

w4.DRAW_COLORS.* = 0x42;w4.rect(10, 10, 32, 32);

However, setting a draw color to 0 will make it transparent. For example, to draw a black outlined rectangle with no fill, set DRAW_COLORS to 0x40.

Other Shapes#

For info on other shape drawing functions like line() and oval(), see the Functions reference.

Direct Framebuffer Access#

The FRAMEBUFFER memory region contains the framebuffer, with each byte containing 4 pixels (2 bits per pixel). In the framebuffer, the palette colors 1-4 are represented numerically as 0-3.

For example, to clear the entire screen to palette color 4, we write 3 into each position:

for (w4.FRAMEBUFFER) |*x| {    x.* = 3 | (3 << 2) | (3 << 4) | (3 << 6);}

Advanced users can implement their own drawing functions by carefully manipulating the framebuffer. For example, to implement a pixel() function that draws a single pixel:

fn pixel(x: i32, y: i32) void {    // The byte index into the framebuffer that contains (x, y)    const idx = (@intCast(usize, y) * 160 + @intCast(usize, x)) >> 2;
    // Calculate the bits within the byte that corresponds to our position    const shift = @intCast(u3, (x & 0b11) * 2);    const mask = @as(u8, 0b11) << shift;
    // Use the first DRAW_COLOR as the pixel color    const palette_color = @intCast(u8, w4.DRAW_COLORS.* & 0b1111);    if (palette_color == 0) {        // Transparent        return;    }    const color = (palette_color - 1) & 0b11;
    // Write to the framebuffer    w4.FRAMEBUFFER[idx] = (color << shift) | (w4.FRAMEBUFFER[idx] & ~mask);}