Raster painting and dynamic textures

§77a (RFC 0004, accepted 2026-08-21; shipped 2026-08-29) is the sanctioned exception to the vector 2D stack of §50–§52: a texture whose texels are produced by something other than the engine — a minimap, a gauge face, procedural art, the §73 canvas view. The engine's whole contribution is a buffer, a version, a size rule, and a place to put it. There is no fillRect, no path builder, no font rasterizer and no compositing model, and §77a states that normatively: every pixel is painted by the host or the application, and an engine-defined drawing API is RFC 0004's rejected alternative C.

Nor is this §62's Canvas 2D _backend_ (@fourjs/render-canvas, an unchanged reserved stub). A backend draws the scene into a host canvas; this seam reads arbitrary pixels out of one. The two share a host surface and nothing else.

Two types carry the whole tier, both in four/render:

The browser adapter

The seam is structural and DOM-free — @fourjs/render compiles with no lib.dom, so it cannot name HTMLCanvasElement (RFC 0004, alternative F). In the discipline of TextureSource, FetchLike, PointerSurface and SurfaceObserver, the engine names a shape, the host supplies a value, and the browser adapter is a few lines in the application. This is the recipe, reproduced from packages/render/src/raster.ts's module header:

import { CanvasTexture, type RasterSource } from "fourJS/render";

const canvas = new OffscreenCanvas(256, 256);
const ctx = canvas.getContext("2d")!;

const source: RasterSource = {
  width: 256,
  height: 256,
  origin: "top-left", // what every host 2D API produces
  paint: () => drawMinimap(ctx, world),
  readPixels: (out) => out.set(ctx.getImageData(0, 0, 256, 256).data),
};
const texture = new CanvasTexture(source);
material.map = texture; // it is a MaterialTexture

// whenever the minimap should change:
texture.invalidate();
// once per frame, from render/real time — never from a fixed step (§33):
texture.update(); // repaints and re-reads only if stale

RasterSource.paint takes no parameter, and that is the seam holding: a callback taking an engine-defined context would make the engine define a drawing API (alternative C), and one taking the host's context would make the engine name a DOM type (alternative F). The source closes over whatever it paints with; the hook exists only so the engine can order the repaint against the read.

update() is yours to call — nothing polls

This line is prominent because RFC 0004 (Q6, adopted) decided it must be: nothing polls, nothing subscribes, and no per-frame hook exists. An application that calls invalidate() and forgets update() sees a stale texture and no diagnostic. Call update() once per frame from render or real time — it repaints and re-reads only when the texture is stale, so an unchanged surface costs one boolean check. Never call it (or paint) from a fixed step: a repaint driven from fixedUpdate couples an unreproducible host cost to the §10 accumulator.

Rules the tier lives under

Cross-references

GPU readback snapshots (RFC 0009)

GpuReadbackSource bridges asynchronous GPU readback to a synchronous raster source. Refresh between rendered frames; never feed these display pixels into simulation, checksums, snapshots, or replay.

import { CanvasTexture, GpuReadbackSource } from "fourJS/render";

const source = new GpuReadbackSource(target, { colorSpace: "srgb" });
const texture = new CanvasTexture(source);
// After the renderer has finished submitting the target's frame:
if (await source.refresh(renderer)) {
  texture.invalidate();
  texture.update();
}
// Attach texture to a material drawn into a different target.
// When finished:
texture.dispose();
source.dispose();

Before the first refresh the snapshot is zero-filled. Refresh returns false for an unsupported renderer or a disposed source; backend failures reject. Only one refresh may be in flight. A target resize requires a new source. The default independent snapshot limit is 64 MiB. Rows remain bottom-first; color space defaults to sRGB and is never guessed from the target.

Render-graph validation detects a texture sampling a snapshot of its own output target, including delayed feedback. Use target.colorTexture directly when CPU bytes are unnecessary.