feat(render3d): Vulkan in a macOS window through MoltenVK; window built-ins take a slice's elements; the NEAR_FADE SPIR-V

- a CAMetalLayer on the view (cocoa.ll win_metal_layer), VK_EXT_metal_surface, QuartzCore linked
  with Vk.*; the drawable measured after the layer sets the backing scale
- vk_mac.ll opens MoltenVK directly after any loader: a bundle ships only libMoltenVK.dylib
- a covered window is not presented to (win_visible); one frame in flight on macOS
  (R3D_VK_INFLIGHT), with images, buffers and descriptor pools held until it is done
- win_held / win_mouse / win_pad / win_touch / win_text / win_present pass slice elements (arg_buf):
  every windowed program died on its first input poll
- variants.list and SPIR-V for the three NEAR_FADE foliage programs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 00:52:45 +03:00
parent 9862ec82e3
commit 38d4ea72b1
25 changed files with 627 additions and 164 deletions

View file

@ -1607,6 +1607,14 @@ go:
%scale = select i1 %ison, i32 %bsv, i32 1
store i32 %scale, ptr @W_glscale
call void @win_gl_update()
; a Metal layer (Vulkan) draws at the same scale
%ml = load ptr, ptr @W_mtl
%noml = icmp eq ptr %ml, null
br i1 %noml, label %out, label %mtl
mtl:
%scd = sitofp i32 %scale to double
%sel_csc = call ptr @sel_registerName(ptr @.s_setcsc)
%rm = call ptr (ptr, ptr, double) @objc_msgSend(ptr %ml, ptr %sel_csc, double %scd)
br label %out
out:
ret void
@ -2035,3 +2043,81 @@ define ptr @win_native_instance() {
entry:
ret ptr null
}
; ---- a Metal layer on the view, for Vulkan through MoltenVK -----------------------------
; VK_EXT_metal_surface presents to a CAMetalLayer, so the view is made layer-hosting with one
; (the layer set before wantsLayer, which is what makes the view host it rather than back it).
; Its contentsScale is the window's backing scale, or the swapchain's pixels are stretched over
; a Retina window's points; @W_glscale follows it, as win_gl_attach sets it for OpenGL. The class
; is looked up by name: the program links QuartzCore when it uses Vk.* (null without it).
; win_metal_layer() -> CAMetalLayer* | null (no window, or no QuartzCore)
@.c_mtllayer = private unnamed_addr constant [13 x i8] c"CAMetalLayer\00"
@.s_layer = private unnamed_addr constant [6 x i8] c"layer\00"
@.s_retain = private unnamed_addr constant [7 x i8] c"retain\00"
@.s_setlayer = private unnamed_addr constant [10 x i8] c"setLayer:\00"
@.s_wantsl = private unnamed_addr constant [15 x i8] c"setWantsLayer:\00"
@.s_setcsc = private unnamed_addr constant [18 x i8] c"setContentsScale:\00"
@W_mtl = internal global ptr null
define ptr @win_metal_layer() {
entry:
%have = load ptr, ptr @W_mtl
%got = icmp ne ptr %have, null
br i1 %got, label %done, label %make
make:
%view = load ptr, ptr @W_view
%win = load ptr, ptr @W_win
%noview = icmp eq ptr %view, null
%nowin = icmp eq ptr %win, null
%nothing = or i1 %noview, %nowin
br i1 %nothing, label %none, label %cls
cls:
%c = call ptr @objc_getClass(ptr @.c_mtllayer)
%noc = icmp eq ptr %c, null
br i1 %noc, label %none, label %go
go:
%sel_l = call ptr @sel_registerName(ptr @.s_layer)
%l0 = call ptr @objc_msgSend(ptr %c, ptr %sel_l)
%sel_r = call ptr @sel_registerName(ptr @.s_retain)
%l = call ptr @objc_msgSend(ptr %l0, ptr %sel_r)
%sel_bs = call ptr @sel_registerName(ptr @.s_bscale)
%bs = call double (ptr, ptr) @objc_msgSend(ptr %win, ptr %sel_bs)
%bsok = fcmp ogt double %bs, 0.0
%scale = select i1 %bsok, double %bs, double 1.0
%sel_cs = call ptr @sel_registerName(ptr @.s_setcsc)
%r0 = call ptr (ptr, ptr, double) @objc_msgSend(ptr %l, ptr %sel_cs, double %scale)
%sel_sl = call ptr @sel_registerName(ptr @.s_setlayer)
%r1 = call ptr (ptr, ptr, ptr) @objc_msgSend(ptr %view, ptr %sel_sl, ptr %l)
%sel_wl = call ptr @sel_registerName(ptr @.s_wantsl)
%r2 = call ptr (ptr, ptr, i8) @objc_msgSend(ptr %view, ptr %sel_wl, i8 1)
%si = fptosi double %scale to i32
store i32 %si, ptr @W_glscale
store ptr %l, ptr @W_mtl
ret ptr %l
done:
ret ptr %have
none:
ret ptr null
}
; ---- is the window on screen ----------------------------------------------------------------------
; A covered or minimised window's CAMetalLayer hands out drawables at the compositor's pace (a
; second's timeout each), so a Vulkan frame waiting for one stalled the whole game at 3 fps behind
; another window. NSWindowOcclusionStateVisible is bit 1 of -occlusionState.
; win_visible() -> 1 | 0 (1 with no window)
@.s_occl = private unnamed_addr constant [15 x i8] c"occlusionState\00"
define i32 @win_visible() {
entry:
%win = load ptr, ptr @W_win
%nowin = icmp eq ptr %win, null
br i1 %nowin, label %yes, label %ask
ask:
%sel = call ptr @sel_registerName(ptr @.s_occl)
%st = call i64 (ptr, ptr) @objc_msgSend(ptr %win, ptr %sel)
%bit = and i64 %st, 2
%vis = icmp ne i64 %bit, 0
%r = zext i1 %vis to i32
ret i32 %r
yes:
ret i32 1
}

View file

@ -398,6 +398,15 @@ define weak ptr @win_native_instance() {
entry:
ret ptr null
}
define weak ptr @win_metal_layer() {
entry:
ret ptr null
}
; the window is always drawn to here (cocoa.ll asks macOS whether it is covered)
define weak i32 @win_visible() {
entry:
ret i32 1
}
; the drawable's size, for a graphics API that follows the window without a GL context (render3d's
; Vulkan resize check); headless there is no window, so 0 x 0. cocoa.ll's real one wins when linked.
; win_set_title: cocoa.ll's in a windowed build; nothing to retitle headless

