Skip to content

Contributing

MichaelFisher1997 edited this page Jan 5, 2026 · 1 revision

Contributing

Guidelines for contributing to ZigCraft, including code style, development workflow, and common implementation tasks.


Development Environment

Prerequisites

The project uses Nix for dependency management. All commands must be wrapped:

nix develop --command <command>

Build Commands

Command Description
nix develop --command zig build Build project
nix develop --command zig build run Build and run
nix develop --command zig build test Run unit tests
nix develop --command zig build -Doptimize=ReleaseFast Release build
nix develop --command zig fmt src/ Format code
nix develop --command zig build check Fast type-check

Clean Build

rm -rf zig-out/ .zig-cache/

Code Style

Naming Conventions

Element Style Example
Types/Structs/Enums PascalCase RenderSystem, BlockType
Functions/Variables snake_case init_renderer, chunk_x
Constants/Globals SCREAMING_SNAKE_CASE MAX_CHUNKS, CHUNK_SIZE
Files snake_case.zig chunk_mesh.zig

Import Order

// 1. Standard library
const std = @import("std");
const Allocator = std.mem.Allocator;

// 2. C imports (always via c.zig)
const c = @import("../c.zig").c;

// 3. Local modules (relative paths)
const Vec3 = @import("../math/vec3.zig").Vec3;
const log = @import("../engine/core/log.zig");

Memory Management

// Functions accepting heap allocation MUST take allocator
pub fn init(allocator: Allocator, ...) !*Self {
    const self = try allocator.create(Self);
    errdefer allocator.destroy(self);
    
    self.* = .{
        .data = try allocator.alloc(u8, size),
        // ...
    };
    errdefer allocator.free(self.data);
    
    return self;
}

pub fn deinit(self: *Self) void {
    self.allocator.free(self.data);
    self.allocator.destroy(self);
}

Error Handling

// Propagate with try
const result = try riskyOperation();

// Define subsystem error sets
pub const RhiError = error{
    ShaderCompilationFailed,
    OutOfMemory,
    DeviceLost,
};

// Log errors
log.log.err("Failed to create buffer: {}", .{err});

Documentation

//! Module-level documentation (top of file)
//! Describes the module's purpose and usage.

/// Public API documentation
/// Describes function behavior, parameters, and return value.
pub fn publicFunction(param: u32) !Result {
    // ...
}

Type Patterns

GPU Resource Handles

pub const BufferHandle = u32;
pub const TextureHandle = u32;
pub const ShaderHandle = u32;

Packed Data

pub const PackedLight = packed struct {
    block_light: u4 = 0,
    sky_light: u4 = 0,
};

GPU-Shared Structs

pub const Vertex = extern struct {
    pos: [3]f32,
    color: [3]f32,
    normal: [3]f32,
    uv: [2]f32,
    tile_id: u32,
    skylight: u8,
    blocklight: u8,
};

Coordinate Systems

Coordinate Type Description
World i32 Global block position
Chunk i32 @divFloor(world, 16)
Local u32 0-15 (X/Z), 0-255 (Y)

Conversion Functions

pub fn worldToChunk(world_coord: i32) i32 {
    return @divFloor(world_coord, 16);
}

pub fn worldToLocal(world_coord: i32) u32 {
    return @intCast(@mod(world_coord, 16));
}

Common Tasks

Adding a New Block Type

  1. Add enum entry in src/world/block.zig:

    pub const BlockType = enum(u8) {
        // ...existing blocks...
        my_new_block = 40,
    };
  2. Register properties:

    pub fn isSolid(self: BlockType) bool {
        return switch (self) {
            .my_new_block => true,
            // ...
        };
    }
    
    pub fn isTransparent(self: BlockType) bool {
        return switch (self) {
            .my_new_block => false,
            // ...
        };
    }
    
    pub fn getColor(self: BlockType) [3]f32 {
        return switch (self) {
            .my_new_block => .{ 0.8, 0.5, 0.2 },
            // ...
        };
    }
    
    pub fn getLightEmission(self: BlockType) u4 {
        return switch (self) {
            .my_new_block => 0,  // or 1-15 if emissive
            // ...
        };
    }
  3. Add texture in src/engine/graphics/texture_atlas.zig:

    pub const TILE_MY_BLOCK = 40;
    
    // In getTilesForBlock():
    .my_new_block => .{ .top = TILE_MY_BLOCK, .bottom = TILE_MY_BLOCK, .side = TILE_MY_BLOCK },
  4. Update mesh logic if special rendering needed in src/world/chunk_mesh.zig

Modifying Shaders

  1. GLSL sources in assets/shaders/vulkan/
  2. SPIR-V validated during zig build test
  3. Uniform names must match exactly between shader and RHI

Adding Unit Tests

// In src/tests.zig
test "Vec3 normalize" {
    const v = Vec3.init(3, 4, 0);
    const n = v.normalize();
    
    try std.testing.expectApproxEqAbs(n.x, 0.6, 0.001);
    try std.testing.expectApproxEqAbs(n.y, 0.8, 0.001);
    try std.testing.expectApproxEqAbs(n.z, 0.0, 0.001);
}

Threading Guidelines

Worker Thread Rules

  • NEVER call RHI or windowing from worker threads
  • Use Mutex for shared state synchronization
  • Use pin()/unpin() when passing chunks to background jobs

GPU Upload Pattern

// Worker thread: prepare data
chunk.pending_vertices = buildMesh();

// Main thread: upload to GPU
while (upload_queue.pop()) |chunk| {
    chunk.upload(vertex_allocator);
}

Verification Checklist

Before submitting changes:

  • zig build test passes (unit tests + shader validation)
  • zig build -Doptimize=ReleaseFast compiles
  • zig fmt src/ applied
  • Visual verification for graphics changes
  • Performance profiling for hot paths

Code Quality

  • Write self-documenting code
  • Use comments to explain why, not what
  • Follow Zig idioms: init/deinit pairs, explicit allocators
  • Add tests for new utility, math, or worldgen logic

Performance Tips

  • Chunk mesh building runs on worker threads
  • Avoid allocations in hot paths
  • Use packed structs for large arrays
  • Profile with ReleaseFast before optimizing
  • Maximum 32 GPU uploads per frame

Project Structure

See Architecture for detailed module organization.


Source: AGENTS.md | Last updated: January 2026

Clone this wiki locally