Skip to content

Step 5: the quoted shaders written as MSL (gpu.METAL), compiled and checked - #840

Merged
paul-hammant merged 5 commits into
mainfrom
step5-msl
Oct 21, 2026
Merged

paul-hammant merged 5 commits into
mainfrom
step5-msl

Conversation

@paul-hammant

@paul-hammant paul-hammant commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Step 5 of docs/aether_quote_design.md section 9: MSL from the quoted shaders' trees. No Metal renderer yet. Section 11 sets no narrower first step, and #312 (the Metal backend) waits on the Vulkan performance pass. So this PR does the part that is clearly in scope: the MSL, compiled and checked. Section 11 now lists the Metal-path options for Nic.

No aether change was needed. Quote's trees carry everything the dialect reads, so this builds on the pinned 0.834.0 and needs no release.

Commits

  1. ae3d.gpu: the quoted shaders written as MSL (gpu.METAL). emit_msl, emit_vertex_msl and emit_screen_vertex_msl reuse the GLSL emitter's expressions and statements, with a CALL_MSL column in the call table. The rules:

    • Conventions are Vulkan's. Each vertex shader negates its position's y as it returns it.
    • Nothing is global. Blocks are passed as struct references, and each texture is passed with its sampler. Helpers that read in place are handed those values by name.
    • std140 by hand. Block structs use packed_float3 and float4 arrays, and every offset and size is a static_assert.
    • Stage interfaces. Varyings are at [[user(locnK)]], attributes at the table's [[attribute(n)]], and colour outputs are four lanes wide.
    • Bindings. Set N is at [[buffer(N)]], storage buffers at 8 + k, and textures/samplers follow capture order.
    • GLSL is unchanged. The dialect is set only while MSL is written.

    tests/test_gpu_msl (no GPU) checks this.

  2. generate_shaders. Writes all 41 MSL files to src/ae3d/metal, and --check byte-compares them. The GLSL, vkscene and SPIR-V are unchanged.

  3. msl_check. tools/mslc.m uses Metal's own compiler through the device, with every warning an error except four noise groups. scripts/msl_check.sh runs it, and ./ci.sh runs that on macOS. xcrun metal also runs where Xcode's Metal Toolchain is installed; it is a separate download and is not on the M6.

  4. docs. Section 9 item 5 and section 11 "What it buys MSL": the rules, how they are held, and three ways to a Metal path, with a recommendation.

