Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
4e4b5cc
refactor(rhi): decouple RHI into subsystems
MichaelFisher1997 Jan 19, 2026
fc85db1
fix(rhi): address review comments - subsystem decoupling improvements
MichaelFisher1997 Jan 19, 2026
f4eba9a
fix(rhi): address code review issues
MichaelFisher1997 Jan 19, 2026
5385021
fix: resolve compilation errors in rhi_vulkan.zig and utils
MichaelFisher1997 Jan 19, 2026
4004853
Merge branch 'main' into Decouple-RHI
MichaelFisher1997 Jan 19, 2026
050f333
fix(rhi): address code review and stabilize command buffer lifecycle
MichaelFisher1997 Jan 20, 2026
f28c5aa
Merge remote-tracking branch 'origin/main' into Decouple-RHI
MichaelFisher1997 Jan 20, 2026
d3ffb61
fix(rhi): address code review and CI cache issues
MichaelFisher1997 Jan 20, 2026
c64dfd5
fix(rhi): resolve resource leaks and standardize error handling
MichaelFisher1997 Jan 20, 2026
d8510f6
ci: downgrade blacksmith runners to 2vcpu-ubuntu-2204
MichaelFisher1997 Jan 20, 2026
d6af768
fix(rhi): address remaining review items and standardize resource cre…
MichaelFisher1997 Jan 20, 2026
f182ce6
fix(rhi): resolve critical UBO leak and staging buffer null ptr risks
MichaelFisher1997 Jan 20, 2026
b7b829f
fix(rhi): remove redundant null checks in resource manager
MichaelFisher1997 Jan 20, 2026
5043ca7
ci: add headless Wayland compositor (Weston) for integration tests
MichaelFisher1997 Jan 20, 2026
e534a97
fix(vulkan): resolve race conditions and GPU hangs
MichaelFisher1997 Jan 20, 2026
5c00b24
ci: add automated world-load smoke test
MichaelFisher1997 Jan 20, 2026
35a9b31
ci: switch runners to blacksmith-2vcpu-ubuntu-2404 to fix glibc mismatch
MichaelFisher1997 Jan 20, 2026
7de438f
ci: enable Vulkan software rendering (Lavapipe) for smoke tests
MichaelFisher1997 Jan 20, 2026
73c9fdc
fix(engine): resolve init memory leaks and audio system crashes
MichaelFisher1997 Jan 20, 2026
fb9d186
fix(vulkan): resolve deadlock and clean up RHI implementation
MichaelFisher1997 Jan 20, 2026
3d0080e
fix(vulkan): resolve RHI deadlocks and improve thread safety
MichaelFisher1997 Jan 20, 2026
56e0584
chore(debug): add debug logging for G-pass and main pass
MichaelFisher1997 Jan 20, 2026
847062e
fix(debug): add missing arguments to std.log.err
MichaelFisher1997 Jan 20, 2026
207d7e3
chore(debug): add debug logging to FrameManager.endFrame and rhi_vulk…
MichaelFisher1997 Jan 20, 2026
d83a30b
fix(vulkan): protect vkQueuePresentKHR with device mutex
MichaelFisher1997 Jan 20, 2026
e2950e8
fix(debug): add logging and null checks to SwapchainPresenter.present
MichaelFisher1997 Jan 21, 2026
f219d83
fix(vulkan): dynamically load vkQueuePresentKHR to avoid NULL symbol …
MichaelFisher1997 Jan 21, 2026
47d1e96
chore(debug): log vkQueuePresentKHR function pointer
MichaelFisher1997 Jan 21, 2026
296feb9
fix(ci): skip presentation and limit frames in smoke test to avoid dr…
MichaelFisher1997 Jan 21, 2026
49ef7d8
fix(vulkan): implement ZIGCRAFT_SKIP_PRESENT logic in SwapchainPresenter
MichaelFisher1997 Jan 21, 2026
9e02487
fix(vulkan): add vkDeviceWaitIdle when skipping present to prevent dr…
MichaelFisher1997 Jan 21, 2026
22da85d
fix(vulkan): use vkQueueWaitIdle instead of vkDeviceWaitIdle when ski…
MichaelFisher1997 Jan 21, 2026
9798d7c
fix(vulkan): don't signal render_finished_semaphore when skipping pre…
MichaelFisher1997 Jan 21, 2026
e8b243f
chore(debug): add logging and fence wait for skip_present
MichaelFisher1997 Jan 21, 2026
c3d7716
chore(debug): even more verbose logging for smoke test
MichaelFisher1997 Jan 21, 2026
f1512e2
fix(ci): reduce smoke test to 1 frame and enable safe mode to avoid c…
MichaelFisher1997 Jan 21, 2026
c04fe06
fix(vulkan): use vkWaitForFences instead of vkDeviceWaitIdle when ski…
MichaelFisher1997 Jan 21, 2026
f472904
fix(vulkan): avoid vkDeviceWaitIdle when skipping present in headless…
MichaelFisher1997 Jan 21, 2026
01dfb3e
fix(vulkan): completely decouple from swapchain for headless smoke tests
MichaelFisher1997 Jan 21, 2026
f28a673
fix(vulkan): implement robust offscreen headless mode for smoke tests
MichaelFisher1997 Jan 21, 2026
fcc17d2
fix(vulkan): fix faulty null check in presenter and add shutdown diag…
MichaelFisher1997 Jan 21, 2026
5cf9899
fix(vulkan): implement validation-only dry run for smoke tests
MichaelFisher1997 Jan 21, 2026
9c169af
fix(vulkan): restore presentation info for local runs and improve err…
MichaelFisher1997 Jan 21, 2026
d997d37
fix(vulkan): fix resource leak on shutdown by manually destroying int…
MichaelFisher1997 Jan 21, 2026
27a67e1
chore: remove verbose debug logs
MichaelFisher1997 Jan 21, 2026
741a34b
chore: add debug log for dry_run detection
MichaelFisher1997 Jan 21, 2026
05cc63d
fix(ci): use compile-time option for headless mode to guarantee dry-r…
MichaelFisher1997 Jan 21, 2026
ebaa9d6
fix(ci): use correct build option syntax and add debug log
MichaelFisher1997 Jan 21, 2026
d32d837
fix(ci): imply dry-run headless mode when smoke-test is enabled
MichaelFisher1997 Jan 22, 2026
b959d95
fix(ci): restore explicit skip-present control for local smoke test s…
MichaelFisher1997 Jan 22, 2026
2863e72
fix(render): align reverse-Z shadow sampling and validation guards
MichaelFisher1997 Jan 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
permissions:
contents: read
id-token: write
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
permissions:
contents: read
id-token: write
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
Expand All @@ -72,12 +72,14 @@ jobs:

