Skip to content

fix(wgpu): build + render the demo on macOS (Metal surface) - #290

Merged
apotema merged 3 commits into
mainfrom
fix/wgpu-macos-surface
Jun 12, 2026
Merged

fix(wgpu): build + render the demo on macOS (Metal surface)#290
apotema merged 3 commits into
mainfrom
fix/wgpu-macos-surface

Conversation

@apotema

@apotema apotema commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What

Make the WebGPU backend build and render on macOS — both the standalone demo and engine-generated games.

The break

backends/wgpu/example/build.zig was inherited from the old Dawn-based wiring (absorbed from labelle-cli) and still linked a nonexistent zdawn artifact plus a dawn system library and dawn_aarch64_macos/dawn_x86_64_macos deps. Since the backend migrated to wgpu-native (wgpu_native_zig, #228/#287), zig build in the example panicked:

thread panic: unable to find artifact 'zdawn'  (available artifact: 'glfw')

The same missing-frameworks problem also lived in the assembler's link_wgpu build template, so any game generated with .backend = .wgpu would fail to link on macOS too.

The fix

  • example/build.zig — drop all Dawn references. wgpu_native_zig's wgpu module already embeds libwgpu_native.a via addObjectFile, and that module is imported by the backend's gfx/window modules, so the native runtime travels into the exe transitively. The example only needs the glfw artifact plus, on Apple targets, the Metal/QuartzCore/Foundation frameworks wgpu-native links at the Compile step (upstream links them per-Compile, not on the module).
  • src/window.zig — surface creation was Win32-only. Add the Apple path: pull the Cocoa NSWindow from GLFW (getCocoaWindow), attach a fresh CAMetalLayer to its content view via three objc_msgSend calls, and feed it to surfaceDescriptorFromMetalLayer. initGpu is refactored to a per-OS createSurface() so the adapter → device → pipeline spine stays shared.
  • src/templates/build_zig.txt (link_wgpu) — link Foundation/QuartzCore/Metal on Apple targets in generated engine builds, mirroring the example fix.

Verification (macOS arm64, Zig 0.16.0)

  • backends/wgpuzig build test: 14/14 pass.
  • backends/wgpu/examplezig build: produces wgpu-demo (links the wgpu-native static lib); running it opens a window and runs the acquire → render → present loop with no warnings — previously it logged surface creation only wired for Windows; rendering disabled and spammed shape batch full (batch never drained). The clean run means gpu_ready is set (full GPU spine initialized) and the shape batch drains each frame.
  • Engine end-to-end: regenerated the gizmo-rendered Ricochet game with .backend = .wgpu. zig build links cleanly; the binary opens a window and runs the full engine loop (waves, physics, gizmo rendering through the wgpu shape path) for the whole run with zero wgpu/render warnings.

Assembler zig build + zig build test stay green. Windows path is unchanged.

The example/build.zig was inherited from the old Dawn-based wiring and
still linked a nonexistent 'zdawn' artifact plus a 'dawn' system lib and
dawn_*_macos deps, so 'zig build' panicked with "unable to find artifact
'zdawn'" on macOS. The backend now rides wgpu-native (wgpu_native_zig),
whose 'wgpu' module already embeds libwgpu_native.a via addObjectFile, so
the runtime travels transitively through gfx/window. Drop the Dawn refs;
the example only needs the glfw artifact plus the Metal/QuartzCore/
Foundation frameworks wgpu-native links at the Compile step.

window.zig only created a surface on Windows (Win32 HWND). Wire the Apple
path: pull the Cocoa NSWindow from GLFW, attach a CAMetalLayer to its
content view via three objc_msgSend calls, and feed it to
surfaceDescriptorFromMetalLayer. initGpu is refactored to a per-OS
createSurface() so the adapter/device/pipeline spine is shared.

Verified on macOS arm64: demo builds, opens a window, and runs the
acquire->render->present loop with no warnings (GPU spine initializes;
shape batch drains each frame).

@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 adds macOS and iOS support to the WGPU backend. It updates the build configuration to link macOS system frameworks (Foundation, QuartzCore, and Metal) instead of Dawn, and implements macOS surface creation in window.zig by dynamically attaching a CAMetalLayer to the GLFW Cocoa window using the Objective-C runtime. A critical compilation issue was identified in window.zig where the lowercase .c calling convention is used instead of the uppercase .C.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +93 to +95
const msgId = @as(*const fn (ObjcId, ?*anyopaque) callconv(.c) ObjcId, @ptrCast(&objc_msgSend));
const msgSetBool = @as(*const fn (ObjcId, ?*anyopaque, i8) callconv(.c) void, @ptrCast(&objc_msgSend));
const msgSetId = @as(*const fn (ObjcId, ?*anyopaque, ObjcId) callconv(.c) void, @ptrCast(&objc_msgSend));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

In Zig, the calling convention for C is represented by the uppercase .C (i.e., callconv(.C)), not lowercase .c. Using .c will result in a compilation error because std.builtin.CallingConvention does not contain a lowercase c member.

    const msgId = @as(*const fn (ObjcId, ?*anyopaque) callconv(.C) ObjcId, @ptrCast(&objc_msgSend));
    const msgSetBool = @as(*const fn (ObjcId, ?*anyopaque, i8) callconv(.C) void, @ptrCast(&objc_msgSend));
    const msgSetId = @as(*const fn (ObjcId, ?*anyopaque, ObjcId) callconv(.C) void, @ptrCast(&objc_msgSend));

The `link_wgpu` build section linked only the glfw artifact, so any game
generated with `.backend = .wgpu` failed to link on macOS/iOS with
undefined Metal/Foundation symbols pulled in by libwgpu_native.a (embedded
in the wgpu module via addObjectFile). Link Foundation/QuartzCore/Metal on
Apple targets, mirroring the example/build.zig fix.

Verified end to end: generated the Ricochet game with `.backend = .wgpu`
on macOS arm64 — `zig build` links, and the binary opens a window and runs
the engine loop (gizmo rendering through the wgpu shape path) with no
rendering-disabled warnings.
@cursor

cursor Bot commented Jun 12, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches native linking and low-level Cocoa/Metal surface setup on the render path; Windows behavior is unchanged and CI adds build-only guards.

Overview
Restores macOS WebGPU builds and rendering by aligning linking with wgpu-native (no Dawn/zdawn) and adding a Metal-backed surface path.

The standalone wgpu demo build.zig stops linking the missing zdawn artifact and Dawn system libs; it keeps GLFW and, on macOS/iOS, links Foundation, QuartzCore, and Metal so libwgpu_native.a resolves. window.zig refactors GPU init to createSurface(): Windows still uses Win32 HWND; macOS gets Cocoa NSWindow from GLFW, attaches a CAMetalLayer via minimal Objective-C runtime calls, and uses surfaceDescriptorFromMetalLayer. Failed surface creation now releases the wgpu instance to avoid leaks on unsupported platforms.

Generated games using .backend = .wgpu get the same Apple framework links in the link_wgpu section of build_zig.txt.

CI re-enables backends/wgpu zig build test, adds a macOS-only backends/wgpu/example zig build, and greps the generated wgpu example build.zig for the three framework links on Linux so template regressions are caught without an Apple runner.

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

@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 c45f039. Configure here.

Comment thread backends/wgpu/src/window.zig
Cursor flagged that the initGpu refactor creates the wgpu instance before
createSurface, so on platforms without a wired surface (Linux) the instance
was created then leaked when createSurface returns null. Release it on that
path.

CI: the macOS build break shipped because nothing built wgpu on macOS.
- Re-enable the wgpu backend WAV-parser unit test (the 0.16 gate is stale;
  it builds only the pure parser, no native deps).
- Build the standalone backends/wgpu/example on the macos-latest runner —
  the real regression lock for the Apple render path (links the Metal/
  QuartzCore/Foundation frameworks and compiles the CAMetalLayer surface).
- Assert the generated wgpu build.zig links those frameworks in the Linux
  examples-integration step, locking the link_wgpu template against a
  silent framework-drop even without a macOS runner there.

(Declined Gemini's callconv(.c) -> .C suggestion: Zig 0.16 uses lowercase
.c — the file already uses callconv(.winapi), zglfw uses callconv(.c), and
this builds + links on macOS.)
@apotema

apotema commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Bot triage:

  • Cursor (instance leak on unsupported platforms) — valid, fixed in 38ae3b7. initGpu now releases the wgpu instance (and nulls it) when createSurface returns null, so the Linux/unsupported path no longer leaks.
  • Gemini (callconv(.c).C) — declined. Zig 0.16 uses the lowercase .c calling convention; .C is the pre-0.15 spelling. The surrounding code already uses callconv(.winapi), the zglfw dep uses callconv(.c), and this branch builds and links on macOS arm64 — .C would not compile.

Added CI so this class of break can't ship silently again: the standalone backends/wgpu/example now builds on the macos-latest runner (real Metal-framework link + CAMetalLayer surface compile), and the Linux examples-integration step asserts the generated wgpu_desktop/build.zig links Foundation/QuartzCore/Metal (locks the link_wgpu template).

@apotema
apotema merged commit b87cb7f into main Jun 12, 2026
4 checks passed
@apotema
apotema deleted the fix/wgpu-macos-surface branch June 12, 2026 20:25
apotema added a commit that referenced this pull request Jun 13, 2026
Backend work since v0.39.1:
- wgpu: macOS Metal surface + textured sprite rendering (#290, #291)
- bgfx: macOS bring-up to on-device Android (#296 epic, #304/#305/#307/#308/#309)
- #310 AndroidBackendContext adapters: sokol (#312) + bgfx (#313) register the
  core seam; bgfx-Android gamepad via the shared android_gamepad sub-package
- bgfx desktop gamepad: GLFW (#315) + SDL HIDAPI / Switch-pad support (#318)
- cached bgfx CI job (#295)

Android codegen now calls core.registerAndroidBackend → requires
labelle-core >= v1.17.0 + labelle-engine >= v1.50.0.
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.

1 participant