a73x

0ce56438

scenario: guard scale change + tighten captureCb errdefer

a73x   2026-04-19 13:39

Fixes two pre-fixture bugs:
  - Runtime scale change in scenario mode would leave rc.cell_w
    etc. stale → silent golden drift. Now fails fast with a
    clear error and exit code 6.
  - captureCb's readback allocation only had a narrow errdefer;
    any error after golden diff would leak img.pixels. Now the
    errdefer spans the whole function.

Plus a tidy: `try` in place of `catch |err| return err`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git a/src/main.zig b/src/main.zig
index 22028e4..cc6cdfc 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -237,9 +237,15 @@ pub fn runScenarios(alloc: std.mem.Allocator, argv: []const [:0]const u8) !u8 {
    };
    defer rc.deinit();

    runTerminal(alloc, &rc) catch |err| {
        std.debug.print("scenario {s}: runtime error: {s}\n", .{ scenario_name, @errorName(err) });
        return @intFromEnum(scenario_runtime.ExitCode.other_error);
    runTerminal(alloc, &rc) catch |err| switch (err) {
        error.ScenarioScaleChangeNotSupported => {
            // already printed; return exit 6 (other render error)
            return @intFromEnum(scenario_runtime.ExitCode.other_error);
        },
        else => {
            std.debug.print("scenario {s}: runtime error: {s}\n", .{ scenario_name, @errorName(err) });
            return @intFromEnum(scenario_runtime.ExitCode.other_error);
        },
    };

    if (rc.tick_fatal) |err| {
@@ -608,6 +614,10 @@ fn runTerminal(alloc: std.mem.Allocator, tick_ctx: ?*scenario_runtime.RunContext
        if (!render_pending) continue;

        if (scale_pending) {
            if (tick_ctx) |_| {
                std.debug.print("scenario: runtime scale change not supported; please pin the compositor output to scale 1 (or re-run on a scale-1 monitor).\n", .{});
                return error.ScenarioScaleChangeNotSupported;
            }
            vk_sync.waitIdleForShutdown(ctx.vkd, ctx.device);

            geom = try rebuildFaceForScale(
diff --git a/src/scenario_runtime.zig b/src/scenario_runtime.zig
index 4b3152f..a66c361 100644
--- a/src/scenario_runtime.zig
+++ b/src/scenario_runtime.zig
@@ -155,10 +155,10 @@ pub fn captureCb(ctx: *anyopaque, label: []const u8) anyerror!png.Image {

    // --- readback ---
    const pixels = try rc.alloc.alloc(u8, @as(usize, rc.px_w) * rc.px_h * 4);
    errdefer rc.alloc.free(pixels);
    try rctx.readbackOffscreen(offscreen, pixels);

    const img: png.Image = .{ .width = rc.px_w, .height = rc.px_h, .pixels = pixels };
    errdefer rc.alloc.free(img.pixels); // covers all subsequent error paths

    // --- write out/<scenario>/<label>.png ---
    const out_dir = try std.fmt.allocPrint(
@@ -230,7 +230,7 @@ pub fn captureCb(ctx: *anyopaque, label: []const u8) anyerror!png.Image {
        return img;
    }

    const diff = imgdiff.compare(img, golden) catch |err| return err;
    const diff = try imgdiff.compare(img, golden);
    if (diff.rmse > imgdiff.RMSE_DEFAULT or diff.max_pixel > imgdiff.PIXEL_MAX_DEFAULT) {
        const lbl = try rc.alloc.dupe(u8, label);
        errdefer rc.alloc.free(lbl);