Verification (M6, Aether 0.834.0, glslang 16.6.0)

  • generate_shaders --check: 95 generated files are current (53 GLSL, vkscene, 41 MSL), and the SPIR-V (892,276 B) is what glslang 16.6.0 makes. The GLSL and SPIR-V are unchanged by this PR.

  • scripts/msl_check.sh: all 41 files compile clean with every warning an error.

  • Against MoltenVK's own MSL of the same SPIR-V (SPIRV-Cross, logged with MVK_CONFIG_DEBUG=1 from test_backend_parity):

    • all 41 files paired;
    • stage interfaces are equal (attributes, varying locations, colour outputs). This found the four-lane output widening, which is now matched;
    • 506 of ae3d.gpu's offsets, asserted on MoltenVK's own block structs, compile.

    This checks interfaces and layouts, not arithmetic.

  • tests/test_gpu_msl: 33 checks pass, with 0 leaks.

  • Negative controls. Each of these fails as it should:

    • a hand-edited .metal (--check);
    • an offset off by four (static_assert);
    • an unused local (warning promoted to error);
    • vec3 as float3 (--check + 13 asserts + test);
    • y left unflipped (test);
    • an array read without its lane (compiler + test);
    • an in-place helper not handed its capture (compiler + test);
    • the dialect leaking into OpenGL's names (test);
    • MoltenVK's struct off by four, and an interface moved (comparison).
  • The existing bar:

    • frames_against: 59 frames against origin/main, 0 differ;
    • Khronos validation with sync at 1x and 4x (parity, zombie_city, street_drive, test_water_sky, test_crowd_render): 0 errors, 0 warnings;
    • full local ./ci.sh: green at e262954 (8 hardware skips); the run at bd52d7c failed only the leaks check on test_gpu_msl, fixed by e262954;
    • aether-linux-x64 lanes: lane_some 108 builds, 0 failed, 0 warnings (test_gpu_msl and test_gpu_lit added); lane_all gcc and clang 263 each, 0 failed, 0 warnings; test_gpu_msl and generate --check pass on Linux x86_64.
  • PR CI: 10/10 green. macos-2 timed out at 25 min on its first run, with every suite up to then ok (Step 6, the scene: per-camera and per-world blocks, the scene's shaders quoted #838 changed ae3d.gpu too and its macos-2 took 14 min). It passed on a rerun in 8m37s.

For Nic

  • Metal path: pick one of three.

    1. Our MSL through MoltenVK's MSL-source shader modules. This means a binding rule, not a backend, and frames are then held to the SPIR-V path.
    2. A Metal harness for the screen passes.
    3. The Metal backend #312 backend.

    Recommended: 1, then 3 after Metal backend #312's groundwork.

  • Binding model. It uses discrete arguments, not the argument buffers section 11 first mentioned.

  • xcrun metal was not run. It needs the Metal Toolchain download (xcodebuild -downloadComponent MetalToolchain). The device compiler, with stricter flags, stands in.

  • scene_rq_vk.frag is still hand-written. It has no MSL.

🤖 Generated with Claude Code

paul-hammant and others added 5 commits October 20, 2026 23:43
A third dialect beside Vulkan's and OpenGL's GLSL, from the same trees:
emit_msl, emit_vertex_msl and emit_screen_vertex_msl. The expressions and
statements are the GLSL emitter's, with a CALL_MSL column in the call table
(sample/read for the texture calls, rsqrt, atan2, the prelude's ae_mod and
ae_mat3) and struct literals as aggregates. Around them:

- Vulkan's conventions: clip z 0..1, the screen's y down, each vertex
  shader's position y negated as it returns it, so a Metal renderer draws
  with the Vulkan renderer's matrices and the frame comes out the same way.
- Nothing global: each block a struct in a buffer, each texture with its
  sampler (ae_smp_<name>), each storage buffer a pointer; the entry's first
  lines name each capture as a reference into its block, so the body reads
  it where it uses it, as the GLSL does. A helper reading in place is handed
  what it reads by name, and so is each helper calling it; a helper whose
  own name would hide one is refused (MSL only).
- std140 by hand: each block struct member by member at ae3d.gpu's offsets,
  padded between (vec3 a packed_float3, scalar arrays float4 arrays read
  through their lanes, an element struct with a maker for its literals),
  every offset and size a static_assert.
- Varyings at [[user(locnK)]] (an int [[flat]]), attributes at the table's
  [[attribute(n)]] (a mat4 four float4s), colour outputs four lanes wide.
- Bindings: set N's block at buffer(N), storage buffers at 8 + k, textures
  and samplers at their capture order.

Nothing the GLSL dialects write changes (the dialect is set only while MSL
is written; names MSL keeps are escaped then alone). tests/test_gpu_msl
holds each rule, the offsets to the GLSL block's, and the GLSL and OpenGL's
names unchanged by writing MSL; negative controls for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…--check

Every quoted shader's stages written as MSL beside their GLSL, 41 files,
which --check compares byte for byte as it does the GLSL (95 generated
files now). The GLSL, ae3d.vkscene and ae3d.vkspirv are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/mslc.m compiles MSL through the device (newLibraryWithSource), behind
pragmas making every clang warning an error but four groups that say
nothing about a shader (C++98 compatibility, padding, Metal 4's deprecated
vertex/fragment keywords, unused parameters). The runtime compiler reports
diagnostics only when a source fails, hence the pragmas.
scripts/msl_check.sh builds it and runs it over src/ae3d/metal, and runs
`xcrun metal -Werror` too where Xcode's Metal Toolchain is installed (it is
a separate download, not on the M6). ./ci.sh runs it on macOS after the
generator's check; each block struct's static_asserts make this a check of
the MSL's std140 layout as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Section 9 item 5 and section 11's "What it buys MSL": the dialect's rules,
how it is held (--check, msl_check, test_gpu_msl, MoltenVK's own MSL of the
same SPIR-V: interfaces equal, 506 offsets asserted on its structs), and
three ways to a Metal path (#312), smallest first, with a recommendation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
./ci.sh's leaks run counted the six shaders the suite made and never gave
back (843 allocations under them). Each is freed now: 0 leaks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paul-hammant
paul-hammant merged commit 1f006d0 into main Oct 21, 2026
19 of 20 checks passed
@paul-hammant
paul-hammant deleted the step5-msl branch October 21, 2026 05:26
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