Skip to content

Auto-compile Metal shaders via SwiftPM build plugin - #1

Merged
skyblanket merged 2 commits into
mainfrom
metal-plugin-trial
Jun 10, 2026
Merged

Auto-compile Metal shaders via SwiftPM build plugin#1
skyblanket merged 2 commits into
mainfrom
metal-plugin-trial

Conversation

@skyblanket

@skyblanket skyblanket commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Replaces the checked-in metallib + manual build_shaders.sh step with a build-tool plugin. Verified: bundle embedding, runtime ShaderLibrary load, incremental recompile on .metal edit, 24/24 tests green locally.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Metal shaders now compile automatically during swift build via a new plugin, eliminating manual compilation steps.
  • Documentation

    • Updated shader development guide with automatic compilation workflow; removed manual build instructions.
  • Chores

    • Eliminated manual shader build script and pre-compiled artifacts from the repository.

- Plugins/MetalCompilerPlugin: buildCommand globs Sources/SwiftRender/Shaders,
  compiles via xcrun metal, output metallib auto-embeds in the module bundle
- checked-in Resources/default.metallib removed; Shaders/ excluded from
  resources (plugin owns them); incremental: editing a .metal recompiles
- kills the silently-stale-shader footgun for good

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@skyblanket, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 59 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd51aeaa-44a7-4372-a058-432aaa2b1e2b

📥 Commits

Reviewing files that changed from the base of the PR and between 9de6045 and 0f247ab.

📒 Files selected for processing (1)
  • Plugins/MetalCompilerPlugin/plugin.swift
📝 Walkthrough

Walkthrough

This PR introduces automatic Metal shader compilation at build time using a SwiftPM build tool plugin. A new MetalCompilerPlugin discovers .metal files in Shaders/ and compiles them to default.metallib. Package.swift is updated to wire the plugin to the SwiftRender target and exclude Shaders from resource packaging. Documentation is updated to reflect the new automatic workflow.

Changes

Metal Shader Compilation Plugin

Layer / File(s) Summary
Plugin Declaration and Implementation
Package.swift, Plugins/MetalCompilerPlugin/plugin.swift
New .plugin target MetalCompilerPlugin with .buildTool() capability. Plugin discovers .metal files under module Shaders/ directory, sorts them, and emits a single xcrun metal build command targeting macosx to produce default.metallib in the plugin work directory. Early returns if target is not a source module, shader directory is unreadable, or no .metal files exist.
SwiftRender Target Integration
Package.swift
SwiftRender target is updated to exclude Shaders from resource processing, include only Resources, and declare MetalCompilerPlugin as a build tool dependency.
User Documentation and Changelog
CONTRIBUTING.md, README.md, CHANGELOG.md
"Adding a shader" section updated to describe placing [[ stitchable ]] functions in .metal files under Sources/SwiftRender/Shaders/ and relying on swift build to compile via the plugin. README updated to note automatic compilation. Changelog documents removal of checked-in metallib artifacts and manual tools/build_shaders.sh step. Prior manual xcrun rebuild instructions removed.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A plugin springs to life so bright,
Metal shaders now compile just right,
With swift build, no manual chore,
The rabbit's code flows smooth and pure! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing automatic Metal shader compilation via a SwiftPM build plugin, which is the core objective reflected across all file modifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch metal-plugin-trial

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
README.md (3)

144-144: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove stale documentation about CLI metallib freshness warning.

This sentence describes a runtime warning from the old manual workflow (when users ran tools/build_shaders.sh separately). With the MetalCompilerPlugin handling incremental compilation automatically during swift build, this warning is no longer relevant. The plugin ensures the metallib is always up-to-date with source changes.

