Arara is a library for building graphics natively built with Zig. It provides a set of primitives for building rendering pipelines and a set of utilities for working with OpenGL.
Note:
This is a work in progress. It is not yet ready for production.
Arara currently targets native macOS builds in build.zig by default (x86_64-macos), and requires:
zig(0.15.1or compatible)glfwharfbuzz- Xcode Command Line Tools (
xcrun, macOS SDK, OpenGL framework)
Install on macOS with Homebrew:
brew install zig glfw harfbuzz
xcode-select --installVerify tools are available:
zig version
pkg-config --modversion harfbuzz
brew --prefix glfw
xcrun --sdk macosx --show-sdk-pathUse the examples index as the entrypoint:
examples/README.md
Per-example guides:
- C smoke example:
examples/c/README.md - Zig balls example:
examples/zig/balls/README.md - Zig Clay Joe layout demo, stdout (unstable):
examples/zig/clay_joe_definition/README.md - Zig Clay Joe render demo, OpenGL window (unstable):
examples/zig/clay_joe_render_demo/README.md
- The render thread (the caller of
renderLoop) owns the GL context and replays draw commands throughRenderer. setThreadUpdatespawns a worker thread. Each tick the worker writes into the nextFrameslot of an internalTripleBuffer(Frame)and publishes it.- The render thread pulls the latest published frame on each
renderLoopiteration. Stale frames are skipped — the consumer always sees the most recentFrame. - Input crosses the thread boundary through
SharedInput(Snapshot)(mutex-protected). The render thread callsupdate; the worker reads viasnapshot(). - The render loop walks the
RenderStagemachine (BEFORE_CLEAR → CLEAR → BEFORE_DRAW → DRAW → BEFORE_END → END). Register side effects (e.g.,glfwSwapBuffers, FPS overlays) with theinterceptBeforeStages/interceptBeforeClearStage/interceptClearStage/interceptBeforeDrawStage/interceptDrawStage/interceptBeforeEndStage/interceptEndStagehooks.
This example shows the resource setup, BufferedRender initialization, threaded update registration, world/screen drawing, and the render loop.
const std = @import("std");
const arara = @import("arara");
const BufferedRender = arara.BufferedRender;
const Color = arara.Color;
const FontBook = arara.FontBook;
const Frame = arara.BufferedRenderFrame;
const OpenGLUtils = arara.OpenGLUtils;
const font_data = @embedFile("src/fonts/monofonto_rg.otf");
const InputSnapshot = struct {};
const SharedInput = arara.SharedInput(InputSnapshot);
const State = struct {};
// Produce the initial state for the update thread
fn produceInitialState(_: f32, _: f32) State {
return .{};
}
// Compute frame logic and queue draw commands
fn update(
frame: *Frame,
_: *State,
_: InputSnapshot,
font_book: *FontBook,
_: f32,
_: f32,
) void {
frame.clearCommands();
frame.setBackground(Color.fromHsl(263.0 / 360.0, 0.4, 0.48));
frame.setCameraPos(0.0, 0.0);
frame.setZoom(1.0);
// World-space commands (affected by camera and zoom)
frame.drawWorldRect(80.0, 80.0, 160.0, 90.0, Color.fromHsl(40.0 / 360.0, 0.8, 0.55));
frame.drawWorldArc(200.0, 150.0, 32.0, 0.0, std.math.pi * 2.0, Color.fromHsl(200.0 / 360.0, 0.7, 0.55));
// Screen-space command (UI overlay)
const font_id = font_book.getFontId("Monofonto") catch return;
frame.drawScreenText(16.0, 16.0, font_id, 24.0, Color.White(), "Hello from BufferedRender", .{});
}
// Main function
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Setup font book
var font_book = FontBook.init(allocator);
defer font_book.deinit();
_ = font_book.addFontData(font_data, "Monofonto");
// Setup window
const width: f32 = 800;
const height: f32 = 600;
const window = try OpenGLUtils.Window.create(width, height);
defer OpenGLUtils.Window.deinit(window);
// Setup shared input
var shared_input: SharedInput = .{};
// Setup buffered render
var buffered_render: BufferedRender(InputSnapshot, State) = .init(
allocator,
.{ .width = width, .height = height, .font_book = font_book },
);
defer buffered_render.deinit();
// Register the update thread
buffered_render.setThreadUpdate(produceInitialState, update, &shared_input);
// Render loop
while (OpenGLUtils.Window.requestNextFrame(window)) {
// Update the shared input
shared_input.update(.{});
// Render the frame
buffered_render.renderLoop();
}
}Build C-consumable artifacts (both static and shared):
zig build c-installDependency paths are discovered automatically (Homebrew/Xcode on macOS).
To force custom locations, pass overrides such as:
zig build c-install -Dglfw_prefix=/custom/glfw/prefix -Dmacos_sdk_path=/custom/MacOSX.sdkAfter building, artifacts are installed under zig-out/:
- Header:
zig-out/include/arara.h - Static library:
zig-out/lib/libarara.a - Shared library (macOS):
zig-out/lib/libarara.dylib
examples/c/smoke.c follows the threaded BufferedRender model:
- C creates and owns the GLFW window/context and runs swap/poll.
- C loads font bytes and passes them to Arara.
- C explicitly controls the update thread with
arara_buffered_render_start_thread/arara_buffered_render_stop_thread. - The C main loop pushes input and calls
arara_buffered_render_render_loop.
For full standalone smoke build/run instructions, see:
examples/c/README.md
- Run tests:
zig build test - Run native benchmarks:
zig build bench
Benchmark output is JSON-lines so it can be archived and compared over time.
Architecture and migration docs are available under docs/:
docs/architecture.mddocs/dependency-boundaries.mddocs/benchmarking.mddocs/testing-strategy.mddocs/adr/ADR-0001-backend-interface.md
- World commands are affected by camera/zoom; screen commands are drawn in screen space.
Framecurrently limits queued commands with:MAX_DRAW_COMMANDS = 64(per world buffer and per screen buffer)MAX_TEXT_LEN = 128(max text payload per text command)
- When a buffer is full,
OverflowPolicydecides what happens..drop_newest(default) drops the incoming command and incrementsworld_dropped_commands/screen_dropped_commands;.replace_lastoverwrites the most recent slot. Tune viaFrame.setOverflowPolicy.
