Skip to content

feat(raylib): prefer IOSurface mode on macOS (#121) - #123

Merged
apotema merged 1 commit into
mainfrom
feat/raylib-iosurface-preview-121
May 16, 2026
Merged

apotema merged 1 commit into
mainfrom
feat/raylib-iosurface-preview-121

Conversation

@apotema

@apotema apotema commented May 16, 2026

Copy link
Copy Markdown
Contributor

Closes #121.

Summary

Wires the per-frame preview lifecycle that landed in #120 to the macOS IOSurface variant of the engine's Preview API (labelle-engine v1.37.3 / #547). On non-macOS targets nothing changes — the SHM beginFrameStream / publishFrame / endFrameStream triple is still emitted and used; on macOS the generated raylib game now uses beginFrameStreamIOSurface / publishFrameIOSurface / endFrameStreamIOSurface for zero-copy frame transport via mach ports.

Where the switch lives

All three call sites are template constants in src/main_zig.zig:

  • PREVIEW_READBACK_SETUP — the deferred teardown picks the matching end* variant.
  • PREVIEW_READBACK_LOOP resize branch — selects beginFrameStream vs beginFrameStreamIOSurface on (re)negotiate.
  • PREVIEW_READBACK_LOOP publish branch — selects publishFrame vs publishFrameIOSurface on each mature PBO.

All three use if (comptime @import("builtin").os.tag == .macos) { ... } else { ... }, mirroring the existing inline pattern already used in PREVIEW_HELPERS for CLOCK_MONOTONIC. Header doc comments on the two const blocks are extended to call out the macOS path and reference the ticket.

The PBO ring math, the resize gating, the 2-frame priming, the bottom-up→top-down strided flip-on-copy, and the _GL_RGBA / _GL_UNSIGNED_BYTE glReadPixels parameters are all untouched.

Why RGBA8 stays the producer-side format

glReadPixels(GL_RGBA, GL_UNSIGNED_BYTE) is what raylib's GL context produces cheaply, and publishFrameIOSurface (engine-side, in preview_mode.zig) takes a []const u8 RGBA8 input and does the RGBA→BGRA swizzle internally during the IOSurface lock/copy. So no producer-side format change, no extra CPU swizzle pass — the existing pixel buffer is handed straight to the IOSurface publisher on macOS, exactly the way it was being handed to the SHM publisher elsewhere.

Test additions

Two regression tests appended to test/tests.zig (alongside the #120 PBO readback tests):

  1. raylib desktop emits both SHM and IOSurface preview lifecycle calls — asserts the generated raylib desktop template contains BOTH _p.beginFrameStream( AND _p.beginFrameStreamIOSurface( (and the publish / end counterparts), plus a comptime-switch sentinel and the RGBA8 readback constant. Also re-runs std.zig.Ast.parse for syntactic validity.
  2. non-raylib loop backends still do not pull in IOSurface lifecycle — loops over .sdl and .sokol, asserts that NEITHER the SHM nor the IOSurface lifecycle strings appear in their generated mains. Locks the macOS gating from accidentally widening scope.

Verified locally

  • zig build of the assembler — clean.
  • zig build test --summary all — 296/300 tests passed (4 skipped), up from 294/298 before the additions (the two new tests + the existing feat(raylib): per-frame PBO readback + publishFrame for PIE viewport #120 tests still pass).
  • Regenerated examples/raylib via the assembler binary directly (the labelle CLI route is blocked by Zig 0.15 in the system PATH); inspected the resulting main.zig and confirmed all three comptime switches emit with both arms present.
  • zig ast-check on the regenerated main.zig — clean.
  • zig build of the regenerated example on macOS (Zig 0.16) — links cleanly. The engine already pulls IOSurface.framework + CoreFoundation via its build script, so no template-side build wiring is needed.

Deferred / out of scope

  • The "render-to-IOSurface FBO" stretch goal mentioned in raylib: prefer IOSurface mode on macOS #121 (skip the glReadPixels copy entirely and have raylib render straight into the IOSurface-backed FBO) is left for a follow-up — it's a bigger refactor that touches the raylib backend's framebuffer, not just the template.

Test plan

  • zig build (assembler) — passes.
  • zig build test — 296/300 pass (4 pre-existing skips).
  • Regenerate examples/raylib and zig ast-check the output.
  • zig build the regenerated example on macOS.
  • End-to-end: launch the regenerated game with LABELLE_PREVIEW=host:port and confirm the editor receives iosurface_bgra8 frames (defer to the gui v0.3.0 side; the editor-side dispatch already exists per feat(sokol): GLCORE/GLES3 PBO readback + publishFrame (#122 slice 1) #124).

The raylib desktop template's per-frame PBO readback now picks the
zero-copy IOSurface variant of the engine's preview lifecycle when
built for macOS, falling back to the SHM path everywhere else.

The switch lives in three template blocks in `src/main_zig.zig` —
PREVIEW_READBACK_SETUP (deferred teardown), the resize branch in
PREVIEW_READBACK_LOOP (beginFrameStream), and the publish branch in
the same loop. All three use `comptime @import("builtin").os.tag
== .macos`, mirroring the existing `CLOCK_MONOTONIC` inline pattern
already in the same file. Other backends (sdl/sokol/bgfx/wgpu) are
untouched — their lifecycle stays control-plane-only.

Producer-side pixel buffer stays RGBA8: glReadPixels still writes
RGBA8 into the PBO, and `publishFrameIOSurface` does the RGBA->BGRA
swizzle internally during the IOSurface lock/copy.

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

cursor Bot commented May 16, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Medium risk because it changes the preview frame-stream lifecycle calls for macOS builds and touches teardown/begin/publish paths in the per-frame render loop; mistakes could break preview streaming or leak resources on macOS. Non-macOS behavior is intended to remain unchanged via comptime gating.

Overview
On raylib desktop, the generated preview readback code now selects IOSurface-based streaming on macOS by switching the Preview lifecycle calls (beginFrameStream*, publishFrame*, endFrameStream*) via comptime @import("builtin").os.tag == .macos, while keeping the existing SHM path for other OSes.

Adds regression tests asserting the raylib template emits both SHM and IOSurface callsites (and the comptime switch sentinel) and that non-raylib loop backends still emit neither SHM nor IOSurface preview lifecycle calls.

Reviewed by Cursor Bugbot for commit b2665e6. 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 macOS-specific support for zero-copy frame transport using IOSurface within the preview readback loop for the Raylib backend. The implementation utilizes comptime branching to select between standard shared memory (SHM) and IOSurface lifecycle methods (beginFrameStreamIOSurface, publishFrameIOSurface, and endFrameStreamIOSurface) based on the target operating system. Additionally, new tests verify that the generated Zig source correctly includes both paths for Raylib while ensuring that non-Raylib backends remain unaffected. I have no feedback to provide as no review comments were submitted.

@apotema
apotema merged commit 2a8393b into main May 16, 2026
4 checks passed
@apotema
apotema deleted the feat/raylib-iosurface-preview-121 branch May 16, 2026 12:43
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.

raylib: prefer IOSurface mode on macOS

1 participant