Craft's standalone CPU raster core is graphics.zig.
It imports Zig's standard library and has no window, kernel, WebView or GPU dependency.
The existing renderer.zig types alias the same core.
| Operation | Executed behavior |
|---|---|
Canvas.clear, setPixel |
Replace stored pixels, including alpha |
drawRect, drawCircle, drawLine |
Replace covered pixels after clipping to the frame |
drawBitmap |
Composite an unscaled atlas region using straight-alpha source-over |
Canvas.drawText |
Family/system font lookup returns TextRenderingUnavailable |
scene.drawSprite |
Affine nearest-neighbor atlas sprites, masked tint and clipping |
scene.drawTriangle |
Affine UV sampling with a top-left shared-edge rule |
scene.drawText |
Explicit borrowed bitmap-font glyphs with UTF-8 and newlines |
scene.drawFrame |
Ordered commands with validation and aggregate budgets before writes |
| Native window or swapchain presentation | Not provided by this core |
Pixel words have the numeric format 0xAARRGGBB. A bitmap borrows tightly packed
RGBA8 bytes synchronously. Drawing retains no source pointer. Invalid dimensions,
byte counts, source bounds or overlap with the writable canvas return an error
before changing the frame. A clipped or empty geometric region draws no pixels.
Canvas.init limits pixel storage to 64 MiB. A host can select its own explicit
budget using Canvas.initWithLimits(allocator, width, height, .{ .max_pixels = n }).
Dimensions and byte arithmetic are checked before allocation. Allocation failure
returns an error. The caller owns a canvas and calls deinit exactly once; copied
canvas values do not create a second owner. Do not mutate its storage or dimensions.
The legacy Renderer owns its one created canvas and rejects another creation
with CanvasAlreadyExists; callers must not separately deinitialize that canvas.
Primitive rasterization visits only clipped frame pixels or the visible major axis of a line. Extreme offscreen coordinates cannot force iteration over the entire offscreen geometry. Directed Bresenham tie behavior is preserved.
Import graphics.scene (also available as renderer.scene). Its functions draw
into the same owned Canvas; they allocate no asset, command or per-frame storage.
All source slices are borrowed synchronously and are never retained. Do not modify
inputs concurrently with a draw. All supplied assets, masks, text, glyph tables and
command storage must be disjoint from the writable canvas.
drawSprite(&canvas, sprite, limits) takes a Bitmap, an integer atlas source
rectangle and a Transform. Local pixel-edge coordinates map to frame coordinates
as x' = a*x + c*y + tx, y' = b*x + d*y + ty. The identity is the default.
Sampling inverse-maps destination pixel centers and selects the source pixel with
floor, so scaling, rotation, mirroring and shear share the same nearest-neighbor
contract. Singular, nonfinite or numerically unrepresentable transforms fail.
Optional clip rectangles intersect the canvas before rasterization.
A Tint multiplies straight RGBA channels using rounded 8-bit arithmetic. Its
optional atlas-sized one-byte-per-pixel mask blends that result with the original
pixel using mask coverage. An absent mask applies tint to every source pixel;
zero coverage preserves the original. Player masks use full-atlas coordinates,
including when drawing an atlas subregion. This is an explicit pixel operation,
not a claim about any original game's palette rules.
drawTriangle takes three Vertex values with frame-space x, y and normalized
u, v in [0,1]. UVs interpolate affinely; nearest sampling clamps the upper
endpoint to the last atlas texel. Either winding is supported. Pixel-center
coverage uses the top-left edge rule, so adjacent triangles do not double-blend
their shared edge. Degenerate triangles and invalid coordinates/UVs fail before
writes. This API supplies 2D textured terrain, without perspective correction or
a hardware pipeline.
drawText requires a BitmapFont containing its RGBA atlas, sorted unique Unicode
scalar Glyph records and a positive line_height. Each glyph supplies an atlas
rectangle, integer bearing and advance. The origin is a top-left pen; newline
resets horizontal advance and adds line height. The complete string and font are
validated before drawing, including missing glyphs. The same affine transform,
mask/tint and clip semantics apply to glyph sprites. System font discovery,
TrueType rasterization, shaping, bidirectional layout and kerning are not provided.
The separate family-based Canvas.drawText continues to return an explicit error.
drawFrame accepts a borrowed slice of Command values (sprite, triangle,
text) and executes it in slice order using straight-alpha source-over. Every
command is validated before the first write, so a later invalid command cannot
leave an earlier command's pixels behind. The default scene.Limits are:
| Limit | Default |
|---|---|
| Asset bytes | 64 MiB of unique typed views, including masks and glyph records |
| Unique asset views | 1,024 per frame, with a fixed supported maximum |
| Clipped bounding-box pixel work | 67,108,864 pixels |
| Font glyph count | 4,096 per font |
| UTF-8 text bytes | 1 MiB |
| Frame commands | 65,536 |
Asset bytes count identical typed views once within a frame. A view's identity is its role (RGBA atlas, player mask or glyph metadata), start address and exact byte length. A full atlas is charged even when commands sample different subregions. Different offsets, lengths and roles remain separate charges, including partially overlapping views. Each command still receives complete validation. Deduplication never removes a command's pixel-work or text-byte charge.
The frame uses a fixed 2,048-slot table with at most 1,024 unique views. Its entries
occupy 48 KiB on 64-bit targets, plus two counters and ordinary call frames. Hosts
must budget that stack storage separately from canvas pixels and borrowed inputs.
No allocator or global cache supplies the table, and all stored addresses expire
at function return. Lookups use bounded probing; exhaustion returns
AssetViewLimitExceeded before drawing. max_asset_views can select a smaller
limit, including zero for an empty frame. Values above
scene.max_supported_asset_views return InvalidAssetViewLimit.
Pixel work and text bytes still accumulate across every frame command. Hosts can explicitly set different byte/work limits; all budget arithmetic is checked. Offscreen geometry is clipped before iteration. Canvas allocation retains its separate pixel budget and allocator error contract. A scene draw does not request more memory, even if that allocator can no longer allocate. Rejected work returns an error before changing the canvas.
The separate craft.metal_surface API presents
completed opaque CPU frames through a native CAMetalLayer. Its macOS fixture
compares actual drawable pixels, including resize and backing scale. This is a
surface adapter, with separate error and memory contracts. It does not change
the legacy GPU pipeline's capabilities below. iOS presentation and native game
integration still require their own execution evidence.
GPU.getRenderingCapabilities describes
implemented execution separately from requested backend settings and hardware
discovery. Currently it reports RGBA8 offscreen clear, with hardware canvas, shader
compilation, drawing, compute, presentation, hardware video decode, post-processing,
ray tracing and multi-GPU discovery unavailable.
Unavailable draw, indexed draw, mesh, compute, effects, decoder, ray-tracing and
enumeration calls return UnsupportedOperation. Shader compilation returns
ShaderCompilationUnavailable. An unexecuted ray query does not return a successful
miss, and an undecoded stream does not return successful empty video bytes.
Unsupported work does not increment successful render statistics. CPU query timing
is not GPU execution evidence. Offscreen pixel allocation is not native presentation.
Offscreen Texture.init limits each attachment to 64 MiB, with an explicit
Texture.initWithLimits byte budget for hosts requiring a different bound. A render
target can own both color and depth attachments. Resizing temporarily owns the old
and new storage, then commits both attachments together. An allocation failure
retains the entire previous frame and releases temporary storage. Upload and
download require exactly matching byte counts, and do not write a rejected buffer.
RGBA8 clears reject nonfinite or out-of-range channels before changing either
attachment. Other clear formats return UnsupportedOperation.
Run the maintained tests with the toolchain pinned by pantry.lock:
bun scripts/zig.ts --core build test:graphics test:renderer test:gpuThe standalone tests import the core directly and assert exact pixels, straight alpha, directed line ties, buffer borrowing, budget rejection and allocation failure cleanup. Renderer tests retain extreme-coordinate regressions and ownership checks. GPU tests reject unavailable operations through their public interfaces. These tests establish CPU behavior and explicit capability errors. Native surfaces, platform rendering require their own execution evidence. Scene tests compare actual transformed sprite, masked tint, shared-edge triangle, UV atlas, UTF-8 glyph and ordered-alpha pixels. They also retain invalid-input, all-or-nothing frame, unique-view byte/count budgets, metadata exhaustion, aliasing and allocation-free repeated-frame checks.