📝 Suggested removal
-
-(If a `.metal` file is newer than the compiled metallib, the CLI warns you loudly — no silently-stale shaders.)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 144, Delete the outdated sentence about a CLI warning that
a ".metal" file is newer than the compiled metallib from the README, since the
MetalCompilerPlugin now performs incremental compilation during swift build;
update any nearby context referencing manual shader build steps to avoid
implying the old runtime warning behavior and ensure references to
MetalCompilerPlugin remain as the source of up-to-date metallib handling.

218-218: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove completed roadmap item.

The first roadmap bullet "SwiftPM build plugin for automatic metallib compilation" is implemented by this PR. It should be removed from the roadmap to avoid confusion about what features are completed versus planned.

📝 Suggested removal
 ## Roadmap
 
-- SwiftPM build plugin for automatic metallib compilation
 - `contact <Scene>` grid-sheet export · transparent ProRes 4444 · 10-bit masters
 - Audio-reactive FFT improvements (configurable bands, onset detection)
 - Linux? No — this is proudly the native-Apple lane.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 218, Remove the completed roadmap item text "SwiftPM build
plugin for automatic metallib compilation" from the README roadmap section by
deleting that bullet entry (the exact string appears in the roadmap list) so the
roadmap only contains planned items; ensure any trailing punctuation or an empty
list marker is cleaned up to keep formatting consistent.

213-213: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify Xcode requirement for the build plugin.

The parenthetical "xcrun metal needed only when editing shaders" is misleading with the new plugin workflow. The Metal toolchain (xcrun metal) is now invoked automatically during swift build by the MetalCompilerPlugin, not just when manually editing shaders. CONTRIBUTING.md more accurately describes this as "requires full Xcode for the metal toolchain."

Consider rephrasing to align with CONTRIBUTING.md and clarify that the toolchain is required for builds, not just manual shader compilation.

📝 Suggested fix
-- Xcode 15+ toolchain (`xcrun metal` needed only when editing shaders)
+- Xcode 15+ toolchain (Metal compiler required for builds)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 213, Update the README line "Xcode 15+ toolchain (`xcrun
metal` needed only when editing shaders)" to clarify that the Metal toolchain is
required for builds and is invoked automatically by the MetalCompilerPlugin
during swift build; reference the same wording/intent used in CONTRIBUTING.md
(e.g., "requires full Xcode for the metal toolchain") and remove the
parenthetical that implies manual invocation is the only case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Package.swift`:
- Line 19: The Package.swift target's plugin declaration is using invalid
syntax; update the target's plugins entry (the one that currently reads plugins:
["MetalCompilerPlugin"]) to use the SwiftPM plugin API by replacing the string
array with a plugin specification such as plugins: [.plugin(name:
"MetalCompilerPlugin")] (if the plugin is in another package use .plugin(name:
"MetalCompilerPlugin", package: "PackageName") instead) so the target's plugin
is declared with .plugin(name: ...) rather than a plain string.

In `@Plugins/MetalCompilerPlugin/plugin.swift`:
- Around line 12-15: The plugin currently swallows failures and empties by
returning [] when directory reading fails or no .metal files are found; change
this to fail fast by propagating an error (or calling fatalError) with a clear
message including shaderDir and the underlying error when
contentsOfDirectory(atPath:) fails, and when metalFiles.isEmpty throw/raise an
error stating "no .metal files found in \(shaderDir)" so the build fails instead
of producing no default.metallib; update the code paths around entries,
metalFiles and shaderDir to return/throw an appropriate Error type (or
fatalError) rather than returning an empty array.

---

