Skip to content
GattoDev edited this page Jul 27, 2026 · 5 revisions

PPU

The PPU handles all graphics. It maintains a software framebuffer — a big flat array of colors — and exposes drawing functions that write into it. When something changes, it uploads the updated pixels to a GPU texture and displays it on screen.

Internal resolution is 256×256 pixels, displayed in a 512×512 window.


The framebuffer

The framebuffer is a flat Array of Color values with one entry per pixel — 65,536 total. Pixel at (x, y) lives at index y * 256 + x.

When any pixel changes, a dirty flag gets set. At the end of the frame, if dirty is true, the ImageTexture gets updated from the Image buffer and the flag clears. This means the texture only gets pushed to the GPU when something actually changed — not every single frame unconditionally.


Drawing functions

pset(x, y, color)

Sets a single pixel. Out-of-bounds coordinates are silently ignored — it won't crash, it just does nothing.

pget(x, y) → Color

Reads a pixel back from the framebuffer. Returns Color.BLACK if out of bounds.

clear(color)

Fills the entire screen with one color. Defaults to black if you don't pass anything. Call this at the start of tick if you don't want draws from the previous frame to persist.

line(x0, y0, x1, y1, color)

Draws a straight line between two points using Bresenham's line algorithm — a classic technique that steps along the longer axis and tracks accumulated error to decide when to step on the shorter one. No floating point, just integers.

rect(x, y, w, h, color)

Draws a rectangle outline. Just four line calls connecting the corners.

rectrot(x, y, size, angle, color)

Draws a rotatable rectangle outline

rectfill(x, y, w, h, color)

Draws a filled rectangle by calling line across each horizontal row from top to bottom.

circle(cx, cy, radius, color)

Draws a circle outline using the midpoint circle algorithm. It exploits 8-way symmetry — only one octant of the circle gets calculated, then mirrored into all eight symmetric positions. Efficient and clean.

circlefill(cx, cy, radius, color)

Draws a filled circle. For each row from -radius to +radius, it calculates the horizontal span using sqrt(r² - y²) and draws a line across it.

triangle(x1,y1, x2,y2, x3,y3, color)

Draws a triangle outline — three line calls between the three vertices.

trianglerot(x, y, size, angle, color)

Draws a rotatable triangle outline

trianglefill(x1,y1, x2,y2, x3,y3, color)

Draws a filled triangle using scanline rasterization:

  1. Sorts the three vertices top-to-bottom by Y
  2. The middle vertex splits the triangle into a top half and bottom half
  3. For each horizontal scanline, interpolates the left and right X edges along the relevant sides
  4. Draws a horizontal line across the span

sprite(x, y, sprite_data, palette)

Draws a 2D Array of palette indices as an image. Index 0 is always treated as transparent and skipped.

Param Type Description
x int X position to draw at
y int Y position to draw at
sprite_data Array 2D array of ints — each value is an index into palette
palette Array[Color] The color for each index; index 0 is never drawn

Need to convert a PNG into sprite_data + palette arrays? Use the sprite converter tool.

text(x, y, str, color, scale,background)

Renders a string using the built-in pixel font. Each character is 6 pixels wide at scale 1. The string is automatically converted to uppercase, so lowercase input is fine. Unknown characters render as ?. The scale parameter multiplies the size of the rendered text (e.g. 2 makes it twice as large in both width and height).

The font

Every character is hand-defined in a dictionary as an array of strings. "1" means draw a pixel, "0" means leave it empty. Characters are 5 rows tall and variable width (usually 3–5 pixels).

Supported characters:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
0 1 2 3 4 5 6 7 8 9
space : . - ( ) ! ? , ' " + * / = # @ % _ < > [ ] & ; ^ ~ | \

You can see all of them at once by running Template.gd — it renders every character to the screen in a grid.