Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8a6d271
docs(brief): add M0.5 housekeeping brief
guysenpai Jun 1, 2026
57c6a93
docs(brief): tick off specs read for M0.5
guysenpai Jun 1, 2026
26dd01c
docs(brief): log blocker B1 on item 2 line-count gate
guysenpai Jun 1, 2026
d471468
docs(brief): log B1 cas-2 resolution and interim GO
guysenpai Jun 1, 2026
d69fe6e
refactor(render): extract captureFrameToPPM into GAL surface (M0.5)
guysenpai Jun 1, 2026
bf9b41d
test(render): cover captureFrameToPPM, add PSNR gate (M0.5)
guysenpai Jun 1, 2026
df98179
perf(ci): drop PSNR rebuild in runtime-smoke-test (M0.5 item 1)
guysenpai Jun 1, 2026
1dd061b
docs(brief): record E1 deliverables and 449-line baseline (M0.5)
guysenpai Jun 1, 2026
bc3a69d
refactor(editor): vk_blit uses vk *Raw wrappers, drop legacy marker
guysenpai Jun 1, 2026
a2de424
feat(lint): forbid the WELD_LEGACY_VK_DISPATCH marker (M0.5 item 4)
guysenpai Jun 1, 2026
09c77ca
docs(brief): record E2 audit, vk_blit migration, E1 CI-green (M0.5)
guysenpai Jun 1, 2026
93c315b
test(render): add WAW two-writers cycle repro (red) (M0.5 item 7)
guysenpai Jun 1, 2026
2be3083
fix(render): serialize WAW writers by insertion order (M0.5 item 7)
guysenpai Jun 1, 2026
67a6ff1
docs(brief): record E3 render-graph WAW fix (M0.5 item 7)
guysenpai Jun 1, 2026
73f756c
test(etch): add keyword-ident codegen repro (red) (M0.5 item 8)
guysenpai Jun 1, 2026
36b8db5
fix(etch): escape Zig-keyword idents in codegen (M0.5 item 8)
guysenpai Jun 1, 2026
8d05fd1
fix(etch): writeValueAsBytes returns TypeMismatch (M0.5 item 10)
guysenpai Jun 1, 2026
031c46e
docs(brief): record E4 etch backend (items 8 + 10) (M0.5)
guysenpai Jun 1, 2026
824aab6
fix(ipc): retry send/recv on EINTR not broken pipe (M0.5 item 9)
guysenpai Jun 1, 2026
d0ba0ad
docs(tests): mark expected warn output benign (M0.5 item 5)
guysenpai Jun 1, 2026
6b896fd
docs(brief): log E5 execution + item 5 root cause
guysenpai Jun 1, 2026
8af5386
fix(ipc): retry sendmsg/recvmsg on EINTR (M0.5 item 9)
guysenpai Jun 2, 2026
6332744
docs(brief): note sendmsg/recvmsg EINTR retry (M0.5 item 9)
guysenpai Jun 2, 2026
092c522
refactor(modules): rename main.zig roots to root.zig (M0.5 item 11)
guysenpai Jun 2, 2026
19ad1d7
docs(brief): log E6 root.zig rename + grep verification (M0.5)
guysenpai Jun 2, 2026
d6f53a2
docs(brief): apply FROZEN amendment v2 + status IN PROGRESS (M0.5)
guysenpai Jun 2, 2026
9192297
docs(brief): mark B1 resolved by amendment v2 (M0.5)
guysenpai Jun 3, 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
20 changes: 10 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,16 @@ jobs:
echo "commit it as tests/golden/smoke_test_software.ppm, then re-run." >&2
exit 0
fi
# `test-render-capture` runs ONLY tests/render/capture.zig — it
# bypasses the generic `zig build test` which pulls in every
# test in the repo (some have unrelated ReleaseSafe issues
# tracked as M0.7 housekeeping debt). Scope: PSNR regression
# gate, nothing else. The test itself raises a typed error
# when PSNR < 40 dB, so a non-zero exit propagates through
# pipefail — no separate grep needed (the previous grep on
# "PSNR" matched the test's success log line that does not
# exist, tripping exit 1 under pipefail on every passing run).
zig build test-render-capture -Doptimize=ReleaseSafe --summary all 2>&1 | tee test-output.txt
# M0.5 item 1: direct PSNR comparison of the PPM already produced
# by the previous step against the golden. `test-ppm-psnr` runs
# ONLY tests/render/ppm_psnr_compare.zig, which imports just `std`
# (no `weld_render`): it compiles in seconds and does NOT rebuild
# the render stack or re-spawn the triangle (the prior step already
# wrote out/smoke_test.ppm). Replaces the rebuild-heavy
# `test-render-capture` invocation (~3-5 min/run). The test raises a
# typed error when PSNR < 40 dB, so a non-zero exit propagates
# through pipefail.
zig build test-ppm-psnr -Doptimize=ReleaseSafe --summary all 2>&1 | tee test-output.txt

