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

@ -43,6 +43,11 @@ function gvk_ext_in(props: bytes, n: int, want: string) -> bool {
# device with the Tier 1 floor switched on. False, with gvk_why set, if any of it is missing:
# the caller stays on OpenGL.
function gvk_surface_ext() -> string {
if Os.platform() == "macos" { return VK_EXT_METAL_SURFACE_EXTENSION_NAME }
return VK_KHR_WIN32_SURFACE_EXTENSION_NAME
}
function gvk_init(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_ready { return true }
gsl_boot(render3d_st)
@ -72,12 +77,13 @@ function gvk_init(render3d_st: mut Render3dState) -> bool {
Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); n_iext += 1
Vk.put_i32(ici, VkInstanceCreateInfo_flags, VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR)
}
# a window's surface: only the Win32 one is built, so a window elsewhere stays on OpenGL
render3d_st.gvk_has_surface = render3d_st.gvk_want_surface and gvk_ext_in(iexts, nie, VK_KHR_SURFACE_EXTENSION_NAME) and gvk_ext_in(iexts, nie, VK_KHR_WIN32_SURFACE_EXTENSION_NAME)
if render3d_st.gvk_want_surface and not render3d_st.gvk_has_surface { render3d_st.gvk_why = "no Win32 surface in this Vulkan instance"; return false }
# a window's surface: Win32 on Windows, Metal (MoltenVK) on macOS
let surf_ext = gvk_surface_ext()
render3d_st.gvk_has_surface = render3d_st.gvk_want_surface and gvk_ext_in(iexts, nie, VK_KHR_SURFACE_EXTENSION_NAME) and gvk_ext_in(iexts, nie, surf_ext)
if render3d_st.gvk_want_surface and not render3d_st.gvk_has_surface { render3d_st.gvk_why = `no {surf_ext} in this Vulkan instance`; return false }
if render3d_st.gvk_has_surface {
Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_SURFACE_EXTENSION_NAME); n_iext += 1
Vk.put_ptr(iext_names, n_iext * 8, VK_KHR_WIN32_SURFACE_EXTENSION_NAME); n_iext += 1
Vk.put_ptr(iext_names, n_iext * 8, surf_ext); n_iext += 1
}
# HDR output: an instance only lists the HDR colour spaces when it asks for them
render3d_st.gvk_has_colorspace = render3d_st.gvk_has_surface and gvk_ext_in(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)
@ -488,6 +494,9 @@ function gvk_cmd_init(render3d_st: mut Render3dState) -> bool {
render3d_st.gvk_fence = bytes(8)
r = Vk.create_fence(render3d_st.gvk_dev, fci, null, render3d_st.gvk_fence)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateFence", r) }
render3d_st.gvk_frame_fence = bytes(8)
r = Vk.create_fence(render3d_st.gvk_dev, fci, null, render3d_st.gvk_frame_fence)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateFence", r) }
return true
}
# a command buffer, begun
@ -510,6 +519,7 @@ function gvk_once_begin(render3d_st: Render3dState) -> pointer {
}
# end it, submit it, wait for it, free it
function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool {
gvk_frame_wait(render3d_st)
var r = Vk.end_command_buffer(cb)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) }
let cbs = bytes(8)
@ -529,12 +539,61 @@ function gvk_once_end(render3d_st: mut Render3dState, cb: pointer) -> bool {
return true
}
# ---- one frame in flight -------------------------------------------------------------------
# A window's frame is submitted and presented without waiting, so the game's next tick runs on the
# CPU while the GPU draws: waiting at the present serialised the two, which cost MoltenVK a third
# of its frame rate against OpenGL. Anything that would reuse what that frame reads - the next
# frame's command buffer and ring, a one-off submit, a descriptor pool reset, a swapchain rebuild -
# waits for it first (gvk_frame_wait), and a buffer it reads is busy (gvk_buf_busy). On by default
# on macOS; R3D_VK_INFLIGHT=0/1 says otherwise.
function gvk_inflight_on(render3d_st: mut Render3dState) -> bool {
if render3d_st.gvk_inflight < 0 {
render3d_st.gvk_inflight = 0
if Os.platform() == "macos" { render3d_st.gvk_inflight = 1 }
if r3d_env_has(render3d_st, "R3D_VK_INFLIGHT") { render3d_st.gvk_inflight = Text.to_int(r3d_env(render3d_st, "R3D_VK_INFLIGHT")) }
}
return render3d_st.gvk_inflight == 1
}
function gvk_frame_submit(render3d_st: mut Render3dState, cb: pointer) -> bool {
gvk_frame_wait(render3d_st)
var r = Vk.end_command_buffer(cb)
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkEndCommandBuffer", r) }
let cbs = bytes(8)
Vk.put_ptr(cbs, 0, cb)
Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_frame_fence)
let si = bytes(VkSubmitInfo_sizeof)
Vk.zero(si, VkSubmitInfo_sizeof)
Vk.put_i32(si, VkSubmitInfo_sType, VK_STRUCTURE_TYPE_SUBMIT_INFO)
Vk.put_i32(si, VkSubmitInfo_commandBufferCount, 1)
Vk.put_ptr(si, VkSubmitInfo_pCommandBuffers, cbs)
r = Vk.queue_submit(render3d_st.gvk_queue, 1, si, Vk.get_i64(render3d_st.gvk_frame_fence, 0))
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkQueueSubmit", r) }
render3d_st.gvk_frame_pending = true
render3d_st.gvk_frame_pending_cb = cb
return true
}
function gvk_frame_wait(render3d_st: mut Render3dState) -> void {
if not render3d_st.gvk_frame_pending { return }
let forever: long = -1
Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_frame_fence, 1, forever)
let cbs = bytes(8)
Vk.put_ptr(cbs, 0, render3d_st.gvk_frame_pending_cb)
Vk.free_command_buffers(render3d_st.gvk_dev, render3d_st.gvk_pool, 1, cbs)
render3d_st.gvk_frame_pending = false
render3d_st.gvk_frame_pending_cb = null
gvk_retire_flush(render3d_st)
}
# ---- teardown -----------------------------------------------------------------------------
function gvk_shutdown(render3d_st: mut Render3dState) -> void {
if not render3d_st.gvk_ready { return }
gvk_frame_wait(render3d_st)
Vk.device_wait_idle(render3d_st.gvk_dev)
gsl_shutdown(render3d_st)
Vk.destroy_fence(render3d_st.gvk_dev, Vk.get_i64(render3d_st.gvk_fence, 0), null)
Vk.destroy_fence(render3d_st.gvk_dev, Vk.get_i64(render3d_st.gvk_frame_fence, 0), null)
Vk.destroy_command_pool(render3d_st.gvk_dev, render3d_st.gvk_pool, null)
Vk.destroy_device(render3d_st.gvk_dev, null)
Vk.destroy_instance(render3d_st.gvk_inst, null)