Contributing
The Zig core lives in zig/simdra/. The TypeScript bindings (src/index.ts plus src/microsharp/index.ts, both surfaced from the package root) sit on top. Most contributions land at one of these layers.
Layout
zig/
├── simdra.zig # entry: re-exports of Sm* types + parseCssColor
└── simdra/
├── core/ # Sm* drawing types + raster pipeline
│ ├── SmSurface.zig
│ ├── SmCanvas.zig
│ ├── SmPaint.zig
│ ├── SmBitmap.zig
│ ├── SmPath.zig
│ ├── SmMatrix.zig
│ ├── SmFont.zig
│ ├── SmScan.zig # shape → coverage rows
│ ├── SmBlitter.zig # coverage rows → pixels
│ └── types.zig # internal enums
├── effects/ # SmGradient, future filters
├── encode/ # PNG / JPEG encoders
├── decode/ # stb_image
├── opts/ # SIMD kernels (NEON / generic)
└── utils/ # SmList, css_color, vendored stb headers
src/
├── index.ts # Canvas 2D binding
├── image/index.ts # MicroSharp binding
└── simdra-zig.d.ts # ambient typesBuild & test
npm test # native via node-zigar — fast iteration
npm run build # WASM bundle via vite (rollup-plugin-zigar)
npm run test:built # post-build smoke test
npm run typecheck # tsc --noEmitnpm test is the inner loop for any change to Zig or TS. Visual regressions render against @napi-rs/canvas with SSIM thresholds; structural assertions cover the rest.
Zig version: pin via zvm or similar to the version node-zigar expects (0.15.x today). Mismatch produces "Unsupported Zig version" at build time.
Conventions
File-is-struct, Sm-prefixed
One Zig "class" per file, Sm-prefixed:
// zig/simdra/core/SmFoo.zig
const std = @import("std");
const SmFoo = @This();
field_a: u32,
field_b: []u8,
pub fn init(allocator: std.mem.Allocator) !SmFoo { ... }
pub fn deinit(self: *SmFoo) void { ... }The file path becomes the JS-visible type name through node-zigar.
Skia-style static factories
Construction is a static method on the type. The first parameter must NOT be *Self — node-zigar dispatches it as a static member of the JS proxy class:
pub fn createBlank(width: u32, height: u32, settings: BitmapSettings) !SmBitmap { ... }
pub fn createBlankWithAllocator(allocator: std.mem.Allocator, ...) !SmBitmap { ... }Always provide both variants for memory-touching factories: a JS-callable form using page_allocator, and a pure-Zig form taking an explicit allocator.
Drawing methods take a SmPaint
pub fn drawRect(self: *SmCanvas, x: f64, y: f64, w: f64, h: f64, paint: *const SmPaint) voidHTML5-shaped sugar (fillRect/strokeRect/clearRect) is a thin wrapper that bundles the current ctx state into a paint and calls drawRect. Add the paint-explicit form first; add a sugar wrapper only if the WebIDL surface needs one.
One blitRow API for everything
SmBlitter.blitRow takes (pixels, dst_w, x, y, n, ?coverage, *const SmPaint). The ?coverage parameter is what makes the same blitter handle:
- Scanline rasterization (today, coverage =
null= full ink). - Anti-aliased path rasterization (future, coverage filled by SmScan).
- Tile-based rasterization (future, coverage filled by tile binner).
Don't write a parallel "AA fill" or "tile fill" code path. Extend blitRow's dispatch on paint.kind, paint.blend_mode, and non-null coverage.
Module graph
Acyclic dependencies, enforced by convention:
opts/ ← leaves (per-arch SIMD kernels)
utils/ ← leaves + types from core/
encode/, decode/ ← leaves (no upward imports)
effects/ ← core/ + utils/
core/ ← opts/, encode/, decode/core/raster.zig is the only file allowed to import opts/simd.zig.
Numeric types
Canvas API floats are f64 (WebIDL unrestricted double). Widths and heights are u32. f32 is wrong and easy to get wrong — review math carefully.
String returns
Use []const u8, not [:0]const u8. zigar's sentinel validator is incompatible with Zig's allocSentinel. The []const u8 slice is auto-flagged string-capable; JS reads it via .string.
Adding a new spec member
- Find the interface in
specs/and the unchecked member. - Decide which folder:
- Pure drawing/data primitive →
core/Sm*.zig(static or instance method). - Shader / gradient / filter →
effects/. - Image encoder →
encode/. - SIMD kernel →
opts/{generic,neon}.zig+ facade inopts/simd.zig. - HTML5 surface (CSS-string handling, data URLs, etc.) →
src/index.ts, delegating to the Sm* primitive.
- Pure drawing/data primitive →
- Exercise it in
test/index.js— visual scene viacompareSceneif pixel-shaped, structural viaplainif not. npm testto verify, thennpm run build && npm run test:built.- Tick the spec checkbox; note the implementation file/path.
Adding a SIMD kernel
Per-arch kernels live in opts/. Each backend exports the same signatures; the dispatcher in opts/simd.zig picks the right one at comptime:
// zig/simdra/opts/simd.zig
const builtin = @import("builtin");
pub const fillU32 = if (builtin.target.cpu.arch == .aarch64)
@import("neon.zig").fillU32
else
@import("generic.zig").fillU32;When adding a new kernel:
- Add the generic version first in
opts/generic.zig—@Vector(N)-based, byte-equal correctness reference. This is what WASM uses (onlyv128SIMD; no NEON). - Add the dispatcher entry in
opts/simd.zig. - Tune in
opts/neon.zigonly if profiling shows it matters. Keep generic as the spec.
Adding a vendor library
simdra vendors stb_truetype and stb_image. Adding another:
- Drop the header(s) into
zig/simdra/utils/. - Write a single TU (
zig/simdra/utils/<lib>.c) defining the*_IMPLEMENTATIONmacro and stripping unused features at the C build level. - Append to
getCSourceFiles()inzig/build.extra.zig. - Bind from a Zig file via
@cImport({ @cInclude("simdra/utils/<lib>.h") });.
useLibc: true is already on in node-zigar.config.json and vite.config.js — libc malloc/realloc/free is available everywhere we ship.
Pull request flow
Right now simdra is in its early days; PRs land via direct review. When opening one:
- Run
npm run typecheck && npm test && npm run build && npm run test:built. All four should be green. - Update
COMPATIBILITY.mdif you touched a spec-member. - Update
CLAUDE.mdif you changed conventions or added a folder. - Tick spec checkboxes in
specs/for any newly-implemented member.
See also
- Architecture — raster pipeline, Sm* taxonomy, where things live.
- Using simdra from Zig — direct Zig API surface.