a73x

0ce56438

scenario: guard scale change + tighten captureCb errdefer

a73x   2026-04-19 13:39

Commit message
scenario: guard scale change + tighten captureCb errdefer

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>

src/main.zig
Old New
@@ -237,9 +237,15 @@ pub fn runScenarios(alloc: std.mem.Allocator, argv: []const [:0]const u8) !u8 {
237 }; 237 };
238 defer rc.deinit(); 238 defer rc.deinit();
239 239
240 runTerminal(alloc, &rc) catch |err| { 240 runTerminal(alloc, &rc) catch |err| switch (err) {
241 std.debug.print("scenario {s}: runtime error: {s}\n", .{ scenario_name, @errorName(err) }); 241 error.ScenarioScaleChangeNotSupported => {
242 return @intFromEnum(scenario_runtime.ExitCode.other_error); 242 // already printed; return exit 6 (other render error)
243 return @intFromEnum(scenario_runtime.ExitCode.other_error);
244 },
245 else => {
246 std.debug.print("scenario {s}: runtime error: {s}\n", .{ scenario_name, @errorName(err) });
247 return @intFromEnum(scenario_runtime.ExitCode.other_error);
248 },
243 }; 249 };
244 250
245 if (rc.tick_fatal) |err| { 251 if (rc.tick_fatal) |err| {
@@ -608,6 +614,10 @@ fn runTerminal(alloc: std.mem.Allocator, tick_ctx: ?*scenario_runtime.RunContext
608 if (!render_pending) continue; 614 if (!render_pending) continue;
609 615
610 if (scale_pending) { 616 if (scale_pending) {
617 if (tick_ctx) |_| {
618 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", .{});
619 return error.ScenarioScaleChangeNotSupported;
620 }
611 vk_sync.waitIdleForShutdown(ctx.vkd, ctx.device); 621 vk_sync.waitIdleForShutdown(ctx.vkd, ctx.device);
612 622
613 geom = try rebuildFaceForScale( 623 geom = try rebuildFaceForScale(
src/scenario_runtime.zig
Old New
@@ -155,10 +155,10 @@ pub fn captureCb(ctx: *anyopaque, label: []const u8) anyerror!png.Image {
155 155
156 // --- readback --- 156 // --- readback ---
157 const pixels = try rc.alloc.alloc(u8, @as(usize, rc.px_w) * rc.px_h * 4); 157 const pixels = try rc.alloc.alloc(u8, @as(usize, rc.px_w) * rc.px_h * 4);
158 errdefer rc.alloc.free(pixels);
159 try rctx.readbackOffscreen(offscreen, pixels); 158 try rctx.readbackOffscreen(offscreen, pixels);
160 159
161 const img: png.Image = .{ .width = rc.px_w, .height = rc.px_h, .pixels = pixels }; 160 const img: png.Image = .{ .width = rc.px_w, .height = rc.px_h, .pixels = pixels };
161 errdefer rc.alloc.free(img.pixels); // covers all subsequent error paths
162 162
163 // --- write out/<scenario>/<label>.png --- 163 // --- write out/<scenario>/<label>.png ---
164 const out_dir = try std.fmt.allocPrint( 164 const out_dir = try std.fmt.allocPrint(
@@ -230,7 +230,7 @@ pub fn captureCb(ctx: *anyopaque, label: []const u8) anyerror!png.Image {
230 return img; 230 return img;
231 } 231 }
232 232
233 const diff = imgdiff.compare(img, golden) catch |err| return err; 233 const diff = try imgdiff.compare(img, golden);
234 if (diff.rmse > imgdiff.RMSE_DEFAULT or diff.max_pixel > imgdiff.PIXEL_MAX_DEFAULT) { 234 if (diff.rmse > imgdiff.RMSE_DEFAULT or diff.max_pixel > imgdiff.PIXEL_MAX_DEFAULT) {
235 const lbl = try rc.alloc.dupe(u8, label); 235 const lbl = try rc.alloc.dupe(u8, label);
236 errdefer rc.alloc.free(lbl); 236 errdefer rc.alloc.free(lbl);