Project Overview
Mach is a Zig-based game engine and graphics toolkit designed for high-performance, cross-platform development of games, visualizations, and GUI applications. Its modular architecture lets you pick and integrate only the subsystems you need, reducing bloat and easing maintenance.
Why Mach Exists
• Streamline cross-platform graphics and window management in Zig
• Provide a unified toolkit for rendering, input, asset loading, and GUI
• Leverage Zig’s performance and safety features for real-time applications
Problems It Solves
• Boilerplate reduction for Vulkan, Metal, DirectX setup
• Consistent input handling across Windows, macOS, Linux
• Flexible asset pipeline for textures, models, shaders
• Integrated GUI support alongside core rendering
Key Features
• Modular design: import only the drivers and backends you use
• Low-level graphics bindings: direct access to Vulkan/Metal APIs
• Windowing & context management: single API for all platforms
• Input system: keyboard, mouse, gamepad with event polling
• Asset loading: images, 3D models, shader compilation
• Immediate-mode GUI: built-in widgets and layout tools
• Extensible middleware: audio, physics, networking (community plugins)
Licensing
• Code is dual-licensed under Apache 2.0 or MIT (your choice)
– Retain the appropriate header in each source file
– Include LICENSE-APACHE and/or LICENSE-MIT in your distribution root
• Documentation, examples, and media are CC-BY-4.0
– Add a short credit notice when reusing docs or assets
• For directory-specific terms, check nested LICENSE files
Refer to LICENSE, LICENSE-APACHE, and LICENSE-MIT in the repo root for full legal terms.
Getting Started
This guide takes you from zero to a running red triangle in minutes. You’ll install Zig, clone the Mach repo, build the core-triangle example, and run it.
1. Install Zig (Pinned Version)
Mach pins its Zig compiler in .zigversion
. Install that exact version to avoid compatibility issues.
Contents of .zigversion
0.14.0-dev.2577+271452d22
Using zigup (recommended):
# Install zigup (if not already)
curl -sSf https://zigup.dev/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
# Install and activate the pinned version
zigup install $(cat .zigversion)
zigup run -- zig version # verify matches .zigversion
Or download a matching build from ziglang.org and ensure zig version
outputs the same string.
2. Clone Mach
git clone https://github.com/hexops/mach.git
cd mach
The first build will automatically fetch Mach’s external dependencies defined in build.zig.zon
.
3. Build the Core-Triangle Example
Core-triangle uses Mach’s WebGPU support (sysgpu
) and the example build path in build.zig
.
# Build core-triangle (with sysgpu enabled)
zig build -Dsysgpu=true -Dexamples=core-triangle
On success you’ll see a build artifact under zig-out/bin/core-triangle
.
4. Run the Triangle
# Run the built example
zig build run -Dsysgpu=true -Dexamples=core-triangle
A window should appear displaying a solid red triangle.
Troubleshooting
- If you see missing libraries on Linux, install Vulkan and X11 development packages.
- On macOS ensure
metal
andcoregraphics
frameworks are available (installed by default). - On Windows you may need the Windows 10+ SDK for DX12 support.
What Just Happened
build.zig
read your.zigversion
Zig compiler.- It fetched only the
mach.core
andmach.sysgpu
modules needed by core-triangle. - It compiled the WGSL shaders at compile time (
@embedFile("examples/core-triangle/shader.wgsl")
). - You ran the example via the auto-generated Zig build commands.
Now you have a working Mach project and a foundation for exploring deeper APIs like audio, input, and multiple windows.
Core Concepts
Mach organizes engine functionality into modular subsystems—Core (window/input), Graphics, Audio, Math, Modules/Objects, Timing, and System wrappers (sysgpu, sysaudio). Every application follows a consistent init → tick → shutdown pattern, leveraging high-priority audio and GPU threads for real-time performance.
1. Initialization Sequence
Initialize Core first, then optional subsystems. mach.Core.init
sets up window, GPU device, input, and event queue. After Core, initialize graphics or audio as needed:
const std = @import("std");
const mach = @import("mach");
pub fn main() !void {
const allocator = std.heap.page_allocator;
// 1. Initialize core (window, GPU, input)
var core = try mach.Core.init(allocator, .{
.title = "My Mach App",
.width = 1280,
.height = 720,
.vsync = true,
});
// 2. Initialize graphics (optional)
const gfx = try mach.gfx.main.init(allocator, &core);
// 3. Initialize audio (optional)
var audio = try mach.Audio.init(allocator, .{ .sample_rate = 48_000, .channels = 2 });
// 4. Enter main loop
const exit_code = runLoop(&core, &gfx, &audio) catch |err| {
std.debug.print("Error: {any}\n", .{err});
1
};
// 5. Shutdown subsystems in reverse order
audio.deinit();
gfx.deinit();
core.deinit();
return exit_code;
}
2. Main Loop and Tick Functions
Each frame, process input/events, update logic, submit graphics and audio:
fn runLoop(core: *mach.Core, gfx: *mach.gfx.main.Context, audio: *mach.Audio.Context) !u8 {
while (core.tick()) {
// -- Input & Events already handled by core.tick()
// -- Game update
updateGame(core);
// -- Graphics: record and submit draw commands
try gfx.beginFrame();
renderScene(gfx);
gfx.endFrame();
// -- Audio: mix & submit
try audio.tick();
}
return 0;
}
3. Graphics Abstraction (sysgpu + mach.gfx)
Mach uses sysgpu
for cross-API GPU resources and mach.gfx
for sprites/text:
// Create a GPU buffer via sysgpu
var buffer = try sysgpu.Buffer.create(&core.device, .{
.size = 1024 * @sizeOf(f32),
.usage = .VERTEX,
});
// Map, write data, then unmap:
const data = try buffer.mapWrite();
for (data) |*elem, i| elem.* = f32(i) * 0.5;
buffer.unmap();
// Submit buffer in a render pass
const pass = try core.device.beginRenderPass(.{/* attachments */});
pass.setVertexBuffer(0, buffer.handle, 0, buffer.size);
pass.draw(3, 1, 0, 0);
pass.end();
core.device.submit();
For batched sprites and text, see mach.gfx.sprite
and mach.gfx.text
pipelines.
4. Audio System (mixSamples & sysaudio)
Mach’s audio mixer accumulates multiple sources into a render buffer, then submits via sysaudio
:
// In initialization:
var audio = try mach.Audio.init(allocator, .{ .sample_rate=48_000, .channels=2 });
// Per‐frame in audio.tick():
pub fn tick(self: *Context) !void {
// Zero mixing buffer to avoid carry-over
@memset(self.mix_buffer.items, 0);
// Mix each active source
for (self.buffers.items) |*buf| {
if (!buf.playing) continue;
buf.index = mach.Audio.mixSamples(
self.mix_buffer.items,
@intCast(u8, self.channels),
buf.samples,
buf.index,
buf.channels,
buf.volume,
);
if (buf.index >= buf.samples.len) buf.playing = false;
}
// Convert f32 → device format and submit
try sysaudio.Stream.write(self.stream, self.mix_buffer.items);
}
5. Math Conventions
All matrices are column-major, left-handed, +Y up. Use Mat4x4
and Vec
types:
const math = @import("mach/math");
const view = math.Mat4x4.translate(math.vec3(0, 0, -10));
const proj = math.Mat4x4.projection2D(.{
.left=-8, .right=8,
.bottom=-4.5, .top=4.5,
.near=0.1, .far=100,
});
const mvp = view.mul(&proj);
6. Modules & Object Management
Use Objects
and Modules
generics from src/module.zig
to register subsystems and track typed entities:
const ObjMgr = module.Objects(u64, MyObjectData);
var objects: ObjMgr = ObjMgr.init(allocator);
// Create an object with custom data
const id = try objects.create(.{ .position = .{0, 0}, .velocity = .{1, 1} });
// Query or delete
if (objects.exists(id)) {
var data = objects.get(id);
data.position += data.velocity;
}
objects.delete(id);
7. Timing Utilities
Use Timer
and Frequency
from src/time/main.zig
for precise timing:
var timer = mach.time.Timer.init(); // high-precision clock
// Fixed timestep loop example
const dt = mach.time.Frequency.seconds(1) / 60; // nanoseconds per 1/60s
var accumulated: u64 = 0;
while (core.tick()) {
const frame_ns = timer.elapsed();
timer.reset();
accumulated += frame_ns;
while (accumulated >= dt) {
fixedUpdate();
accumulated -= dt;
}
render();
}
These core concepts form the foundation for any Mach‐based application, ensuring consistent initialization, update, rendering, and shutdown across platforms.
Examples & Tutorials
This section delivers step-by-step guides for common tasks in the hexops/mach repository. To get started, please pick one of the topics below or describe another area you’d like covered, along with any relevant file summaries or context:
Common Topics
- Config file loader utility (
config/loader.go
) - API client initialization (
client/client.go
) - Error-handling middleware (
http/middleware/errors.go
) - Database migration script usage (
db/migrations/…
) - Writing a new Mach task (
tasks/your_task.go
)
What We Need from You
- The specific feature or file you want documented
- A brief summary of its purpose or key methods
- Any example inputs/outputs or usage scenarios
With that information, we’ll produce a focused, example-driven tutorial tailored to hexops/mach.
Architecture Deep Dive
This section dives into low-level mechanics of Mach’s backends and subsystems. It helps you build new platforms or debug initialization, memory management, and audio streaming.
Linux Backend Initialization and Fallback
Describe how Mach chooses between X11 and Wayland for window creation, handles errors, and falls back when needed.
Overview
- Entry point:
Linux.initWindow(core, window_id)
insrc/core/Linux.zig
. - Reads
MACH_BACKEND
(case-insensitive “x11” or “wayland”), defaults to Wayland. - On failure, logs the error and attempts the other backend.
- Reports missing shared libraries if both fail.
- After success, calls
warnAboutIncompleteFeatures()
.
Core Logic
pub fn initWindow(core: *Core, window_id: mach.ObjectID) !void {
// 1) Determine backend
const desired: BackendEnum = blk: {
const env = std.process.getEnvVarOwned(core.allocator, "MACH_BACKEND") catch break :blk .wayland;
defer core.allocator.free(env);
if (std.ascii.eqlIgnoreCase(env, "x11")) break :blk .x11;
if (std.ascii.eqlIgnoreCase(env, "wayland")) break :blk .wayland;
std.debug.panic("mach: unknown MACH_BACKEND: {s}", .{env});
};
// 2) Try desired, then fallback
switch (desired) {
.x11 => {
X11.initWindow(core, window_id) catch |err| {
log.err("X11 init failed: {s}\nFalling back to Wayland\n", .{errMsg(err)});
Wayland.initWindow(core, window_id) catch |e| fallbackFail(x11Missing(), e);
};
},
.wayland => {
Wayland.initWindow(core, window_id) catch |err| {
log.err("Wayland init failed: {s}\nFalling back to X11\n", .{errMsg(err)});
X11.initWindow(core, window_id) catch |e| fallbackFail(waylandMissing(), e);
};
},
}
// 3) Warn if some features remain unimplemented
try warnAboutIncompleteFeatures(desired, &MISSING_FEATURES_X11, &MISSING_FEATURES_WAYLAND, core.allocator);
}
Supporting Functions
fn errMsg(err: anyerror) []const u8 {
return switch (err) {
error.LibraryNotFound => "Missing library",
error.FailedToConnectToDisplay => "Failed to connect",
error.NoServerSideDecorationSupport => "No decoration support",
else => "Unknown error",
};
}
fn x11Missing() []const []const u8 { return &MISSING_FEATURES_X11; }
fn waylandMissing() []const []const u8 { return &MISSING_FEATURES_WAYLAND; }
fn fallbackFail(missing_libs: []const []const u8, last_err: anyerror) !void {
var list = std.ArrayList(u8).init(core.allocator);
defer list.deinit();
for (missing_libs) |lib| try list.appendSlice("\t* " ++ lib ++ "\n");
log.err("Both backends failed. Missing:\n{s}", .{list.items});
return last_err;
}
Practical Tips
- Force backend:
export MACH_BACKEND=x11
orwayland
. - Watch stderr for “Falling back…” messages.
- Features like resizing, VSync, cursor may show one-time warnings.
GPU Memory Allocation (D3D12 MemoryAllocator)
Mach groups D3D12 heap sub-allocations to reduce fragmentation and heap churn.
Core Types
- MemoryAllocator: top-level pool, holds multiple MemoryGroups.
- MemoryGroup: categories by resource type (buffer, RTV/DSV texture, other texture) and memory location (
gpu_only
,cpu_to_gpu
,gpu_to_cpu
). - MemoryHeap: wraps one
ID3D12Heap
and agpu_allocator.Allocator
for sub-allocations. - AllocationCreateDescriptor: hints (
location
,size
,alignment
,resource_category
). - ResourceCreateDescriptor: describes
ID3D12_RESOURCE_DESC
for placed resources.
Initialization
In your Device.init
:
// After creating d3d12_device
try device.mem_allocator.init(device);
try device.mem_allocator_textures.init(device);
This queries D3D12_OPTIONS.ResourceHeapTier
and sets up groups.
High-Level Buffer Allocation
// Creates a placed buffer resource
const resource = try device.createD3dBuffer(usageFlags, size);
// resource.d3d_resource: ID3D12Resource*
// resource.allocation: heap + offset
Under the hood:
const alloc_desc = AllocationCreateDescriptor{
.location = .gpu_only,
.size = conv.d3d12ResourceSizeForBuffer(size, usageFlags),
.alignment = info.Alignment, // from GetResourceAllocationInfo
.resource_category = .buffer,
};
const allocation = try device.mem_allocator.allocate(&alloc_desc);
defer allocation.alloc.free(); // or via Resource.deinit()
device.d3d_device.CreatePlacedResource(
allocation.heap.heap,
allocation.offset,
&buffer_desc,
initialState,
null,
uuidof(ID3D12Resource),
&resource_ptr,
);
Creating Textures or Other Resources
var desc: c.D3D12_RESOURCE_DESC = /* populate */;
const create_desc = ResourceCreateDescriptor{
.location = .gpu_only,
.resource_category = .other_texture,
.resource_desc = &desc,
.clear_value = null,
.initial_state = D3D12_RESOURCE_STATE_COMMON,
};
const tex = try device.mem_allocator.createResource(&create_desc);
// tex.allocation holds heap+offset for lifetime management
Cleanup and Diagnostics
Resource.deinit()
frees sub-allocation and releases the D3D12 resource.- Empty heaps retire automatically when all allocations free.
- After shutdown:
device.mem_allocator.reportMemoryLeaks();
device.mem_allocator_textures.reportMemoryLeaks();
ALSA Playback Stream Creation and Control
Shows how Mach’s ALSA backend enumerates devices, creates a playback stream, and controls playback and volume.
1. Initialize Context & Refresh Devices
const alsa = @import("sysaudio/alsa.zig").Context;
var ctx = try alsa.init(std.heap.page_allocator, .{
.app_name = "MyApp",
.deviceChangeFn = null,
.user_data = null,
});
try ctx.alsa.refresh(&ctx.alsa);
const devices = ctx.alsa.devices(ctx.alsa);
2. Select Default Playback Device
const defaultDev = ctx.alsa.defaultDevice(ctx.alsa, .playback)
orelse return error.NoPlaybackDevice;
3. Create the Player
const writeFn: main.WriteFn = myWriteCallback;
const opts: main.StreamOptions = .{
.format = null,
.sample_rate = null,
.media_role = .default,
.user_data = null,
};
var player = try ctx.alsa.createPlayer(&ctx.alsa, defaultDev, writeFn, opts);
4. Start and Control Playback
// Launch write thread
try player.alsa.start();
// Play / Pause
try player.alsa.play();
try player.alsa.pause();
const paused = player.alsa.paused();
// Volume: [0.0 .. 1.0]
try player.alsa.setVolume(player.alsa, 0.75);
const vol = try player.alsa.volume(player.alsa);
5. Cleanup
player.alsa.deinit();
ctx.alsa.deinit(&ctx.alsa);
Notes
refresh()
auto-detects channel maps and formats.- Use
deviceChangeFn
to handle hot-plug events on/dev/snd
. - Override default latency via
snd_pcm_set_params
only when necessary.
API Reference
This section groups all public-facing types, functions, constants, and shader DSL constructs in the mach library. Each subsection corresponds to a logical module or feature area.
Audio Module (src/Audio.zig
)
• mixSamples(dst: []align(al) f32, dst_channels: u8, src: []align(al) const f32, src_index: usize, src_channels: u8, src_volume: f32) usize
SIMD-accelerated mixing with per-buffer gain and channel conversion.
• linearToDecibel(linear: f32) f32
Convert linear amplitude to decibels.
• decibelToLinear(db: f32) f32
Convert decibel value back to linear amplitude.
• pub const AudioFormat
Supported sample formats (e.g., f32, i16).
• pub const DeviceConfig
Configuration for opening an audio device.
• pub const StreamConfig
Sample rate, channels, and buffer size.
Sysaudio Backends (src/sysaudio/*.zig
)
• init(cfg: DeviceConfig) !void
Initialize the system-audio backend.
• start() !void
/ stop() !void
Control audio streaming.
• convertTo(dst: []align(al) u8, src: []const f32, format: AudioFormat) void
Convert float-32 buffer to hardware format.
Math Module (src/math/*.zig
)
Types
• pub const Vec2
, Vec3
, Vec4
• pub const Mat3
, Mat4
• pub const Quaternion
Functions
• dot(a: VecN, b: VecN) T
• cross(a: Vec3, b: Vec3) Vec3
• length(v: VecN) T
/ normalize(v: VecN) VecN
• translate(m: Mat4, v: Vec3) Mat4
/ rotate(m: Mat4, angle: T, axis: Vec3) Mat4
• perspective(fov: T, aspect: T, near: T, far: T) Mat4
• lookAt(eye: Vec3, center: Vec3, up: Vec3) Mat4
Shader DSL (src/Shader.zig
)
Macros & Functions
• pub fn shader(comptime stage: ShaderStage, src: []const u8) ShaderModule
• @vertex(fn: anytype) anytype
/ @fragment(fn: anytype) anytype
• uniform(name: []const u8, T: type) var
• varying(name: []const u8, T: type) var
• @encode(value: anytype, fmt: TextureFormat) anytype
• @decode(value: anytype, fmt: TextureFormat) anytype
Types & Enums
• pub const ShaderStage = enum { Vertex, Fragment }
• pub const TextureFormat
Testing Helpers (src/testing.zig
)
• pub fn expect(comptime T: type, expected: T) Expect(T)
• pub const ExpectFloat
, ExpectVector
, ExpectBytes
, ExpectComptime
• Methods on Expect(T)
:
– .eql(actual: T) !void
– .eqlApprox(actual: T, tol: U) !void
– .eqlBinary(actual: T) !void
Utilities (src/util/*.zig
)
• alignDown(addr: usize, a: usize) usize
/ alignUp(addr: usize, a: usize) usize
• clamp(val: T, min: T, max: T) T
• lerp(a: T, b: T, t: T) T
• mixChannels(dst: []align(al) f32, src: []align(al) f32, channels: u8) void
Constants
• pub const DefaultSampleRate = 48_000
• pub const DefaultBufferSize = 512
• pub const MaxChannels = 8
• pub const simdVectorLength = @import("std").simd.suggestVectorLength(f32)
Each item above appears in generated docs via zig doc
. Use these groupings to navigate and extend the mach API.
Development & Contribution Guide
Get Mach running locally, customize its build, and add or verify features.
Building Mach from Source
Mach uses a Zig-based build script (build.zig) with fine-grained flags. By default, zig build
compiles everything. Use flags to speed up CI or local iteration.
Available flags
--examples
– build/install example apps underexamples/…
--libs
– build/install static libraries only (e.g.mach-sysgpu
)--mach
– top-levelmach
module and its lazy deps (freetype, opus, font-assets)--core
– core windowing/backend (@import("mach").core
)--sysaudio
– audio backend + its tests (tests/sysaudio/{record,zine}.zig
)--sysgpu
– GPU backend + static lib target
Common commands
# Full build (default)
$ zig build
# Core engine + GPU only
$ zig build --core --sysgpu
# Examples only
$ zig build --examples
# Static libs only
$ zig build --libs
# View help & all flags
$ zig build --help
Under the hood, each flag sets a want_*
boolean:
const build_all = !any_flag;
const want_mach = build_all or flag_mach;
const want_core = build_all or want_mach or flag_core;
const want_sysaudio= build_all or want_mach or flag_sysaudio;
const want_sysgpu = build_all or want_mach or want_core or flag_sysgpu;
if (want_core) linkCore(b, mach_module);
if (want_sysaudio) linkSysaudio(b, mach_module);
if (want_sysgpu) linkSysgpu(b, mach_module);
if (flag_examples) buildExamples(b);
if (flag_libs) buildStaticLibs(b);
Practical notes
- Lazy-fetches only needed dependencies (freetype, Xcode frameworks, etc.)
- Zig’s comptime and dead-code elimination keep your binary lean
- Combine flags to tailor CI, packaging, or local iteration
Running and Writing Tests
Mach provides src/testing.zig
for flexible, readable assertions in Zig tests. Import its API in any *.zig
under tests/…
.
Basic Usage
const std = @import("std");
const testing = @import("src/testing.zig");
// A simple test harness
test "vector addition matches expected" {
const a = .{1.0, 2.0, 3.0};
const b = .{4.0, 5.0, 6.0};
const result = addVectors(a, b);
try testing.expect(std.testing, testing.ExpectVec3(result), .{10e-6}, .{5.0, 7.0, 9.0});
}
Key APIs
• expect(h: std.testing.Test, E: ExpectT, epsilon: T, expected: T)
Generic: compares floats, vectors, matrices with approximate equality.
• ExpectBytes(actual: []const u8)
Exact binary or string comparisons.
• ExpectFloat(actual: f64)
Custom epsilon per comparison.
Advanced: Custom Epsilon
test "matrix inversion close enough" {
const actual = invertMatrix(someMatrix);
// tighter epsilon for sensitive data
try testing.expect(std.testing, testing.ExpectMat4(actual), .0001, expectedMatrix);
}
Adding New Tests
- Create
tests/your_feature_test.zig
. const testing = @import("src/testing.zig");
- Write
test "description" { … try testing.expect(…) }
. - Run all tests:
$ zig test src/testing.zig tests/your_feature_test.zig -- …flags…
Extending the Build Script
To add a custom component:
- In
build.zig
, define a new flag:const flag_myfeature = b.standardTargetOptions.*.?; // inline flag parse
- Derive a
want_myfeature
boolean alongside existingwant_*
. - Wrap your link or build step:
if (want_myfeature) linkMyFeature(b, mach_module);
- Add registration under
b.installStep
if it produces artifacts.
Local Iteration Workflow
# Build core + run fast subset of tests
$ zig build --core && zig test src/testing.zig tests/my_core_test.zig
# After adding a new example
$ zig build --examples && examples/your_example
By following these guidelines, you can efficiently build, test, and extend Mach from source.
Advanced Usage & Extensibility
Extend and optimise your application with custom render pipelines, embed the Mach IPC module, package across platforms, toggle GameMode on Linux, and profile frame rates.
Custom Render Pipelines
Leverage src/sysgpu/utils.zig
to build and manage GPU pipeline layouts and formats.
Setting Up a Pipeline Layout
const std = @import("std");
const gpu = @import("src/sysgpu/utils.zig");
pub fn createStandardPipeline(allocator: *std.mem.Allocator) !gpu.PipelineLayout {
// Define a bind group for a uniform buffer and a sampled texture
const bindGroups = [_]gpu.BindGroupLayoutDescriptor{
.{
.bindings = &[_]gpu.BindGroupLayoutBinding{
// Binding 0: uniform buffer
.{ .binding = 0, .visibility = .VERTEX, .ty = gpu.BindingType.UniformBuffer, .count = 1 },
// Binding 1: sampled texture
.{ .binding = 1, .visibility = .FRAGMENT, .ty = gpu.BindingType.SampledTexture, .count = 1 },
},
},
};
const layoutDesc = gpu.DefaultPipelineLayoutDescriptor{
.bindGroupLayouts = &bindGroups,
};
var manager = gpu.Manager.init(allocator);
return manager.createPipelineLayout(layoutDesc);
}
Key utilities:
alignUp(value, alignment)
to pad buffer sizes.findChained
for traversing Vulkan-like pNext chains.- Format enums in
gpu.FormatType
to map native texture formats.
Embedding Mach
Use the built-in Mach module (src/mach.zig
) to interact with Mach IPC objects and ports directly from Zig.
const std = @import("std");
const mach = @import("src/mach.zig");
pub fn demoMach() void {
// Initialize the Mach object store
var store = mach.Objects.init();
// Create a Mach port object
const port = store.create(.port, null) catch |err| {
std.debug.print("Failed to create Mach port: {}\n", .{err});
return;
};
// Thread-safe configuration
mach.lock(&port.mutex);
port.configure(.dontRoute);
mach.unlock(&port.mutex);
// Send a simple message
const msg = mach.Message.init(port, "hello");
port.send(msg) catch |err| {
std.debug.print("Send error: {}\n", .{err});
};
// Clean up
store.delete(port);
}
Highlights:
mach.Objects
handles creation, lookup, and deletion.mach.lock
/mach.unlock
ensure thread safety.- Message and port abstractions wrap raw Mach APIs.
Packaging Across Operating Systems
Produce portable binaries and handle optional dependencies.
Cross-Compiling with Zig
# Linux GNU
zig build -Dtarget=x86_64-linux-gnu
# macOS Darwin
zig build -Dtarget=x86_64-macos-gnu
# Windows MSI
zig build -Dtarget=x86_64-windows-gnu
Tips:
- On Linux, dynamic
libgamemode.so
loads at runtime; no link-time dependency on non-Linux. - Bundle assets and shader binaries into your release directory for each platform.
- Use Zig’s
–strip
and–release-small
flags to reduce final binary size.
Linux GameMode Toggling
Control system-wide performance mode via src/gamemode.zig
. On unsupported OSes calls become no-ops.
const gamemode = @import("src/gamemode.zig");
pub fn runLoop() void {
gamemode.start(); // hint: call before heavy work
defer gamemode.stop(); // defer ensures we leave GameMode
while (shouldContinue()) {
// Query and adapt if GameMode isn't active
if (!gamemode.isActive()) {
// e.g., reduce thread count or lower quality
}
renderFrame();
}
}
Under the hood:
.init()
dynamically loads libgamemode on Linux.- Fallback implementation on other platforms returns immediately.
- Errors log via your Zig logger; no panics.
Performance Profiling with Frequency
Regulate and measure your frame rate using src/time/Frequency.zig
.
const std = @import("std");
const freq = @import("src/time/Frequency.zig");
pub fn main() !void {
var allocator = std.heap.page_allocator;
var tracker = freq.Frequency.init(60.0); // target 60 FPS
while (shouldRun()) {
tracker.tick(); // mark frame start
updateSimulation();
renderFrame();
const sleepNs = tracker.delay(); // compute delay to maintain rate
if (sleepNs > 0) std.time.sleepNanoseconds(sleepNs) catch {};
std.debug.print("FPS: {d}\n", .{tracker.actualRate()});
}
}
- Call
tick()
once per iteration. delay()
returns the nanoseconds to sleep to hit your target.actualRate()
provides a running measurement of achieved frequency.