- name: Upload capture artifact
if: always()
Expand Down
229 changes: 229 additions & 0 deletions briefs/M0.5-housekeeping.md

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub fn build(b: *std.Build) void {
// audio tests and, later, by the runtime once the audio strategy
// selection wires in.
const audio_module = b.createModule(.{
.root_source_file = b.path("src/modules/audio/main.zig"),
.root_source_file = b.path("src/modules/audio/root.zig"),
.target = target,
.optimize = optimize,
});
Expand All @@ -72,7 +72,7 @@ pub fn build(b: *std.Build) void {
// `b.dependency("weld", ...).module("weld_render")` — a prerequisite of
// the `examples/triangle/` sub-project (brief §Scope).
const render_module = b.addModule("weld_render", .{
.root_source_file = b.path("src/modules/render/main.zig"),
.root_source_file = b.path("src/modules/render/root.zig"),
.target = target,
.optimize = optimize,
});
Expand Down Expand Up @@ -327,6 +327,9 @@ pub fn build(b: *std.Build) void {
.{ .path = "tests/bindings/vk_abi_test.zig" },
.{ .path = "tests/bindings/wayland_abi_test.zig", .wl_protocols = true },
.{ .path = "tests/etch/corpus_test.zig", .etch = true },
// M0.5 item 8 — Etch idents that collide with Zig keywords must
// codegen to parseable (escaped) Zig. RED before the lower.zig fix.
.{ .path = "tests/etch/keyword_ident_test.zig", .etch = true },
.{ .path = "tests/etch_interp/corpus_test.zig", .etch_interp = true },
// M0.3 — common platform layer tests.
.{ .path = "tests/platform/fs_vfs_test.zig" },
Expand Down Expand Up @@ -370,6 +373,14 @@ pub fn build(b: *std.Build) void {
// unrelated tests with current ReleaseSafe issues — tracked as
// M0.7 housekeeping debt).
.{ .path = "tests/render/capture.zig", .render = true, .dedicated_step = "test-render-capture" },
// M0.5 item 2 — GAL capture helper surface coverage (encodePpm +
// Device.captureFrameToPPM); §13 consumer test, runs on every platform.
.{ .path = "tests/render/capture_helper.zig", .render = true },
// M0.5 item 1 — direct PSNR gate reading the pre-produced
// out/smoke_test.ppm with no rebuild and no triangle re-spawn.
// std-only (no .render), so `zig build test-ppm-psnr` compiles in
// seconds and replaces the rebuild-heavy `test-render-capture` in CI.
.{ .path = "tests/render/ppm_psnr_compare.zig", .dedicated_step = "test-ppm-psnr" },
// M0.4 § Scope Post-Review — hot-reload filewatch latency < 200 ms.
// Skip if glslc absent from PATH.
.{ .path = "tests/render/shader_hot_reload.zig", .render = true },
Expand Down
72 changes: 10 additions & 62 deletions examples/triangle/src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,10 @@ fn frameClearColor(frame: u32) gal.types.ColorClear {
};
}

/// Render frame `frame_idx` into an offscreen R8G8B8A8_UNORM texture,
/// copy it through a staging buffer, and write the result as a binary
/// PPM (P6) to `path`. Used by the smoke-test capture path consumed by
/// `tests/render/capture.zig`. The `pipeline` argument is the same
/// Render frame `frame_idx` into an offscreen R8G8B8A8_UNORM texture, then
/// delegate the GPU readback + PPM write to the public GAL helper
/// `Device.captureFrameToPPM` (M0.5 item 2; cf. `gal/capture.zig`). The
/// render leaves the texture in `transfer_src` layout. The `pipeline` argument is the same
/// triangle pipeline used in the interactive loop — drawn over the
/// clear-color background so the captured PPM exercises the full
/// forward path (vertex → rasterizer → fragment → blend), not just
Expand All @@ -229,19 +229,10 @@ fn captureFrame(
const offscreen_view = try device.createTextureView(offscreen, .{ .label = "capture.view" });
defer device.destroyTextureView(offscreen_view);

const staging_bytes: u64 = @as(u64, FRAME_WIDTH) * FRAME_HEIGHT * 4;
const staging = try device.createBuffer(.{
.label = "capture.staging",
.size = staging_bytes,
.usage = .{ .copy_dst = true },
.host_visible = true,
});
defer device.destroyBuffer(staging);

const fence = try device.createFence(false);
defer device.destroyFence(fence);

const enc = try device.createCommandEncoder("capture");
const enc = try device.createCommandEncoder("capture.render");
defer device.destroyCommandEncoder(enc);

var pass = try enc.beginRenderPass(.{
Expand All @@ -258,62 +249,19 @@ fn captureFrame(
pass.setScissor(0, 0, FRAME_WIDTH, FRAME_HEIGHT);
pipeline.draw(&pass);
pass.end();

enc.copyTextureToBuffer(
.{ .texture = offscreen, .aspect = .color },
.{ .buffer = staging, .bytes_per_row = FRAME_WIDTH * 4 },
.{ .width = FRAME_WIDTH, .height = FRAME_HEIGHT },
);
enc.finish();

try device.submit(enc, .{ .fence = fence });
try device.waitFence(fence, std.math.maxInt(u64));

const rgba = try device.mapBuffer(staging);
defer device.unmapBuffer(staging);

try writePPM(allocator, io, path, rgba);
// Readback + PPM encode/write now live on the public GAL surface
// (`Device.captureFrameToPPM`, M0.5 item 2). The render above left the
// offscreen texture in `transfer_src` layout, the contract the helper
// expects (cf. `gal/capture.zig`).
try device.captureFrameToPPM(allocator, io, offscreen, FRAME_WIDTH, FRAME_HEIGHT, path);
log.info("captured frame {d} -> {s}", .{ frame_idx, path });
}

/// PPM P6 writer. Strips alpha from the RGBA8 source (drops every fourth
/// byte). `path`'s parent directory is created if missing. Binary P6 format:
/// "P6\n<W> <H>\n255\n" followed by W*H*3 bytes of RGB.
fn writePPM(
allocator: std.mem.Allocator,
io: std.Io,
path: []const u8,
rgba: []const u8,
) !void {
if (std.fs.path.dirname(path)) |dir| {
std.Io.Dir.cwd().createDirPath(io, dir) catch |e| switch (e) {
error.PathAlreadyExists => {},
else => return e,
};
}

var file = try std.Io.Dir.cwd().createFile(io, path, .{ .truncate = true });
defer file.close(io);

var buf: [4096]u8 = undefined;
var writer = file.writer(io, &buf);
const w = &writer.interface;

try w.print("P6\n{d} {d}\n255\n", .{ FRAME_WIDTH, FRAME_HEIGHT });

const pixel_count = @as(usize, FRAME_WIDTH) * FRAME_HEIGHT;
var rgb = try allocator.alloc(u8, pixel_count * 3);
defer allocator.free(rgb);
var i: usize = 0;
while (i < pixel_count) : (i += 1) {
rgb[i * 3 + 0] = rgba[i * 4 + 0];
rgb[i * 3 + 1] = rgba[i * 4 + 1];
rgb[i * 3 + 2] = rgba[i * 4 + 2];
}
try w.writeAll(rgb);
try w.flush();
}

fn runVulkan(allocator: std.mem.Allocator, io: std.Io, args: Args) !void {
var window = try window_mod.Window.create(allocator, .{
.title = "Weld Triangle (M0.4)",
Expand Down
45 changes: 37 additions & 8 deletions src/core/ipc/transport_posix.zig
Original file line number Diff line number Diff line change
Expand Up @@ -224,16 +224,28 @@ pub const Backend = struct {
var offset: usize = 0;
while (offset < bytes.len) {
const n = sys.write(self.fd, bytes.ptr + offset, bytes.len - offset);
if (n < 0) return error.BrokenPipe;
if (n < 0) switch (std.c.errno(n)) {
// A signal interrupted the write before any byte moved —
// retry the same chunk rather than mis-report a broken
// pipe (mirrors std.posix's own `.INTR => continue`).
.INTR => continue,
else => return error.BrokenPipe,
};
if (n == 0) return error.BrokenPipe;
offset += @intCast(n);
}
}

pub fn recv(self: *Backend, buffer: []u8) Error!usize {
const n = sys.read(self.fd, buffer.ptr, buffer.len);
if (n < 0) return error.BrokenPipe;
return @intCast(n);
while (true) {
const n = sys.read(self.fd, buffer.ptr, buffer.len);
if (n < 0) switch (std.c.errno(n)) {
// Interrupted by a signal with nothing read yet — retry.
.INTR => continue,
else => return error.BrokenPipe,
};
return @intCast(n);
}
}

pub fn sendWithHandles(
Expand Down Expand Up @@ -273,8 +285,17 @@ pub const Backend = struct {
.msg_flags = 0,
};

const n = sys.sendmsg(self.fd, &msg, MSG_NOSIGNAL);
if (n < 0) return error.BrokenPipe;
while (true) {
const n = sys.sendmsg(self.fd, &msg, MSG_NOSIGNAL);
if (n < 0) switch (std.c.errno(n)) {
// Interrupted before any byte/ancillary left the socket —
// retry the whole message. Nothing was sent, so the passed
// fds are not duplicated. (Same EINTR contract as `send`.)
.INTR => continue,
else => return error.BrokenPipe,
};
break;
}
}

pub fn recvWithHandles(
Expand All @@ -301,8 +322,16 @@ pub const Backend = struct {
.msg_flags = 0,
};

const n = sys.recvmsg(self.fd, &msg, 0);
if (n < 0) return error.BrokenPipe;
var n: isize = undefined;
while (true) {
n = sys.recvmsg(self.fd, &msg, 0);
if (n < 0) switch (std.c.errno(n)) {
// Interrupted with nothing received yet — retry.
.INTR => continue,
else => return error.BrokenPipe,
};
break;
}

var handle_count: usize = 0;
if (msg.msg_controllen >= @sizeOf(CmsgHdr) and handles_out.len > 0) {
Expand Down
16 changes: 8 additions & 8 deletions src/editor/vk_blit.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
//! S6 editor Vulkan blit renderer.
//!
//! WELD_LEGACY_VK_DISPATCH — pre-M0.4 code, migration to the Vulkan GAL
//! (cf. `src/modules/render/gal/vulkan/`) tracked as Phase 1+ debt.
//! The `no_device_dispatch_outside_gal` linter rule is suspended for
//! this file via the marker above.
//! Uses the idiomatic `vk.Device`/`vk.Queue` wrappers throughout, including
//! the `*Raw` acquire/present variants (`acquireNextImageKHRRaw`,
//! `presentKHRRaw`) where the raw `Result` is needed for swapchain-resize
//! handling — no `vk.device_dispatch.*` access remains, so the
//! `no_device_dispatch_outside_gal` rule holds with no grandfather marker.
//!
//! Opens a window-sized swapchain and a fullscreen-quad pipeline that
//! samples the runtime-written viewport shm framebuffer (1280×720
Expand Down Expand Up @@ -952,12 +953,11 @@ pub fn drawFrame(r: *Renderer) vk.Error!bool {
const cur = r.current_frame;
try r.device.waitForFences(&.{r.in_flight[cur]}, 1, std.math.maxInt(u64));

// Use the raw dispatch for `vkAcquireNextImageKHR` so we can
// Use the `*Raw` acquire wrapper (`acquireNextImageKHRRaw`) so we can
// see `suboptimal_khr` and `error_out_of_date_khr` directly
// — the wrapped Device method folds suboptimal into success.
var img_index: u32 = 0;
const acquire_result = vk.device_dispatch.vkAcquireNextImageKHR(
r.device,
const acquire_result = r.device.acquireNextImageKHRRaw(
r.swapchain,
std.math.maxInt(u64),
r.image_available[cur],
Expand Down Expand Up @@ -1117,7 +1117,7 @@ pub fn drawFrame(r: *Renderer) vk.Error!bool {
.p_image_indices = @ptrCast(&img_index),
.p_results = @ptrCast(&per_swapchain_result),
};
const present_call = vk.device_dispatch.vkQueuePresentKHR(r.queue, &present);
const present_call = r.queue.presentKHRRaw(&present);
switch (present_call) {
.success => {},
.suboptimal_khr => r.swapchain_dirty = true,
Expand Down
Loading
Loading