Outside diff comments:
In `@README.md`:
- Line 144: Delete the outdated sentence about a CLI warning that a ".metal"
file is newer than the compiled metallib from the README, since the
MetalCompilerPlugin now performs incremental compilation during swift build;
update any nearby context referencing manual shader build steps to avoid
implying the old runtime warning behavior and ensure references to
MetalCompilerPlugin remain as the source of up-to-date metallib handling.
- Line 218: Remove the completed roadmap item text "SwiftPM build plugin for
automatic metallib compilation" from the README roadmap section by deleting that
bullet entry (the exact string appears in the roadmap list) so the roadmap only
contains planned items; ensure any trailing punctuation or an empty list marker
is cleaned up to keep formatting consistent.
- Line 213: Update the README line "Xcode 15+ toolchain (`xcrun metal` needed
only when editing shaders)" to clarify that the Metal toolchain is required for
builds and is invoked automatically by the MetalCompilerPlugin during swift
build; reference the same wording/intent used in CONTRIBUTING.md (e.g.,
"requires full Xcode for the metal toolchain") and remove the parenthetical that
implies manual invocation is the only case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: add58148-15f3-4817-acc0-67140f3f5c5a

📥 Commits

Reviewing files that changed from the base of the PR and between c3e5027 and 9de6045.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • CONTRIBUTING.md
  • Package.swift
  • Plugins/MetalCompilerPlugin/plugin.swift
  • README.md
  • Sources/SwiftRender/Resources/default.metallib

Comment thread Package.swift
.process("Shaders"),
]
],
plugins: ["MetalCompilerPlugin"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For // swift-tools-version: 5.10, is this valid in Package.swift`?
.target(name: "SwiftRender", plugins: ["MetalCompilerPlugin"])

Or must plugin usage be declared as:
.target(name: "SwiftRender", plugins: [.plugin(name: "MetalCompilerPlugin")])?`

💡 Result:

Yes, for // swift-tools-version: 5.10, the following is valid: .target(name: "SwiftRender", plugins: ["MetalCompilerPlugin"]) You must only use the explicit structured form when you need to specify the plugin’s package (i.e., the plugin is provided by another package). In that case the syntax is: .target(name: "SwiftRender", plugins: [.plugin(name: "MetalCompilerPlugin")]) If the plugin is from a dependency, you’d use the package-qualified form: .target(name: "SwiftRender", plugins: [.plugin(name: "MetalCompilerPlugin", package: "<package-name>")]) Evidence: Apple’s plugin product/target API documents the .plugin(name:capability:dependencies:path:exclude:sources:) and shows plugin usage in target declarations using .plugin(name: ...) (i.e., structured entries), and SwiftPM’s manifest/plugin usage is defined as plugins: [PluginUsage] where PluginUsage provides plugin(_ name: String, package: String? = nil). [1][2] Also, the SwiftPM evolution/proposal docs show the modern/typed syntax for target plugin usage as plugins: [ .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator") ], and it notes that when the plugin is within the same package you omit --package (so no package parameter is needed). [3]

Citations:


🌐 Web query:

SwiftPM 5.10 Package.swift target plugins: ["PluginName"] syntax valid?

💡 Result:

Yes, that specific syntax is valid (for SwiftPM 5.10 and the modern manifest API): in a target declaration you can use the plugins: parameter with an array of Target.PluginUsage, written like plugins: [.plugin(name: "PluginName")] (optionally with package: for plugins coming from another package) [1][2]. Example: let package = Package( name: "Example", targets: [.target( name: "MyTarget", plugins: [.plugin(name: "PluginName") ]) ]) Important constraints / common gotchas: 1) The plugins: parameter applies only to targets like .target(...), .executableTarget(...), and .testTarget(...) that use the manifest API supporting plugin usage [2]. 2) The plugin must be declared as a plugin target in the same Package.swift (or otherwise present in the dependency closure) and must have a compatible capability (e.g., if it’s used via plugins:, SwiftPM expects the plugin to support the buildTool capability, since plugins are “applied to a target” before the build system creates its dependency graph) [2]. 3) If you declare a plugin capability that doesn’t match what SwiftPM expects, you can get build errors complaining about mismatched capabilities (for instance, a declared command plugin being treated inconsistently) [3].

Citations:


🌐 Web query:

SwiftPM 5.10 plugins: ["PluginName"] in Package.swift target declaration valid or must use .plugin(name:)

💡 Result:

In SwiftPM 5.10, a target’s plugin usage must be declared with.plugin(name:) (optionally with package:), inside the target’s plugins: array. You cannot just list plugin names like ["PluginName"] in Package.swift’s target declaration. Authoritative syntax: - Define a plugin target in targets with.plugin(name:..., capability:...). [1][2] - Attach a plugin to a regular target using the plugins: parameter on.target /.executableTarget /.testTarget, with plugins: [.plugin(name: "OpenAPIGenerator", package: "...") ]. [3][4] So if your target looks like: - Wrong (not supported):.target(name: "X", plugins: ["PluginName"]) - Right (supported):.target(name: "X", plugins: [.plugin(name: "PluginName")]) (add package: only if the plugin comes from another package). [3][4]

Citations:


Fix SwiftPM target plugin declaration syntax in Package.swift (line 19).

plugins: ["MetalCompilerPlugin"] isn’t valid; target plugin usage must be declared as plugins: [.plugin(name: "MetalCompilerPlugin")] (add package: only if the plugin comes from another package).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Package.swift` at line 19, The Package.swift target's plugin declaration is
using invalid syntax; update the target's plugins entry (the one that currently
reads plugins: ["MetalCompilerPlugin"]) to use the SwiftPM plugin API by
replacing the string array with a plugin specification such as plugins:
[.plugin(name: "MetalCompilerPlugin")] (if the plugin is in another package use
.plugin(name: "MetalCompilerPlugin", package: "PackageName") instead) so the
target's plugin is declared with .plugin(name: ...) rather than a plain string.