View file

@ -52,6 +52,16 @@ define ptr @win_native_instance() {
entry:
ret ptr null
}
; macOS only (cocoa.ll): the CAMetalLayer MoltenVK presents to
define ptr @win_metal_layer() {
entry:
ret ptr null
}
; the window is always drawn to here (cocoa.ll asks macOS whether it is covered)
define i32 @win_visible() {
entry:
ret i32 1
}
define void @win_set_title(ptr %title) {
entry:

View file

@ -4,8 +4,8 @@
; macOS has no Vulkan of its own: the LunarG loader and MoltenVK (Vulkan over
; Metal) come with the Vulkan SDK or with a game that bundles them. So the loader
; is looked for at run time - beside the executable, in the usual library
; directories, then under $VULKAN_SDK - and a Mac without it gets 0 from vk_open
; and stays on OpenGL. The contract is vk_win.ll's:
; directories, then MoltenVK on its own (a bundle's Contents/Frameworks), then under
; $VULKAN_SDK - and a Mac without any gets 0 from vk_open and stays on OpenGL. The contract is vk_win.ll's:
;
; lvk_open() -> 1 | 0 lvk_sym(name) -> ptr lvk_has(name) -> 1 | 0
;
@ -25,6 +25,10 @@ declare i32 @lvk_bind()
@.lvk_c3 = private unnamed_addr constant [36 x i8] c"/opt/homebrew/lib/libvulkan.1.dylib\00"
@.lvk_env = private unnamed_addr constant [11 x i8] c"VULKAN_SDK\00"
@.lvk_fmt = private unnamed_addr constant [25 x i8] c"%s/lib/libvulkan.1.dylib\00"
@.lvk_fmtm = private unnamed_addr constant [25 x i8] c"%s/lib/libMoltenVK.dylib\00"
@.lvk_m0 = private unnamed_addr constant [49 x i8] c"@executable_path/../Frameworks/libMoltenVK.dylib\00"
@.lvk_m1 = private unnamed_addr constant [35 x i8] c"@executable_path/libMoltenVK.dylib\00"
@.lvk_m2 = private unnamed_addr constant [18 x i8] c"libMoltenVK.dylib\00"
@lvk_lib = internal global ptr null
; RTLD_NOW (2) | RTLD_LOCAL (4)
@ -34,52 +38,51 @@ entry:
ret ptr %h
}
; the candidates in order: a Vulkan loader first (the SDK's, so the validation layer can be
; stacked in development), then MoltenVK itself, which exports every vk* entry point and is what
; a bundle ships in Contents/Frameworks - no loader, no ICD json.
@lvk_paths = internal constant [7 x ptr] [ptr @.lvk_c0, ptr @.lvk_c1, ptr @.lvk_c2, ptr @.lvk_c3, ptr @.lvk_m0, ptr @.lvk_m1, ptr @.lvk_m2]
define i32 @lvk_open() {
entry:
%have = load ptr, ptr @lvk_lib
%open = icmp ne ptr %have, null
br i1 %open, label %ok, label %t0
t0:
%h0 = call ptr @lvk_try(ptr @.lvk_c0)
%n0 = icmp eq ptr %h0, null
br i1 %n0, label %t1, label %got0
got0:
store ptr %h0, ptr @lvk_lib
br i1 %open, label %ok, label %loop
loop:
%i = phi i32 [ 0, %entry ], [ %i1, %next ]
%slot = getelementptr [7 x ptr], ptr @lvk_paths, i32 0, i32 %i
%path = load ptr, ptr %slot
%h = call ptr @lvk_try(ptr %path)
%miss = icmp eq ptr %h, null
br i1 %miss, label %next, label %got
next:
%i1 = add i32 %i, 1
%more = icmp slt i32 %i1, 7
br i1 %more, label %loop, label %sdk
got:
store ptr %h, ptr @lvk_lib
br label %bind
t1:
%h1 = call ptr @lvk_try(ptr @.lvk_c1)
%n1 = icmp eq ptr %h1, null
br i1 %n1, label %t2, label %got1
got1:
store ptr %h1, ptr @lvk_lib
br label %bind
t2:
%h2 = call ptr @lvk_try(ptr @.lvk_c2)
%n2 = icmp eq ptr %h2, null
br i1 %n2, label %t3, label %got2
got2:
store ptr %h2, ptr @lvk_lib
br label %bind
t3:
%h3 = call ptr @lvk_try(ptr @.lvk_c3)
%n3 = icmp eq ptr %h3, null
br i1 %n3, label %t4, label %got3
got3:
store ptr %h3, ptr @lvk_lib
br label %bind
t4:
%sdk = call ptr @getenv(ptr @.lvk_env)
%nosdk = icmp eq ptr %sdk, null
br i1 %nosdk, label %fail, label %t4b
t4b:
sdk:
%sdkp = call ptr @getenv(ptr @.lvk_env)
%nosdk = icmp eq ptr %sdkp, null
br i1 %nosdk, label %fail, label %s1
s1:
%buf = alloca [1024 x i8]
%w = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buf, i64 1024, ptr @.lvk_fmt, ptr %sdk)
%w = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buf, i64 1024, ptr @.lvk_fmt, ptr %sdkp)
%h4 = call ptr @lvk_try(ptr %buf)
%n4 = icmp eq ptr %h4, null
br i1 %n4, label %fail, label %got4
br i1 %n4, label %s2, label %got4
s2:
%w2 = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buf, i64 1024, ptr @.lvk_fmtm, ptr %sdkp)
%h5 = call ptr @lvk_try(ptr %buf)
%n5 = icmp eq ptr %h5, null
br i1 %n5, label %fail, label %got5
got4:
store ptr %h4, ptr @lvk_lib
br label %bind
got5:
store ptr %h5, ptr @lvk_lib
br label %bind
bind:
%n = call i32 @lvk_bind()
br label %ok

View file

@ -547,6 +547,16 @@ entry:
%inst = call ptr @GetModuleHandleA(ptr null)
ret ptr %inst
}
; macOS only (cocoa.ll): the CAMetalLayer MoltenVK presents to
define ptr @win_metal_layer() {
entry:
ret ptr null
}
; the window is always drawn to here (cocoa.ll asks macOS whether it is covered)
define i32 @win_visible() {
entry:
ret i32 1
}
; set (%on != 0) or clear a key's bit in the 256-bit held set
define void @win_held_bit(i32 %k, i32 %on) {