Skip to content

chore: v0.34.1 hotfix — sokol screenshot bmp.zig + gl extern leak (#222) - #223

Merged
apotema merged 3 commits into
mainfrom
hotfix/v0.34.1-sokol-screenshot
May 26, 2026
Merged

apotema merged 3 commits into
mainfrom
hotfix/v0.34.1-sokol-screenshot

Conversation

@apotema

@apotema apotema commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

v0.34.1 now fully closes #222 — all three issues, not just 1 + 3 — and ships the complete sokol screenshot stack for macOS Metal. Stacked on top of the original hotfix (e9744d3):

1. sokol-zig pin bump — closes #222 issue 2

backends/sokol/build.zig.zon: bump from 6b8609d → 887b30f (labelle-toolkit/sokol-zig feat/with-sokol-imgui-no-app branch HEAD after labelle-toolkit/sokol-zig#1 merged). That fork PR restored the native accessor exports including _sapp_metal_get_current_drawable — the symbol the Metal readback needs.

  • New hash: sokol-0.1.0-pb1HK-OCNwDtCJMfpeiJuF5TsFpITiO13canPXZMSksO
  • Bumped in lockstep across all three in-repo zon files: backends/sokol/, gui/sokol-imgui/, gui/simple-sokol/ (the pin comment requires they match exactly — see assembler#207).

2. Metal readback gate → live

backends/sokol/src/screenshot/metal.zig: flip fork_exports_drawable from false to true. With the symbol exported by the bumped fork, the live readback branch now links cleanly. The false branch is kept as compile-time rollback safety + documentation of the link edge.

The inline extern fn sapp_metal_get_current_drawable is also retagged to extern "c" fn (see review #3 below).

3. Address gemini-code-assist medium findings (5/5)

File Finding Resolution
bmp.zig writeBytesViaLibc hardcodes std.heap.page_allocator Add allocator: std.mem.Allocator param, forward from writeBmp
bmp.zig writeBmp call site doesn't forward allocator Pass allocator through
bmp.zig writeBmpFromBgra call site same Pass allocator through
gl.zig inner extern fn (glReadPixels/glPixelStorei/glGetError) → extern "c" fn (stdlib pattern; see std.c.fopen)
metal.zig inner extern fn sapp_metal_get_current_drawable → extern "c" fn

Per PR #218's verification cycle, gemini is correct on the extern "c" direction this time (feedback_gemini_inverted_corrections advises checking; verified by following the stdlib pattern).

Issue 1 (bmp.zig) and Issue 3 (gl.zig extern leak)

Original e9744d3 commit unchanged on these — see the original PR body for rationale.

Test plan

  • zig build test from assembler root — green
  • zig build from backends/sokol/example/ — green
  • Locally-built 0.34.1 assembler binary + staged backend in the labelle cache: assembler templates regenerate cleanly, includes resolve, sokol_clib compiles with the new pin
  • End-to-end screenshot smoke (flying-platform-labelle labelle run --screenshot=/tmp/fp.bmp) — blocked on parallel labelle-imgui pin bump (see below)
  • Reviewer to confirm: no behaviour change for raylib / GL-backed builds
  • Reviewer to confirm: pre-existing tests / CI still green

Follow-up required: labelle-imgui pin coordination

labelle-imgui needs a parallel pin bump to 887b30f in both its top-level build.zig.zon and bridges/sokol/build.zig.zon. Without it, the bridge resolves b.dependency("sokol", .{...}) against the OLD fork commit while the assembler resolves against the NEW one — Zig caches them as two separate sokol_clib artifacts, the cimgui include-path injection lands on only one of them, and sokol_imgui.c fails to find cimgui.h at build time.

Symptom (without the imgui-side bump):

sokol_imgui.c:8:10: error: 'cimgui.h' file not found

Non-imgui sokol consumers (backends/sokol/example/) link and run fine — the issue is strictly the imgui+sokol pin diamond.

Closes #222.

… 1+3)

Two link-breaking regressions slipped through 0.34.0's CI because none
of the existing build matrix exercises a fully-linked end-to-end consumer
on macOS native (Metal-only) hardware. Both are fixed here; the third
bug in #222 (sokol-zig fork export of _sapp_metal_get_current_drawable)
is being handled separately in the fork and will land in a follow-up
that flips the new `fork_exports_drawable` comptime gate in
screenshot/metal.zig.

Issue 1 — bmp.zig used std.fs.cwd().createFile, which Zig 0.16 removed.
  Replace with libc fopen/fwrite/fclose via a shared `writeBytesViaLibc`
  helper, matching the same pattern PR #218 already used in
  gfx/texture.zig and audio/legacy.zig for the legacy path-based loaders.
  The window module gains `link_libc = true` so the libc surface is on
  the link line — libc was already pulled in by the gfx/audio modules
  for stb_image / stb_vorbis, so this adds no new system dep.

Issue 3 — screenshot/gl.zig declared `extern fn glReadPixels` (et al.)
  at module scope. The comptime gate in window.readbackGL prevents the
  *call* from running on Darwin, but the extern declarations themselves
  leak into the linker's symbol table as unresolved references because
  the module is still part of the analysis graph. On macOS native, the
  OpenGL framework isn't linked, so those references fail. Move the
  extern decls inside the function body (so they only elaborate when
  `readback` is compiled) and add an early-return on Darwin so the body
  is dead-stripped before the extern is reached.

Issue 2 (mitigation only) — screenshot/metal.zig had the same
  module-scope extern leak shape for `sapp_metal_get_current_drawable`.
  The fork patch that exports the symbol isn't in this PR, but we gate
  the extern declaration the same way (inside the function, behind a
  `fork_exports_drawable: bool = false` comptime flag) and add a clear
  stub log message so the binary links cleanly on macOS Metal until the
  fork patch lands. Follow-up PR flips the flag to true.

Verification:
  - `zig build test` from assembler root: green.
  - `zig build` from backends/sokol/example: green.
  - flying-platform-labelle `labelle run --timeout=3s` against a
    locally-installed 0.34.1 binary on macOS aarch64 (Metal):
    builds + links cleanly, launches, runs the loading→playing
    transition, hits the 3s timeout. The screenshot path is still
    broken on Metal (waiting on Issue 2) but the build itself works,
    which is what FP needed.
@cursor

cursor Bot commented May 26, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches native linking (libc, GL/Metal symbols), a coordinated sokol-zig pin across packages, and live Metal framebuffer readback; imgui consumers still need matching pins to avoid duplicate sokol artifacts.

Overview
v0.34.1 completes the sokol screenshot hotfix for #222: dependency pin, macOS Metal capture, and build/link fixes for Zig 0.16.

The labelle-toolkit/sokol-zig fork is bumped to 887b30f in backends/sokol, gui/sokol-imgui, and gui/simple-sokol so _sapp_metal_get_current_drawable is available. metal.zig sets fork_exports_drawable to true and runs the real readback path again, with sapp_metal_get_current_drawable declared as an inline extern "c" inside readback.

BMP output no longer uses std.fs.cwd().createFile (removed in Zig 0.16). bmp.zig writes via a new writeBytesViaLibc helper (fopen / fwrite / fclose) and threads the caller’s allocator for the null-terminated path; the window module enables link_libc = true in build.zig.

gl.zig avoids module-scope GL symbols that broke macOS links when OpenGL isn’t linked: a comptime Darwin early return uses builtin.target.os.tag, and glReadPixels / related symbols are extern "c" inside the non-Darwin path only.

Reviewed by Cursor Bugbot for commit bda9a7b. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces changes to handle screenshot writing and readback mechanisms under Zig 0.16. It links libc to write BMP files via libc file operations, and scopes OpenGL and Metal extern declarations inside function bodies to prevent undefined-symbol errors at link time on platforms where those backends are unavailable. The review feedback suggests passing the existing allocator to the libc-based file writer instead of hardcoding std.heap.page_allocator, and recommends using extern "c" fn instead of extern fn when declaring external C functions.

Comment thread backends/sokol/src/screenshot/bmp.zig Outdated
Comment on lines +32 to +38
fn writeBytesViaLibc(path: []const u8, data: []const u8) !void {
const path_z = try std.heap.page_allocator.dupeZ(u8, path);
defer std.heap.page_allocator.free(path_z);
const fp = std.c.fopen(path_z.ptr, "wb") orelse return error.FileWriteFailed;
defer _ = std.c.fclose(fp);
if (std.c.fwrite(data.ptr, 1, data.len, fp) != data.len) return error.FileWriteFailed;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of hardcoding std.heap.page_allocator, pass the allocator parameter that is already provided to the calling functions (writeBmp and writeBmpFromBgra). This allows the caller to control the allocation strategy (e.g., using an arena or a general-purpose allocator).

fn writeBytesViaLibc(allocator: std.mem.Allocator, path: []const u8, data: []const u8) !void {
    const path_z = try allocator.dupeZ(u8, path);
    defer allocator.free(path_z);
    const fp = std.c.fopen(path_z.ptr, "wb") orelse return error.FileWriteFailed;
    defer _ = std.c.fclose(fp);
    if (std.c.fwrite(data.ptr, 1, data.len, fp) != data.len) return error.FileWriteFailed;
}

Comment thread backends/sokol/src/screenshot/bmp.zig Outdated
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
try file.writeAll(data);
try writeBytesViaLibc(path, data);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pass the allocator to writeBytesViaLibc.

    try writeBytesViaLibc(allocator, path, data);

Comment thread backends/sokol/src/screenshot/bmp.zig Outdated
const file = try std.fs.cwd().createFile(path, .{});
defer file.close();
try file.writeAll(data);
try writeBytesViaLibc(path, data);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pass the allocator to writeBytesViaLibc.

    try writeBytesViaLibc(allocator, path, data);

Comment on lines +57 to +61
const gl = struct {
extern fn glReadPixels(x: i32, y: i32, w: i32, h: i32, format: u32, type_: u32, data: ?*anyopaque) void;
extern fn glPixelStorei(pname: u32, param: i32) void;
extern fn glGetError() u32;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When declaring external C functions in Zig (with link_libc = true), use extern "c" fn instead of extern fn without a library name. The "c" is a logical reference to libc, not a literal filename, and is correctly resolved by the compiler across all platforms, including Windows MSVC.

    const gl = struct {
        extern "c" fn glReadPixels(x: i32, y: i32, w: i32, h: i32, format: u32, type_: u32, data: ?*anyopaque) void;
        extern "c" fn glPixelStorei(pname: u32, param: i32) void;
        extern "c" fn glGetError() u32;
    };
References
  1. When declaring external C functions in Zig (with link_libc = true), use extern "c" fn instead of extern fn without a library name. The "c" is a logical reference to libc, not a literal filename, and is correctly resolved by the compiler across all platforms, including Windows MSVC.

Comment on lines +143 to +145
const sapp_metal_get_current_drawable = (struct {
extern fn sapp_metal_get_current_drawable() ?*const anyopaque;
}).sapp_metal_get_current_drawable;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When declaring external C functions in Zig (with link_libc = true), use extern "c" fn instead of extern fn without a library name. The "c" is a logical reference to libc, not a literal filename, and is correctly resolved by the compiler across all platforms, including Windows MSVC.

    const sapp_metal_get_current_drawable = (struct {
        extern "c" fn sapp_metal_get_current_drawable() ?*const anyopaque;
    }).sapp_metal_get_current_drawable;
References
  1. When declaring external C functions in Zig (with link_libc = true), use extern "c" fn instead of extern fn without a library name. The "c" is a logical reference to libc, not a literal filename, and is correctly resolved by the compiler across all platforms, including Windows MSVC.

…ack, address review

Expands the v0.34.1 hotfix to make sokol screenshot actually work on
macOS Metal end-to-end, not just link cleanly. Three layers stacked
on top of the original e9744d3 commit:

1. sokol-zig pin bump (closes #222 issue 2)

   `backends/sokol/build.zig.zon`: bump the labelle-toolkit/sokol-zig
   fork pin from 6b8609d → 887b30f. The new commit is the fork's
   `feat/with-sokol-imgui-no-app` branch HEAD after labelle-toolkit/
   sokol-zig#1 merged, which restored the native accessor exports
   including `_sapp_metal_get_current_drawable` — the symbol the
   Metal readback path needs.

   New hash: sokol-0.1.0-pb1HK-OCNwDtCJMfpeiJuF5TsFpITiO13canPXZMSksO

   `gui/sokol-imgui/build.zig.zon` and `gui/simple-sokol/build.zig.zon`
   bump in lockstep — the pin comment explicitly requires all three
   to match (assembler#207) or Zig caches two `sokol_clib` artifacts
   and two `_sg` states.

2. Flip the Metal readback gate to live

   `backends/sokol/src/screenshot/metal.zig`: with the symbol now
   exported, flip `fork_exports_drawable: bool = false → true`. The
   stub branch is retained for compile-time rollback safety; the
   live branch's inline `extern fn sapp_metal_get_current_drawable`
   declaration now resolves cleanly at link time.

3. Address gemini medium-priority review findings on PR #223

   - `bmp.zig` ×3: thread `allocator: std.mem.Allocator` through
     `writeBytesViaLibc` instead of hardcoding `std.heap.page_allocator`;
     forward the existing allocator from `writeBmp` and
     `writeBmpFromBgra` call sites.
   - `gl.zig` ×1: change the inner `extern fn` block (glReadPixels,
     glPixelStorei, glGetError) to `extern "c" fn`. This is the
     canonical libc-linkage form — see std.c.fopen and the 630+
     other usages in stdlib. Per PR #218's verification, this is
     the correct direction (gemini sometimes inverts; not this time).
   - `metal.zig` ×1: same `extern fn` → `extern "c" fn` change on
     `sapp_metal_get_current_drawable` for symmetry with gl.zig
     and the stdlib pattern.

Verification:
  - `zig build test` from assembler root: green.
  - `zig build` from backends/sokol/example: green.
  - Locally-built 0.34.1 assembler binary + staged backend in the
    labelle cache: assembler templates regenerate the project
    build cleanly. The end-to-end `labelle run --screenshot`
    smoke is blocked on a parallel labelle-imgui pin bump (see
    follow-up note below) — when only the assembler-side pin
    moves, the labelle-imgui bridge still pulls in sokol@6b8609d
    via its own `bridges/sokol/build.zig.zon`, producing two
    `sokol_clib` artifacts in the final binary and a
    `cimgui.h file not found` error at the sokol_imgui.c compile.
    Non-imgui sokol consumers (the example) link and run.

Follow-up:
  labelle-imgui needs a parallel pin bump to 887b30f in both
  `build.zig.zon` (top-level) and `bridges/sokol/build.zig.zon`
  before flying-platform-labelle (or any imgui-using sokol consumer)
  can take 0.34.1 end-to-end.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5033c42. Configure here.

Comment thread backends/sokol/src/screenshot/gl.zig
…ost)

cursor[bot] caught this on PR #223: the Darwin early-return in
backends/sokol/src/screenshot/gl.zig was gated on builtin.os.tag —
which is the *host* OS during cross-compilation. Building the window
module for aarch64-macos / x86_64-macos from a non-Apple host would
skip the bail-out and reintroduce the undefined-symbol link failures
this change was meant to fix.

backends/sokol/src/screenshot/metal.zig:105 already used
builtin.target.os.tag correctly; gl.zig is now consistent.
@apotema
apotema merged commit 2fb6017 into main May 26, 2026
4 checks passed
@apotema
apotema deleted the hotfix/v0.34.1-sokol-screenshot branch May 26, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sokol takeScreenshot broken at runtime — linker errors on macOS native build

1 participant