Comment on lines +12 to +15
guard let entries = try? fm.contentsOfDirectory(atPath: shaderDir.string) else { return [] }
let metalFiles = entries.filter { $0.hasSuffix(".metal") }.sorted()
.map { shaderDir.appending($0) }
guard !metalFiles.isEmpty else { return [] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail fast when shader discovery fails (don’t silently no-op).

Line 12 and Line 15 return [] on read failure / empty shader set, which can let build pass without producing default.metallib and defer the breakage to runtime.

Suggested fix
+enum MetalCompilerPluginError: Error {
+    case shaderDirectoryUnreadable(String)
+    case noMetalShadersFound(String)
+}
+
 `@main`
 struct MetalCompilerPlugin: BuildToolPlugin {
     func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
         guard let target = target as? SourceModuleTarget else { return [] }
         let shaderDir = target.directory.appending("Shaders")
         let fm = FileManager.default
-        guard let entries = try? fm.contentsOfDirectory(atPath: shaderDir.string) else { return [] }
+        let entries: [String]
+        do {
+            entries = try fm.contentsOfDirectory(atPath: shaderDir.string)
+        } catch {
+            throw MetalCompilerPluginError.shaderDirectoryUnreadable(shaderDir.string)
+        }
         let metalFiles = entries.filter { $0.hasSuffix(".metal") }.sorted()
             .map { shaderDir.appending($0) }
-        guard !metalFiles.isEmpty else { return [] }
+        guard !metalFiles.isEmpty else {
+            throw MetalCompilerPluginError.noMetalShadersFound(shaderDir.string)
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Plugins/MetalCompilerPlugin/plugin.swift` around lines 12 - 15, The plugin
currently swallows failures and empties by returning [] when directory reading
fails or no .metal files are found; change this to fail fast by propagating an
error (or calling fatalError) with a clear message including shaderDir and the
underlying error when contentsOfDirectory(atPath:) fails, and when
metalFiles.isEmpty throw/raise an error stating "no .metal files found in
\(shaderDir)" so the build fails instead of producing no default.metallib;
update the code paths around entries, metalFiles and shaderDir to return/throw
an appropriate Error type (or fatalError) rather than returning an empty array.

…ry (sandbox-safe on cold builds)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@skyblanket
skyblanket merged commit 54550d0 into main Jun 10, 2026
2 checks passed
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