From 38d4ea72b18b299ce2b895b2d7e3b21868ce1d41 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 00:52:45 +0300 Subject: [PATCH] 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 --- changes/near-fade-spirv.md | 4 + changes/vulkan-on-macos.md | 10 + changes/window-buffers.md | 7 + packages/ludic.render3d/env.ludic | 4 + packages/ludic.render3d/gpu.ludic | 5 +- packages/ludic.render3d/gpu_vk.ludic | 67 ++- packages/ludic.render3d/gpu_vk_draw.ludic | 50 ++- packages/ludic.render3d/gpu_vk_res.ludic | 16 +- .../shaders/spv/266d2401.frag.spv | Bin 0 -> 4888 bytes .../shaders/spv/266d2401.vert.spv | Bin 0 -> 16864 bytes .../shaders/spv/5e70d731.frag.spv | Bin 0 -> 46456 bytes .../shaders/spv/5e70d731.vert.spv | Bin 0 -> 16864 bytes .../shaders/spv/b603a056.frag.spv | Bin 0 -> 45244 bytes .../shaders/spv/b603a056.vert.spv | Bin 0 -> 16864 bytes .../ludic.render3d/shaders/spv/manifest.txt | 406 +++++++++++++----- packages/ludic.render3d/shaders/variants.list | 7 +- runtime/native/cocoa.ll | 86 ++++ runtime/native/gl.ll | 9 + runtime/native/gl_win_nowin.ll | 10 + runtime/native/vk_mac.ll | 77 ++-- runtime/native/win32.ll | 10 + selfhost/backend/emit_intrin.ludic | 6 + selfhost/backend/emit_intrin2.ludic | 12 +- selfhost/main.ludic | 2 + tools/ludic-cli/build.ludic | 3 +- 25 files changed, 627 insertions(+), 164 deletions(-) create mode 100644 changes/near-fade-spirv.md create mode 100644 changes/vulkan-on-macos.md create mode 100644 changes/window-buffers.md create mode 100644 packages/ludic.render3d/shaders/spv/266d2401.frag.spv create mode 100644 packages/ludic.render3d/shaders/spv/266d2401.vert.spv create mode 100644 packages/ludic.render3d/shaders/spv/5e70d731.frag.spv create mode 100644 packages/ludic.render3d/shaders/spv/5e70d731.vert.spv create mode 100644 packages/ludic.render3d/shaders/spv/b603a056.frag.spv create mode 100644 packages/ludic.render3d/shaders/spv/b603a056.vert.spv diff --git a/changes/near-fade-spirv.md b/changes/near-fade-spirv.md new file mode 100644 index 00000000..fd4161e3 --- /dev/null +++ b/changes/near-fade-spirv.md @@ -0,0 +1,4 @@ +bump: patch +type: fix +**The foliage's near-fade programs have SPIR-V.** The three `NEAR_FADE` variants were missing +from `variants.list`, so on Vulkan the trees' and cards' programs failed to build. diff --git a/changes/vulkan-on-macos.md b/changes/vulkan-on-macos.md new file mode 100644 index 00000000..29c65201 --- /dev/null +++ b/changes/vulkan-on-macos.md @@ -0,0 +1,10 @@ +bump: minor +type: feature +**render3d's Vulkan renderer runs in a macOS window, through MoltenVK.** `R3D_GFX=vk` (or +`gpu_request("vulkan")`) now presents to a `CAMetalLayer` on the window (`VK_EXT_metal_surface`, +the runtime's `win_metal_layer()`), where it used to fall back to OpenGL. The loader also opens +MoltenVK on its own - `Contents/Frameworks/libMoltenVK.dylib` in a bundle, beside the executable, +or `$VULKAN_SDK/lib` - so a game needs no Vulkan SDK to ship it. A covered window is not drawn to +(its layer would hold every frame for a second), and on macOS one frame stays in flight so the CPU +runs the next tick while the GPU draws. OpenGL stays the default: on an M4 Pro the Vulkan frame is +still about a third slower, most of it in the alpha-tested foliage. diff --git a/changes/window-buffers.md b/changes/window-buffers.md new file mode 100644 index 00000000..edec5a2a --- /dev/null +++ b/changes/window-buffers.md @@ -0,0 +1,7 @@ +bump: patch +type: fix +**A windowed program's input no longer stops it on the first frame.** Since slices carry their +length, the window's built-ins (`win_held`, `win_mouse`, `win_pad`, `win_touch`, `win_text`, +`win_present`) were handed the slice's header instead of its elements, so the platform wrote the +mouse position over the length and the next read failed its bounds check. They take the elements, +as an `extern` call already did. diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 047de807..d99143a5 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -199,6 +199,10 @@ export state Render3dState { gvk_al_spare: []int = null # ids to reuse gvk_pool: long = 0 gvk_fence: bytes = null + gvk_frame_fence: bytes = null # the presented frame still on the GPU (one frame in flight) + gvk_frame_pending: bool = false + gvk_frame_pending_cb: pointer = null + gvk_inflight: int = -1 # -1 until asked: 1 leaves the presented frame in flight gvk_has_hqr: bool = false gvk_ts_period: float = 0.0 # float bits: nanoseconds per timestamp tick gvk_qpool: long = 0 diff --git a/packages/ludic.render3d/gpu.ludic b/packages/ludic.render3d/gpu.ludic index c99478d1..90489d07 100644 --- a/packages/ludic.render3d/gpu.ludic +++ b/packages/ludic.render3d/gpu.ludic @@ -49,9 +49,10 @@ function gpu_select(render3d_st: mut Render3dState) -> int { if render3d_st.gpu_wanted == 0 { render3d_st.gpu_wanted = GPU_GL } render3d_st.gpu_kind = GPU_GL if render3d_st.gpu_wanted == GPU_VK { - # a window needs a surface, and only the Win32 one is built + # a window needs a surface: Win32 on Windows, a CAMetalLayer through MoltenVK on macOS render3d_st.gvk_want_surface = is_windowed() - if is_windowed() and Os.platform() != "windows" { render3d_st.gpu_fallback_reason = "the Vulkan window is built for Windows only" } + let os = Os.platform() + if is_windowed() and os != "windows" and os != "macos" { render3d_st.gpu_fallback_reason = `no Vulkan window is built for {os}` } else if not gvk_init(render3d_st) { render3d_st.gpu_fallback_reason = render3d_st.gvk_why } else if not gvk_manifest(render3d_st) { render3d_st.gpu_fallback_reason = "the renderer's SPIR-V manifest is missing" } else { render3d_st.gpu_kind = GPU_VK } diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index 69743591..2793f781 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -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) diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index aaf0f55e..8efb6e85 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -729,6 +729,7 @@ function gvk_sc_clear(render3d_st: mut Render3dState) -> void { # the pool is full: finish the frame recorded so far (it may use sets from it), then start again function gvk_kpool_reset(render3d_st: mut Render3dState) -> void { gvk_flush(render3d_st) + gvk_frame_wait(render3d_st) Vk.reset_descriptor_pool(render3d_st.gvk_dev, render3d_st.gvk_kpool, 0) gvk_sc_clear(render3d_st) } @@ -953,6 +954,7 @@ function gvk_screen_make(render3d_st: mut Render3dState, w: int, h: int) -> bool function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer { if render3d_st.gvk_cb == null { + gvk_frame_wait(render3d_st) gvk_frame_reset(render3d_st) render3d_st.gvk_cb = gvk_once_begin(render3d_st) } @@ -1279,7 +1281,13 @@ function gvk_flush(render3d_st: mut Render3dState) -> void { # - and that image is presented. function gvk_present(render3d_st: mut Render3dState) -> void { gvk_prof_frame(render3d_st) - if render3d_st.gvk_swap != 0 { gvk_present_window(render3d_st); render3d_st.gvk_frame_no += 1; gvk_retire_flush(render3d_st); return } + if render3d_st.gvk_swap != 0 { + gvk_present_window(render3d_st) + render3d_st.gvk_frame_no += 1 + # what the frame in flight retired goes when it is done (gvk_frame_wait) + if not render3d_st.gvk_frame_pending { gvk_retire_flush(render3d_st) } + return + } if render3d_st.gvk_cb == null { return } gvk_pass_end(render3d_st) gvk_once_end(render3d_st, render3d_st.gvk_cb) @@ -1332,6 +1340,8 @@ function gvk_open(render3d_st: mut Render3dState, w: int, h: int, title: string) if is_windowed() { win_open(w, h, 1, title) # the window exists before main: this retitles it win_gl_resize(w, h) + # macOS: the Metal layer sets the backing scale, so it exists before the drawable is measured + if Os.platform() == "macos" and win_metal_layer() == null { render3d_st.gvk_why = "no Metal layer on the window"; return false } if render3d_st.gvk_size_buf == null { render3d_st.gvk_size_buf = words(4) } win_gl_drawable(render3d_st.gvk_size_buf) if render3d_st.gvk_size_buf[0] > 0 and render3d_st.gvk_size_buf[1] > 0 { gl_set_drawable(render3d_st.gvk_size_buf[0], render3d_st.gvk_size_buf[1]) } @@ -1519,13 +1529,24 @@ function gvk_read_screen(render3d_st: mut Render3dState, w: int, h: int, out: po } # ---- the window: surface and swapchain ---------------------------------------------------------- -# Windows only for now (the Win32 surface); gpu_select keeps a window elsewhere on OpenGL. +# The Win32 surface on Windows; on macOS a CAMetalLayer the runtime hangs off the view (MoltenVK). extern function win_native_window() -> pointer = "win_native_window" extern function win_native_instance() -> pointer = "win_native_instance" +extern function win_metal_layer() -> pointer = "win_metal_layer" +extern function win_visible() -> int = "win_visible" - -function gvk_surface_make(render3d_st: mut Render3dState) -> bool { - if render3d_st.gvk_surface != 0 { return true } +function gvk_surface_os(render3d_st: mut Render3dState, out: pointer) -> bool { + if Os.platform() == "macos" { + let layer = win_metal_layer() + if layer == null { render3d_st.gvk_why = "no Metal layer on the window"; return false } + let mci = bytes(VkMetalSurfaceCreateInfoEXT_sizeof) + Vk.zero(mci, VkMetalSurfaceCreateInfoEXT_sizeof) + Vk.put_i32(mci, VkMetalSurfaceCreateInfoEXT_sType, VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT) + Vk.put_ptr(mci, VkMetalSurfaceCreateInfoEXT_pLayer, layer) + let rm = Vk.create_metal_surface_ext(render3d_st.gvk_inst, mci, null, out) + if rm != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateMetalSurfaceEXT", rm) } + return true + } let hwnd = win_native_window() if hwnd == null { render3d_st.gvk_why = "no window to present to"; return false } let sci = bytes(VkWin32SurfaceCreateInfoKHR_sizeof) @@ -1533,9 +1554,15 @@ function gvk_surface_make(render3d_st: mut Render3dState) -> bool { Vk.put_i32(sci, VkWin32SurfaceCreateInfoKHR_sType, VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR) Vk.put_ptr(sci, VkWin32SurfaceCreateInfoKHR_hinstance, win_native_instance()) Vk.put_ptr(sci, VkWin32SurfaceCreateInfoKHR_hwnd, hwnd) - let out = bytes(8) let r = Vk.create_win32_surface_khr(render3d_st.gvk_inst, sci, null, out) if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateWin32SurfaceKHR", r) } + return true +} + +function gvk_surface_make(render3d_st: mut Render3dState) -> bool { + if render3d_st.gvk_surface != 0 { return true } + let out = bytes(8) + if not gvk_surface_os(render3d_st, out) { return false } render3d_st.gvk_surface = gvk_handle(out) let ok = bytes(4) Vk.put_i32(ok, 0, 0) @@ -1551,6 +1578,7 @@ function gvk_surface_make(render3d_st: mut Render3dState) -> bool { # (Re)make the swapchain for a w x h client area. The old one is handed over and then destroyed. function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool { if not gvk_surface_make(render3d_st) { return false } + gvk_frame_wait(render3d_st) Vk.device_wait_idle(render3d_st.gvk_dev) let caps = bytes(VkSurfaceCapabilitiesKHR_sizeof) Vk.get_physical_device_surface_capabilities_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, caps) @@ -1641,6 +1669,14 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool { function gvk_present_window(render3d_st: mut Render3dState) -> void { let cb = gvk_frame_cb(render3d_st) gvk_pass_end(render3d_st) + # a covered window is not drawn to: its layer would hold the frame for a drawable the compositor + # hands back once a second. The frame still runs, paced at about 60 Hz, so the game keeps time. + if win_visible() == 0 { + if gvk_inflight_on(render3d_st) { gvk_frame_submit(render3d_st, cb) } else { gvk_once_end(render3d_st, cb) } + render3d_st.gvk_cb = null + Time.sleep_us(16000) + return + } if render3d_st.gvk_swap_stale { gvk_once_end(render3d_st, cb); render3d_st.gvk_cb = null; gvk_swap_make(render3d_st, render3d_st.gvk_swap_w, render3d_st.gvk_swap_h); return } let idx = bytes(4) Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence) @@ -1670,7 +1706,7 @@ function gvk_present_window(render3d_st: mut Render3dState) -> void { Vk.cmd_blit_image(cb, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR) gvk_barrier(cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) gvk_barrier(cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR) - gvk_once_end(render3d_st, cb) + if gvk_inflight_on(render3d_st) { gvk_frame_submit(render3d_st, cb) } else { gvk_once_end(render3d_st, cb) } render3d_st.gvk_cb = null let pi = bytes(VkPresentInfoKHR_sizeof) Vk.zero(pi, VkPresentInfoKHR_sizeof) diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index e17a6f15..298cb72c 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -134,6 +134,7 @@ function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> poin return Vk.get_ptr(pp, 0) } function gvk_staging_free(render3d_st: mut Render3dState) -> void { + gvk_frame_wait(render3d_st) # the frame in flight may still read it Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem) Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_st_buf, null) Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, null) @@ -142,6 +143,7 @@ function gvk_staging_free(render3d_st: mut Render3dState) -> void { # (Re)make the image behind a handle: glTexImage2D on a texture that already has one replaces it. function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool, ifmt: int, w: int, h: int, layers: int, with_mips: bool) -> bool { + gvk_frame_wait(render3d_st) # the frame in flight may still read it if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) { return false } let vkfmt = gvk_format(ifmt) if vkfmt == VK_FORMAT_UNDEFINED { print(`r3d: vulkan: no image format for GL format {ifmt}`); return false } @@ -304,6 +306,7 @@ function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w: # A target made without pixels has one level; the renderer asking it for mipmaps (the exposure # measure reads the HDR scene's smallest level every frame) grows it a full chain, level 0 kept. function gvk_tex_grow_mips(render3d_st: mut Render3dState, tex: int, w: int, h: int) -> bool { + gvk_frame_wait(render3d_st) # the frame in flight may still read it let old_image = render3d_st.gvk_tex_image[tex] let old_view = render3d_st.gvk_tex_view[tex] let old_mem = render3d_st.gvk_tex_mem[tex] @@ -420,6 +423,7 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in } function gvk_tex_release(render3d_st: mut Render3dState, tex: int) -> void { + gvk_frame_wait(render3d_st) # the frame in flight may still read it if render3d_st.gvk_tex_image[tex] == 0 { return } let zero: long = 0 Vk.destroy_image_view(render3d_st.gvk_dev, render3d_st.gvk_tex_view[tex], null) @@ -553,8 +557,8 @@ function gvk_buf_new(render3d_st: mut Render3dState) -> int { function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { if render3d_st.gvk_buf[b] == 0 { return } let zero: long = 0 - if render3d_st.gvk_buf_used[b] == render3d_st.gvk_frame_no { - # a draw recorded this frame still reads it: destroy it once the frame has been submitted + if gvk_buf_busy(render3d_st, b) { + # a draw recorded this frame (or the frame in flight) still reads it: destroy it once the frame has been submitted push(render3d_st.gvk_retired_buf, render3d_st.gvk_buf[b]); push(render3d_st.gvk_retired_mem, render3d_st.gvk_buf_mem[b]) } else { Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null) @@ -562,6 +566,12 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { } render3d_st.gvk_buf[b] = zero; render3d_st.gvk_buf_mem[b] = zero; render3d_st.gvk_buf_size[b] = 0; render3d_st.gvk_buf_map[b] = null; render3d_st.gvk_buf_used[b] = 0 } +# read by a draw recorded this frame, or by the presented frame still on the GPU +function gvk_buf_busy(render3d_st: Render3dState, b: int) -> bool { + let u = render3d_st.gvk_buf_used[b] + return u == render3d_st.gvk_frame_no or (render3d_st.gvk_frame_pending and u == render3d_st.gvk_frame_no - 1) +} + # the frame's submitted work is done: storage retired during it can go function gvk_retire_flush(render3d_st: mut Render3dState) -> void { if render3d_st.gvk_retired_buf == null { return } @@ -587,7 +597,7 @@ function gvk_buf_is_gpu(render3d_st: Render3dState, b: int) -> bool { return ren function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool { if b <= 0 or b >= len(render3d_st.gvk_buf) { return false } # already big enough, and no draw this frame reads what is there: fill it in place - if render3d_st.gvk_buf[b] != 0 and render3d_st.gvk_buf_size[b] >= n and (render3d_st.gvk_buf_used[b] != render3d_st.gvk_frame_no or gvk_buf_is_gpu(render3d_st, b)) { return true } + if render3d_st.gvk_buf[b] != 0 and render3d_st.gvk_buf_size[b] >= n and (not gvk_buf_busy(render3d_st, b) or gvk_buf_is_gpu(render3d_st, b)) { return true } gvk_buf_release(render3d_st, b) var size = n if size < 64 { size = 64 } diff --git a/packages/ludic.render3d/shaders/spv/266d2401.frag.spv b/packages/ludic.render3d/shaders/spv/266d2401.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..ab9042752c819d126000371044c97020737a9b2c GIT binary patch literal 4888 zcmZveOOF**5XT#a89+uJ0)nD=fdK(WL`1{~Fask4iabPojy65__Pw`5&+YL(2JitI zW8%U@7j7^t`~=3GE{yR5U?LkgZdkcAZhS@)|G(2+eNWJn)SRk+)u~gbx~k?5%-^&y z2<8P#fYqdA>|A($HtV2Y6s(opNW+P^9`(}hnO0KI z+A{~!ta>g`bPJtcCP6Q(MxAO@i^I8DWYh7UzOFNyCf!m;9DQ9oYBl0g#|!(qZhRq5 zr5keh5pyr>^jcvpX^S^}u*9obnzc)OsPHutx@QT`-$g!p;cPpuCuuishiN<)r+T-= zzR2n8StG2)txnRtD0xU3A}8dj7ouy%Nu${f>rt9!^}68l-djqW>8KO;@g)wgrpauG z%ksWqhj(VGgDhv&Ip>hMN8>$PfOluk=5{Vlsm7dDepx7^n? z6BmfRd&Nz<^Ui0U``er@nNEukE%z%OUW+cKWTPMVDu>sTcAO{Q?pWQ&^N%IjeBJJJ z%}6hDAI#3I>Er)7!#C-L?u;u6^L0lbuV$@&ecjo|+i_HFN-XbR=XBjpn6;CJTFQLg zWq316`Orh^5bnL(@C@7hyR3JZnxz_BXOy~ooUWQ?y;|65Mzu@{HeZ$8S2E-3CHJ+V z@YM*DmI|Y*T9tkNJvNrQTHJ}cifAC)N;b6+ckIJU_ia8zl+Rp=V0)*!}Rh32D_AFouUPUN%^8L$vq31 zdL0st6*6^-O#P#$K7E+;QVMjPDHg zy@xS(z;1XLGY9M!597OosTIoa!JGjb_AvSoY`KRqTfo+O7;^=z;$h4bun`ZVuEDl? z7_$Uy)Wa$Tw$sBl6xePL+gM)EP3fgnHl`B9Ck5yxf(t zf1WwQd8N|nnGx7i-<(JG?U@zmsde(-uxl^dgcgvix=6~GgHvhBlt)5^_L1g z{em9Z^ec7ie5dCZRUX6b;<&3KS|fY>BBQTJ#?L$v_~V64-+`@`Outn`tosU?{G#VP zeFl$D{F8Th^0!uh=^HR)J5S$`zq^FeFW7(~+blHMW}(S83z4~x{@^~|O}z9*MMO;N zgUxOpatD6##d+Rp#M%EN{N8MXjIH7RZayp?WMVPgc5+5v5-&YV&XK8eFml8>dXhY2 zPcNdw$0~937w5?Rh={jx4*eF<`EM>C;M{$Mj{IYfOn>H=Et!Km2+7BzHU^o;+~J9r{nVUpsMdhrdi-r^-7Z1$LYtD~!O15qZ48 z&>s@dSz1->*BUBQQIERJ?!wDao8qbRK_Ci?9JdRpjHD z0@vnzQj1{J(WLg1BJMfi_Po|km3S^cCz*4@$_crDUPM001-dgL%l`}FnQPd*Tg339 zcxE3uvwKNA_nbXH4K;=$f4{J$n1>{XH3gyONeuG|yzSrM4s zH7(xWfG!e!Q248g$0s)ShFCm!O+-xeESTL_7Y}B&)esMcJ@t}kXHhTcnj$dj1>Wif K>>ow8SM)E%wzzHp literal 0 HcmV?d00001 diff --git a/packages/ludic.render3d/shaders/spv/266d2401.vert.spv b/packages/ludic.render3d/shaders/spv/266d2401.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..ba23f6d59c4facdc74c4a7186171dc660f03716c GIT binary patch literal 16864 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 literal 0 HcmV?d00001 diff --git a/packages/ludic.render3d/shaders/spv/5e70d731.frag.spv b/packages/ludic.render3d/shaders/spv/5e70d731.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..529200bbf59cf68489a8af4d5f8a65055fe77b87 GIT binary patch literal 46456 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 literal 0 HcmV?d00001 diff --git a/packages/ludic.render3d/shaders/spv/5e70d731.vert.spv b/packages/ludic.render3d/shaders/spv/5e70d731.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..215b17d64e976a8aff39075e09daeabdfda81fdf GIT binary patch literal 16864 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 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 +} diff --git a/runtime/native/gl.ll b/runtime/native/gl.ll index 5b7b6f74..235a254a 100644 --- a/runtime/native/gl.ll +++ b/runtime/native/gl.ll @@ -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 diff --git a/runtime/native/gl_win_nowin.ll b/runtime/native/gl_win_nowin.ll index 539ed6f6..52025b88 100644 --- a/runtime/native/gl_win_nowin.ll +++ b/runtime/native/gl_win_nowin.ll @@ -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: diff --git a/runtime/native/vk_mac.ll b/runtime/native/vk_mac.ll index 51c976db..e34cd65b 100644 --- a/runtime/native/vk_mac.ll +++ b/runtime/native/vk_mac.ll @@ -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 diff --git a/runtime/native/win32.ll b/runtime/native/win32.ll index 81cd4bd7..c7fc24d3 100644 --- a/runtime/native/win32.ll +++ b/runtime/native/win32.ll @@ -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) { diff --git a/selfhost/backend/emit_intrin.ludic b/selfhost/backend/emit_intrin.ludic index a7907d6a..ef33a771 100644 --- a/selfhost/backend/emit_intrin.ludic +++ b/selfhost/backend/emit_intrin.ludic @@ -20,6 +20,12 @@ function is_intrinsic(name: pointer) -> bool { function emit_bind(rest: pointer) -> pointer { let r = nreg(); emit(" "); emit(r); emit(" = "); emit(rest); emit("\n"); return r } function arg_code(e: Node, i: int) -> pointer { let v = emit_expr(e.kids[i]); return v.code } +# a buffer handed to the platform layer: a slice's elements, never its header (as an extern's are) +function arg_buf(e: Node, i: int) -> pointer { + let v = emit_expr(e.kids[i]) + if is_slice_ty(v.ty) { return emit_bind(`load ptr, ptr {slice_field(v.code, 0)}`) } + return v.code +} function emit_intrinsic(name: pointer, e: Node) -> Val { g_intrin_ok = true diff --git a/selfhost/backend/emit_intrin2.ludic b/selfhost/backend/emit_intrin2.ludic index 594f9364..61514fa0 100644 --- a/selfhost/backend/emit_intrin2.ludic +++ b/selfhost/backend/emit_intrin2.ludic @@ -70,7 +70,7 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val { return val("0", "void") } if (name == "win_present") { - let a = arg_code(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2) + let a = arg_buf(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2) emit(" call void @win_present(ptr "); emit(a); emit(", i32 "); emit(b); emit(", i32 "); emit(c); emit(")\n") return val("0", "void") } @@ -78,19 +78,19 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val { # #50 — fill a caller buffer with the platform device state (windowed only, # DCE'd headless). win_held: an 8-word held-key bitset; win_mouse: [x, y, # button-mask, wheel-delta]. - if (name == "win_held") { let a = arg_code(e, 0); emit(" call void @win_held(ptr "); emit(a); emit(")\n"); return val("0", "void") } - if (name == "win_mouse") { let a = arg_code(e, 0); emit(" call void @win_mouse(ptr "); emit(a); emit(")\n"); return val("0", "void") } + if (name == "win_held") { let a = arg_buf(e, 0); emit(" call void @win_held(ptr "); emit(a); emit(")\n"); return val("0", "void") } + if (name == "win_mouse") { let a = arg_buf(e, 0); emit(" call void @win_mouse(ptr "); emit(a); emit(")\n"); return val("0", "void") } # what the active keyboard layout types on a held-set key position (windowed only, # DCE'd headless): a code point, 0 for none. if (name == "win_key_char") { let a = arg_code(e, 0); return val(emit_bind(`call i32 @win_key_char(i32 {a})`), "int") } # what the player TYPED since the last call - UTF-16 code units into the caller's buffer, one per # word, returning how many. The layout, the modifiers and any dead key are already applied, which # is why a name field must read this rather than build text out of key codes. - if (name == "win_text") { let a = arg_code(e, 0); let b = arg_code(e, 1); return val(emit_bind(`call i32 @win_text(ptr {a}, i32 {b})`), "int") } + if (name == "win_text") { let a = arg_buf(e, 0); let b = arg_code(e, 1); return val(emit_bind(`call i32 @win_text(ptr {a}, i32 {b})`), "int") } # #51 — win_pad: 6 words/pad [connected, mask, lx, ly, rx, ry]; win_touch: 3 # words/point [active, x, y]. Both windowed-only, DCE'd headless. - if (name == "win_pad") { let a = arg_code(e, 0); emit(" call void @win_pad(ptr "); emit(a); emit(")\n"); return val("0", "void") } - if (name == "win_touch") { let a = arg_code(e, 0); emit(" call void @win_touch(ptr "); emit(a); emit(")\n"); return val("0", "void") } + if (name == "win_pad") { let a = arg_buf(e, 0); emit(" call void @win_pad(ptr "); emit(a); emit(")\n"); return val("0", "void") } + if (name == "win_touch") { let a = arg_buf(e, 0); emit(" call void @win_touch(ptr "); emit(a); emit(")\n"); return val("0", "void") } # #89 — set the OS cursor capture mode (windowed only; DCE'd headless). if (name == "win_cursor_mode") { let a = arg_code(e, 0); emit(" call void @win_cursor_mode(i32 "); emit(a); emit(")\n"); return val("0", "void") } # #22 audio backend — AVAudioPlayer via audio.ll; windowed-only, DCE'd headless. diff --git a/selfhost/main.ludic b/selfhost/main.ludic index d4222bad..566b4982 100644 --- a/selfhost/main.ludic +++ b/selfhost/main.ludic @@ -358,6 +358,8 @@ entry { var vkl = join_path(home, "runtime/native/vk_mac.ll") if g_target_win { vkl = join_path(home, "runtime/native/vk_win.ll") } cmd = `{cmd} {vkt} {vkl}` + # the CAMetalLayer a macOS window presents through, found by name in cocoa.ll + if not g_target_win { cmd = `{cmd} -Wl,-needed_framework,QuartzCore` } } # A shipped Windows game is a GUI-subsystem program: started from Explorer, a # console-subsystem one opens a console window behind the game. mainCRTStartup keeps diff --git a/tools/ludic-cli/build.ludic b/tools/ludic-cli/build.ludic index 6a7f7de6..39c62872 100644 --- a/tools/ludic-cli/build.ludic +++ b/tools/ludic-cli/build.ludic @@ -102,7 +102,8 @@ function gl_link_flags(ll: pointer) -> pointer { function vk_link_flags(ll: pointer) -> pointer { if not shq(`grep -q "@lvk_" {ll}`) { return "" } let home = ludic_home() - return ` {home}runtime/native/vk_thunks.ll {home}runtime/native/vk_mac.ll` + # QuartzCore: the CAMetalLayer a window presents through (cocoa.ll finds the class by name) + return ` {home}runtime/native/vk_thunks.ll {home}runtime/native/vk_mac.ll -Wl,-needed_framework,QuartzCore` } # A program that uses Http.* calls the @hs_* transport; link http.ll and Foundation only