- name: Run unit tests
run: nix develop --command zig build test
env:
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache-global

integration-test:
permissions:
contents: read
id-token: write
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
Expand All @@ -92,9 +94,37 @@ jobs:
restore-prefixes-first-match: nix-${{ runner.os }}-
paths: ~/.cache/nix

- name: Start headless Wayland compositor
run: |
mkdir -p /tmp/runtime-runner
chmod 700 /tmp/runtime-runner
export XDG_RUNTIME_DIR=/tmp/runtime-runner
nix develop --command weston --socket=headless --backend=headless-backend.so --width=1280 --height=720 &
echo "WAYLAND_DISPLAY=headless" >> $GITHUB_ENV
echo "XDG_RUNTIME_DIR=/tmp/runtime-runner" >> $GITHUB_ENV
sleep 5 # Wait for Weston to start up properly

- name: Run integration smoke test
env:
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache-global
run: |
nix develop --command zig build test-integration

- name: Run world load smoke test (headless)
env:
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache-global
XDG_RUNTIME_DIR: /tmp/runtime-runner
WAYLAND_DISPLAY: headless
VK_ICD_FILENAMES: /run/opengl-driver/share/vulkan/icd.d/lvp_icd.x86_64.json
# Skip presentation and use dry-run mode to avoid driver crashes in CI
# Using build option -Dskip-present=true to guarantee it's baked in
# 3 frames is enough to test multi-frame synchronization logic in dry-run
ZIGCRAFT_SMOKE_FRAMES: "3"
ZIGCRAFT_SAFE_MODE: "1"
run: |
mkdir -p $XDG_RUNTIME_DIR
xvfb-run -a nix develop --command zig build test-integration
# Find the actual path to the mesa driver and validation layers in the nix store
LVP_PATH=$(nix build --no-link --print-out-paths nixpkgs#mesa.drivers)/share/vulkan/icd.d/lvp_icd.x86_64.json
LAYER_PATH=$(nix build --no-link --print-out-paths nixpkgs#vulkan-validation-layers)/share/vulkan/explicit_layer.d
export VK_ICD_FILENAMES=$LVP_PATH
export VK_LAYER_PATH=$LAYER_PATH
nix develop --command zig build run -Dsmoke-test=true -Dskip-present=true
2 changes: 1 addition & 1 deletion .github/workflows/opencode-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
opencode:
# Don't run on draft PRs; do run when they become ready_for_review.
if: ${{ github.event.pull_request.draft == false }}
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
id-token: write
contents: write
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/opencode-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

jobs:
triage:
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
id-token: write
contents: write
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/opencode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
id-token: write
contents: write
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/repo-automation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ permissions:
jobs:
label-pr:
if: github.event_name == 'pull_request_target'
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Label PR
uses: actions/labeler@v6
Expand All @@ -25,7 +25,7 @@ jobs:

label-issue:
if: github.event_name == 'issues'
runs-on: blacksmith-4vcpu-ubuntu-2404
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
zig-cache/
.zig-cache/
.zig-cache-new/
zig-out/
blocks-temp/
libs/zig-image/
result
.direnv/
*-profile*
test_output.txt
.env
.env.*
!.env.example
Expand Down
2 changes: 1 addition & 1 deletion assets/shaders/vulkan/sky.frag
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ float getVolShadow(vec3 p, float viewDepth) {

if (proj.x < 0.0 || proj.x > 1.0 || proj.y < 0.0 || proj.y > 1.0 || proj.z > 1.0) return 1.0;

return texture(uShadowMaps, vec4(proj.xy, float(layer), proj.z - 0.002));
return texture(uShadowMaps, vec4(proj.xy, float(layer), proj.z + 0.002));
}

// Raymarched God Rays (Phase 4)
Expand Down
Binary file modified assets/shaders/vulkan/sky.frag.spv
Binary file not shown.
6 changes: 3 additions & 3 deletions assets/shaders/vulkan/terrain.frag
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ float findBlocker(vec2 uv, float zReceiver, int layer) {
for (int j = -1; j <= 1; j++) {
vec2 offset = vec2(i, j) * searchRadius;
float depth = texture(uShadowMapsRegular, vec3(uv + offset, float(layer))).r;
if (depth < zReceiver) {
if (depth > zReceiver) {
blockerDepthSum += depth;
numBlockers++;
}
Expand Down Expand Up @@ -168,7 +168,7 @@ float calculateShadow(vec3 fragPosWorld, float nDotL, int layer) {
float avgBlockerDepth = findBlocker(projCoords.xy, currentDepth, layer);
if (avgBlockerDepth == -1.0) return 0.0; // No blockers

float penumbraSize = (currentDepth - avgBlockerDepth) / avgBlockerDepth;
float penumbraSize = (avgBlockerDepth - currentDepth) / max(avgBlockerDepth, 0.0001);
float filterRadius = penumbraSize * 0.01; // Adjust multiplier for softness
filterRadius = clamp(filterRadius, 0.0005, 0.005); // Min/max blur

Expand Down Expand Up @@ -196,7 +196,7 @@ float getVolShadow(vec3 p, float viewDepth) {

if (proj.x < 0.0 || proj.x > 1.0 || proj.y < 0.0 || proj.y > 1.0 || proj.z > 1.0) return 1.0;

return texture(uShadowMaps, vec4(proj.xy, float(layer), proj.z - 0.002));
return texture(uShadowMaps, vec4(proj.xy, float(layer), proj.z + 0.002));
}

// Raymarched God Rays (Phase 4)
Expand Down
Binary file modified assets/shaders/vulkan/terrain.frag.spv
Binary file not shown.
6 changes: 6 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ pub fn build(b: *std.Build) void {
const enable_debug_shadows = b.option(bool, "debug_shadows", "Enable debug shadow visualization resources") orelse false;
options.addOption(bool, "debug_shadows", enable_debug_shadows);

const smoke_test = b.option(bool, "smoke-test", "Enable automated smoke test mode (auto-loads world and exits)") orelse false;
options.addOption(bool, "smoke_test", smoke_test);

const skip_present = b.option(bool, "skip-present", "Skip presentation (headless mode) to avoid driver crashes") orelse false;
options.addOption(bool, "skip_present", skip_present);

const zig_math = b.createModule(.{
.root_source_file = b.path("libs/zig-math/math.zig"),
.target = target,
Expand Down
2 changes: 2 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@
pkgs.zls
pkgs.pkg-config
pkgs.glslang
pkgs.weston
];

buildInputs = [
pkgs.sdl3
pkgs.vulkan-loader
pkgs.vulkan-headers
pkgs.vulkan-validation-layers
pkgs.mesa.drivers
];

shellHook = ''
Expand Down
33 changes: 33 additions & 0 deletions src/engine/audio/backend.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,36 @@ pub const IAudioBackend = struct {
self.vtable.setCategoryVolume(self.ptr, category, volume);
}
};

pub const DummyAudioBackend = struct {
backend: IAudioBackend,

pub fn init() DummyAudioBackend {
return .{
.backend = .{
.ptr = undefined,
.vtable = &VTABLE,
},
};
}

fn update(_: *anyopaque) void {}
fn setListener(_: *anyopaque, _: Vec3, _: Vec3, _: Vec3) void {}
fn playSound(_: *anyopaque, _: *const types.SoundData, _: types.PlayConfig) types.VoiceHandle {
return .{ .id = 0, .generation = 0 };
}
fn stopVoice(_: *anyopaque, _: types.VoiceHandle) void {}
fn stopAll(_: *anyopaque) void {}
fn setMasterVolume(_: *anyopaque, _: f32) void {}
fn setCategoryVolume(_: *anyopaque, _: types.SoundCategory, _: f32) void {}

const VTABLE = IAudioBackend.VTable{
.update = update,
.setListener = setListener,
.playSound = playSound,
.stopVoice = stopVoice,
.stopAll = stopAll,
.setMasterVolume = setMasterVolume,
.setCategoryVolume = setCategoryVolume,
};
};
44 changes: 30 additions & 14 deletions src/engine/audio/system.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const log = @import("../core/log.zig");

pub const AudioSystem = struct {
allocator: std.mem.Allocator,
backend: *sdl_backend.SDLAudioBackend,
backend: backend_pkg.IAudioBackend,
backend_ptr: ?*anyopaque = null, // To free if we allocated it
dummy_backend: ?backend_pkg.DummyAudioBackend = null,
manager: manager_pkg.SoundManager,

// Config
Expand All @@ -20,16 +22,26 @@ pub const AudioSystem = struct {
pub fn init(allocator: std.mem.Allocator) !*AudioSystem {
log.log.info("Initializing Audio System...", .{});

const config = sdl_backend.AudioConfig{};
const backend_inst = try sdl_backend.SDLAudioBackend.create(allocator, config);

const self = try allocator.create(AudioSystem);
errdefer allocator.destroy(self);

self.* = .{
.allocator = allocator,
.backend = backend_inst,
.backend = undefined,
.manager = manager_pkg.SoundManager.init(allocator),
};

const config = sdl_backend.AudioConfig{};
if (sdl_backend.SDLAudioBackend.create(allocator, config)) |backend_inst| {
self.backend = backend_inst.backend;
self.backend_ptr = @ptrCast(backend_inst);
} else |err| {
log.log.warn("Failed to initialize SDL Audio Backend: {}. Falling back to dummy backend.", .{err});
self.dummy_backend = backend_pkg.DummyAudioBackend.init();
self.backend = self.dummy_backend.?.backend;
self.enabled = false;
}

// Create some default test sounds
_ = try self.manager.createTestSound("test_tone");

Expand All @@ -45,14 +57,17 @@ pub const AudioSystem = struct {
pub fn deinit(self: *AudioSystem) void {
self.stopAll();
self.manager.deinit();
self.backend.destroy();
if (self.backend_ptr) |ptr| {
const backend_inst: *sdl_backend.SDLAudioBackend = @ptrCast(@alignCast(ptr));
backend_inst.destroy();
}
self.allocator.destroy(self);
}

/// Update the audio backend. Should be called once per frame.
pub fn update(self: *AudioSystem) void {
if (!self.enabled) return;
self.backend.backend.update();
self.backend.update();
}

/// Update the listener's 3D position and orientation.
Expand All @@ -61,23 +76,23 @@ pub const AudioSystem = struct {
/// listener_up: Up vector (normalized).
pub fn setListener(self: *AudioSystem, listener_pos: Vec3, listener_fwd: Vec3, listener_up: Vec3) void {
if (!self.enabled) return;
self.backend.backend.setListener(listener_pos, listener_fwd, listener_up);
self.backend.setListener(listener_pos, listener_fwd, listener_up);
}

/// Set the master volume (applied to all sounds).
/// volume: 0.0 to 1.0
pub fn setMasterVolume(self: *AudioSystem, volume: f32) void {
if (!self.enabled) return;
const clamped = std.math.clamp(volume, 0.0, 1.0);
self.backend.backend.setMasterVolume(clamped);
self.backend.setMasterVolume(clamped);
}

/// Set volume for a specific category (Music, SFX, Ambient).
/// volume: 0.0 to 1.0
pub fn setCategoryVolume(self: *AudioSystem, category: types.SoundCategory, volume: f32) void {
if (!self.enabled) return;
const clamped = std.math.clamp(volume, 0.0, 1.0);
self.backend.backend.setCategoryVolume(category, clamped);
self.backend.setCategoryVolume(category, clamped);
}

/// Play a sound by name (2D, no spatialization).
Expand All @@ -91,7 +106,7 @@ pub const AudioSystem = struct {
}

if (self.manager.getSound(handle)) |sound| {
return self.backend.backend.playSound(sound, .{});
return self.backend.playSound(sound, .{});
}
return null;
}
Expand All @@ -104,7 +119,7 @@ pub const AudioSystem = struct {
if (handle == types.InvalidSoundHandle) return null;

if (self.manager.getSound(handle)) |sound| {
return self.backend.backend.playSound(sound, .{
return self.backend.playSound(sound, .{
.is_spatial = true,
.position = pos,
});
Expand All @@ -115,11 +130,12 @@ pub const AudioSystem = struct {
/// Stop a specific voice handle.
pub fn stop(self: *AudioSystem, handle: types.VoiceHandle) void {
if (!self.enabled) return;
self.backend.backend.stopVoice(handle);
self.backend.stopVoice(handle);
}

/// Stop all currently playing sounds.
pub fn stopAll(self: *AudioSystem) void {
self.backend.stopAllVoices();
if (!self.enabled) return;
self.backend.stopAll();
}
};
6 changes: 3 additions & 3 deletions src/engine/ecs/systems/render.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ pub const RenderSystem = struct {
rhi: *RHI,
missing_transform_logged: bool,

pub fn init(rhi: *RHI) RenderSystem {
const buffer = rhi.*.createBuffer(@sizeOf(@TypeOf(wireframe.line_vertices)), .vertex);
rhi.*.uploadBuffer(buffer, std.mem.asBytes(&wireframe.line_vertices));
pub fn init(rhi: *RHI) !RenderSystem {
const buffer = try rhi.*.createBuffer(@sizeOf(@TypeOf(wireframe.line_vertices)), .vertex);
try rhi.*.uploadBuffer(buffer, std.mem.asBytes(&wireframe.line_vertices));

return .{
.buffer_handle = buffer,
Expand Down
8 changes: 4 additions & 4 deletions src/engine/graphics/atmosphere_system.zig
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ pub const AtmosphereSystem = struct {
};
const cloud_indices = [_]u16{ 0, 1, 2, 0, 2, 3 };

self.cloud_vbo = rhi_instance.createBuffer(@sizeOf(@TypeOf(cloud_vertices)), .vertex);
self.cloud_ebo = rhi_instance.createBuffer(@sizeOf(@TypeOf(cloud_indices)), .index);
self.cloud_vbo = try rhi_instance.createBuffer(@sizeOf(@TypeOf(cloud_vertices)), .vertex);
self.cloud_ebo = try rhi_instance.createBuffer(@sizeOf(@TypeOf(cloud_indices)), .index);

rhi_instance.uploadBuffer(self.cloud_vbo, std.mem.asBytes(&cloud_vertices));
rhi_instance.uploadBuffer(self.cloud_ebo, std.mem.asBytes(&cloud_indices));
try rhi_instance.uploadBuffer(self.cloud_vbo, std.mem.asBytes(&cloud_vertices));
try rhi_instance.uploadBuffer(self.cloud_ebo, std.mem.asBytes(&cloud_indices));

return self;
}
Expand Down
Loading
Loading