feat(vk): Vk.* - Vulkan 1.0-1.4 generated from the registry, loaded at run time

ludic-dev vkgen reads vk.xml into runtime/native/vk_api.ludic (constants, every struct's
<Struct>_sizeof and <Struct>_<field> offsets, one extern per command) and vk_thunks.ll.
Every size and offset was compiled against the SDK's C headers; `ludic-dev test` checks the
tracked files against the registry wherever the Vulkan SDK is installed.

vk_win.ll (vulkan-1.dll) and vk_mac.ll (libvulkan.1.dylib, MoltenVK) open the loader at run
time, so a program built with Vk.* starts on a machine without Vulkan. ludicc and ludic
build link both for any program that uses Vk.*. The seeds are regenerated for the new
compiler.

vk_probe reports what a machine's Vulkan can do; vk_compute dispatches a Slang compute
shader and reads the picture back, clean under the validation layer on an RTX 3070 Ti and
on an M4 Pro through MoltenVK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 09:52:12 +03:00
parent 043d8d81a2
commit 208cad7ca1
18 changed files with 52468 additions and 40972 deletions

View file

@ -0,0 +1,36 @@
---
id: vk
title: Vk
order: 61
---
Vulkan for Ludic. <code>Vk.*</code> binds <strong>Vulkan 1.0–1.4</strong> and the extensions a modern renderer is built around — swapchain and HDR colour spaces, ray query and acceleration structures, opacity micromaps, mesh shaders, variable rate shading, memory budget, pipeline libraries, NVIDIA low latency, and portability enumeration for MoltenVK. Every command is a method named by its snake case (<code>vkCreateInstance</code> → <code>Vk.create_instance</code>, <code>vkCmdDrawIndexedIndirectCount</code> → <code>Vk.cmd_draw_indexed_indirect_count</code>), every <code>VK_*</code> constant is available as written, and every struct has its size and field offsets as constants: <code>VkDeviceCreateInfo_sizeof</code>, <code>VkDeviceCreateInfo_queueCreateInfoCount</code>. The binding is generated by <code>ludic-dev vkgen</code> from the Vulkan registry (<code>vk.xml</code>), and every size and offset is checked against the SDK's C headers.
A struct is plain memory filled by field name: <code>bytes(Vk…_sizeof)</code>, <code>Vk.zero(p, n)</code>, then <code>Vk.put_i32</code> / <code>Vk.put_i64</code> / <code>Vk.put_ptr(p, offset, value)</code>, read back with <code>Vk.get_i32</code> / <code>get_i64</code> / <code>get_ptr</code>, and <code>Vk.at(p, offset)</code> for a nested struct or an inline array. Numbers follow the C types: a <code>float</code> is its IEEE bit pattern in an <code>int</code>, and <code>uint64_t</code>, <code>VkDeviceSize</code> and non-dispatchable handles are <code>long</code>. A negative <code>long</code> has to come from a <code>long</code> variable — an <code>int</code> literal passed straight to a <code>long</code> parameter is zero-extended.
The loader is opened <strong>at run time</strong> by <code>Vk.open()</code>, never linked: <code>vulkan-1.dll</code> on Windows, <code>libvulkan.1.dylib</code> (the LunarG loader and MoltenVK) on macOS — beside the executable, in the library paths, or under <code>$VULKAN_SDK</code>. It returns 0 on a machine without Vulkan, and the program carries on; <code>Vk.has(name)</code> says whether one command is there. Using <code>Vk.*</code> links the generated thunks and the loader; a program that does not is unchanged. See <code>examples/rendering/vk_probe.ludic</code> (what a machine's Vulkan can do) and <code>vk_compute.ludic</code> (a Slang compute shader, dispatched and read back).
```ludic
program Probe {
property Marker { on: int = 1 }
model Anchor { Marker }
handler Boot phase Start {
spawn Anchor {}
if Vk.open() == 0 { print("no Vulkan here"); quit() }
let app = bytes(VkApplicationInfo_sizeof)
Vk.zero(app, VkApplicationInfo_sizeof)
Vk.put_i32(app, VkApplicationInfo_sType, VK_STRUCTURE_TYPE_APPLICATION_INFO)
Vk.put_i32(app, VkApplicationInfo_apiVersion, (1 << 22) | (3 << 12))
let ci = bytes(VkInstanceCreateInfo_sizeof)
Vk.zero(ci, VkInstanceCreateInfo_sizeof)
Vk.put_i32(ci, VkInstanceCreateInfo_sType, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO)
Vk.put_ptr(ci, VkInstanceCreateInfo_pApplicationInfo, app)
let out = bytes(8)
if Vk.create_instance(ci, null, out) == VK_SUCCESS {
print("a Vulkan instance")
Vk.destroy_instance(Vk.get_ptr(out, 0), null)
}
quit()
}
}
```

View file

@ -0,0 +1,349 @@
# vk_compute.ludic — the first work a Ludic program hands a GPU through Vulkan: a
# Slang compute shader (vk_compute.slang) fills a storage buffer with a picture, the
# CPU maps the buffer and writes it as a PPM. No window, so it runs headless over SSH
# and under the validation layer.
#
# Every struct is filled by field name from the generated layouts (vk_api.ludic), and
# every call goes through the generated thunks - this is the whole path the Vulkan
# renderer will use, end to end: loader, instance, device, memory, shader module,
# descriptor set, push constants, pipeline, command buffer, submit, fence, map.
# slangc examples/rendering/vk_compute.slang -target spirv -profile spirv_1_5
# -entry main -stage compute -o build/vk_compute.spv
# Windows: bin/ludicc examples/rendering/vk_compute.ludic --headless
# (Vk.* links the Vulkan runtime)
# macOS: the same with vk_mac.ll; VULKAN_SDK set to the SDK's macOS directory
program VkCompute {
property Marker { on: int = 1 }
model Anchor { Marker }
const W: int = 640
const H: int = 360
var dev: pointer = null
function fail(what: string, r: int) -> void { print(`vulkan: {what} failed, VkResult {r}`); quit() }
function handle(out: bytes) -> long { return vk_get_i64(out, 0) }
function has_ext(props: bytes, n: int, want: string) -> bool {
for i in 0 .. n {
if string(vk_at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName)) == want { return true }
}
return false
}
function read_all(path: string) -> bytes {
let f = file_open(path, "rb")
if f == null { return null }
file_seek(f, 0, 2)
let n = file_tell(f)
file_seek(f, 0, 0)
let b = bytes(n + 4)
file_read(f, b, n)
file_close(f)
spv_len = n
return b
}
var spv_len: int = 0
handler Boot phase Start {
spawn Anchor {}
var spv_path = "build/vk_compute.spv"
if Os.has_env("VKC_SPV") { spv_path = Os.env("VKC_SPV") }
let spv = read_all(spv_path)
if spv == null { print(`vulkan: no SPIR-V at {spv_path} (compile vk_compute.slang with slangc)`); quit() }
if Vk.open() == 0 { print("vulkan: no loader"); quit() }
# ---- instance (portability enumeration where offered: MoltenVK)
let cnt = bytes(4)
vk_put_i32(cnt, 0, 0)
vk_enumerate_instance_extension_properties(null, cnt, null)
let nie = vk_get_i32(cnt, 0)
let iexts = bytes(nie * VkExtensionProperties_sizeof + 8)
vk_enumerate_instance_extension_properties(null, cnt, iexts)
let portability = has_ext(iexts, nie, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)
let app = bytes(VkApplicationInfo_sizeof)
vk_zero(app, VkApplicationInfo_sizeof)
vk_put_i32(app, VkApplicationInfo_sType, VK_STRUCTURE_TYPE_APPLICATION_INFO)
vk_put_ptr(app, VkApplicationInfo_pApplicationName, "vk_compute")
vk_put_i32(app, VkApplicationInfo_apiVersion, (1 << 22) | (3 << 12))
let iext_names = bytes(8)
let ici = bytes(VkInstanceCreateInfo_sizeof)
vk_zero(ici, VkInstanceCreateInfo_sizeof)
vk_put_i32(ici, VkInstanceCreateInfo_sType, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO)
vk_put_ptr(ici, VkInstanceCreateInfo_pApplicationInfo, app)
if portability {
vk_put_ptr(iext_names, 0, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)
vk_put_i32(ici, VkInstanceCreateInfo_flags, VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR)
vk_put_i32(ici, VkInstanceCreateInfo_enabledExtensionCount, 1)
vk_put_ptr(ici, VkInstanceCreateInfo_ppEnabledExtensionNames, iext_names)
}
let out = bytes(8)
var r = vk_create_instance(ici, null, out)
if r != VK_SUCCESS { fail("vkCreateInstance", r) }
let inst = vk_get_ptr(out, 0)
# ---- the first device, its first queue family that computes
vk_put_i32(cnt, 0, 1)
let devs = bytes(8)
vk_enumerate_physical_devices(inst, cnt, devs)
let pd = vk_get_ptr(devs, 0)
let props = bytes(VkPhysicalDeviceProperties_sizeof)
vk_get_physical_device_properties(pd, props)
print(`vulkan: {string(vk_at(props, VkPhysicalDeviceProperties_deviceName))}`)
vk_put_i32(cnt, 0, 0)
vk_get_physical_device_queue_family_properties(pd, cnt, null)
let nq = vk_get_i32(cnt, 0)
let qprops = bytes(nq * VkQueueFamilyProperties_sizeof + 8)
vk_get_physical_device_queue_family_properties(pd, cnt, qprops)
var family = -1
for q in 0 .. nq {
let flags = vk_get_i32(qprops, q * VkQueueFamilyProperties_sizeof + VkQueueFamilyProperties_queueFlags)
if family < 0 and (flags & VK_QUEUE_COMPUTE_BIT) != 0 { family = q }
}
vk_put_i32(cnt, 0, 0)
vk_enumerate_device_extension_properties(pd, null, cnt, null)
let nde = vk_get_i32(cnt, 0)
let dexts = bytes(nde * VkExtensionProperties_sizeof + 8)
vk_enumerate_device_extension_properties(pd, null, cnt, dexts)
let prio = bytes(4)
vk_put_i32(prio, 0, 0x3F800000)
let qci = bytes(VkDeviceQueueCreateInfo_sizeof)
vk_zero(qci, VkDeviceQueueCreateInfo_sizeof)
vk_put_i32(qci, VkDeviceQueueCreateInfo_sType, VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO)
vk_put_i32(qci, VkDeviceQueueCreateInfo_queueFamilyIndex, family)
vk_put_i32(qci, VkDeviceQueueCreateInfo_queueCount, 1)
vk_put_ptr(qci, VkDeviceQueueCreateInfo_pQueuePriorities, prio)
let dext_names = bytes(8)
let dci = bytes(VkDeviceCreateInfo_sizeof)
vk_zero(dci, VkDeviceCreateInfo_sizeof)
vk_put_i32(dci, VkDeviceCreateInfo_sType, VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO)
vk_put_i32(dci, VkDeviceCreateInfo_queueCreateInfoCount, 1)
vk_put_ptr(dci, VkDeviceCreateInfo_pQueueCreateInfos, qci)
# a portability driver must be told the program knows it is one
if has_ext(dexts, nde, "VK_KHR_portability_subset") {
vk_put_ptr(dext_names, 0, "VK_KHR_portability_subset")
vk_put_i32(dci, VkDeviceCreateInfo_enabledExtensionCount, 1)
vk_put_ptr(dci, VkDeviceCreateInfo_ppEnabledExtensionNames, dext_names)
}
r = vk_create_device(pd, dci, null, out)
if r != VK_SUCCESS { fail("vkCreateDevice", r) }
dev = vk_get_ptr(out, 0)
vk_get_device_queue(dev, family, 0, out)
let queue = vk_get_ptr(out, 0)
# ---- a host-visible storage buffer for the picture
let nbytes: long = W * H * 4
let bci = bytes(VkBufferCreateInfo_sizeof)
vk_zero(bci, VkBufferCreateInfo_sizeof)
vk_put_i32(bci, VkBufferCreateInfo_sType, VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO)
vk_put_i64(bci, VkBufferCreateInfo_size, nbytes)
vk_put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT)
vk_put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
r = vk_create_buffer(dev, bci, null, out)
if r != VK_SUCCESS { fail("vkCreateBuffer", r) }
let buf = handle(out)
let req = bytes(VkMemoryRequirements_sizeof)
vk_get_buffer_memory_requirements(dev, buf, req)
let mp = bytes(VkPhysicalDeviceMemoryProperties_sizeof)
vk_get_physical_device_memory_properties(pd, mp)
let want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
let allowed = vk_get_i32(req, VkMemoryRequirements_memoryTypeBits)
var mtype = -1
for t in 0 .. vk_get_i32(mp, VkPhysicalDeviceMemoryProperties_memoryTypeCount) {
let pf = vk_get_i32(mp, VkPhysicalDeviceMemoryProperties_memoryTypes + t * VkMemoryType_sizeof + VkMemoryType_propertyFlags)
if mtype < 0 and ((allowed >> t) & 1) == 1 and (pf & want) == want { mtype = t }
}
if mtype < 0 { print("vulkan: no host-visible memory type"); quit() }
let mai = bytes(VkMemoryAllocateInfo_sizeof)
vk_zero(mai, VkMemoryAllocateInfo_sizeof)
vk_put_i32(mai, VkMemoryAllocateInfo_sType, VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO)
vk_put_i64(mai, VkMemoryAllocateInfo_allocationSize, vk_get_i64(req, VkMemoryRequirements_size))
vk_put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, mtype)
r = vk_allocate_memory(dev, mai, null, out)
if r != VK_SUCCESS { fail("vkAllocateMemory", r) }
let mem = handle(out)
let zero: long = 0
r = vk_bind_buffer_memory(dev, buf, mem, zero)
if r != VK_SUCCESS { fail("vkBindBufferMemory", r) }
# ---- the shader, its layout, the pipeline
let smci = bytes(VkShaderModuleCreateInfo_sizeof)
vk_zero(smci, VkShaderModuleCreateInfo_sizeof)
vk_put_i32(smci, VkShaderModuleCreateInfo_sType, VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO)
let code_size: long = spv_len
vk_put_i64(smci, VkShaderModuleCreateInfo_codeSize, code_size)
vk_put_ptr(smci, VkShaderModuleCreateInfo_pCode, spv)
r = vk_create_shader_module(dev, smci, null, out)
if r != VK_SUCCESS { fail("vkCreateShaderModule", r) }
let module = handle(out)
let binding = bytes(VkDescriptorSetLayoutBinding_sizeof)
vk_zero(binding, VkDescriptorSetLayoutBinding_sizeof)
vk_put_i32(binding, VkDescriptorSetLayoutBinding_binding, 0)
vk_put_i32(binding, VkDescriptorSetLayoutBinding_descriptorType, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
vk_put_i32(binding, VkDescriptorSetLayoutBinding_descriptorCount, 1)
vk_put_i32(binding, VkDescriptorSetLayoutBinding_stageFlags, VK_SHADER_STAGE_COMPUTE_BIT)
let dslci = bytes(VkDescriptorSetLayoutCreateInfo_sizeof)
vk_zero(dslci, VkDescriptorSetLayoutCreateInfo_sizeof)
vk_put_i32(dslci, VkDescriptorSetLayoutCreateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)
vk_put_i32(dslci, VkDescriptorSetLayoutCreateInfo_bindingCount, 1)
vk_put_ptr(dslci, VkDescriptorSetLayoutCreateInfo_pBindings, binding)
let set_layouts = bytes(8)
r = vk_create_descriptor_set_layout(dev, dslci, null, set_layouts)
if r != VK_SUCCESS { fail("vkCreateDescriptorSetLayout", r) }
let pcr = bytes(VkPushConstantRange_sizeof)
vk_put_i32(pcr, VkPushConstantRange_stageFlags, VK_SHADER_STAGE_COMPUTE_BIT)
vk_put_i32(pcr, VkPushConstantRange_offset, 0)
vk_put_i32(pcr, VkPushConstantRange_size, 12)
let plci = bytes(VkPipelineLayoutCreateInfo_sizeof)
vk_zero(plci, VkPipelineLayoutCreateInfo_sizeof)
vk_put_i32(plci, VkPipelineLayoutCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO)
vk_put_i32(plci, VkPipelineLayoutCreateInfo_setLayoutCount, 1)
vk_put_ptr(plci, VkPipelineLayoutCreateInfo_pSetLayouts, set_layouts)
vk_put_i32(plci, VkPipelineLayoutCreateInfo_pushConstantRangeCount, 1)
vk_put_ptr(plci, VkPipelineLayoutCreateInfo_pPushConstantRanges, pcr)
r = vk_create_pipeline_layout(dev, plci, null, out)
if r != VK_SUCCESS { fail("vkCreatePipelineLayout", r) }
let layout = handle(out)
let cpci = bytes(VkComputePipelineCreateInfo_sizeof)
vk_zero(cpci, VkComputePipelineCreateInfo_sizeof)
vk_put_i32(cpci, VkComputePipelineCreateInfo_sType, VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO)
let st = VkComputePipelineCreateInfo_stage
vk_put_i32(cpci, st + VkPipelineShaderStageCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
vk_put_i32(cpci, st + VkPipelineShaderStageCreateInfo_stage, VK_SHADER_STAGE_COMPUTE_BIT)
vk_put_i64(cpci, st + VkPipelineShaderStageCreateInfo_module, module)
vk_put_ptr(cpci, st + VkPipelineShaderStageCreateInfo_pName, "main")
vk_put_i64(cpci, VkComputePipelineCreateInfo_layout, layout)
r = vk_create_compute_pipelines(dev, zero, 1, cpci, null, out)
if r != VK_SUCCESS { fail("vkCreateComputePipelines", r) }
let pipeline = handle(out)
# ---- the descriptor set that points the shader at the buffer
let psz = bytes(VkDescriptorPoolSize_sizeof)
vk_put_i32(psz, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
vk_put_i32(psz, VkDescriptorPoolSize_descriptorCount, 1)
let dpci = bytes(VkDescriptorPoolCreateInfo_sizeof)
vk_zero(dpci, VkDescriptorPoolCreateInfo_sizeof)
vk_put_i32(dpci, VkDescriptorPoolCreateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO)
vk_put_i32(dpci, VkDescriptorPoolCreateInfo_maxSets, 1)
vk_put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 1)
vk_put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, psz)
r = vk_create_descriptor_pool(dev, dpci, null, out)
if r != VK_SUCCESS { fail("vkCreateDescriptorPool", r) }
let pool = handle(out)
let dsai = bytes(VkDescriptorSetAllocateInfo_sizeof)
vk_zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
vk_put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO)
vk_put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, pool)
vk_put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1)
vk_put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, set_layouts)
let sets = bytes(8)
r = vk_allocate_descriptor_sets(dev, dsai, sets)
if r != VK_SUCCESS { fail("vkAllocateDescriptorSets", r) }
let dbi = bytes(VkDescriptorBufferInfo_sizeof)
vk_put_i64(dbi, VkDescriptorBufferInfo_buffer, buf)
vk_put_i64(dbi, VkDescriptorBufferInfo_offset, zero)
vk_put_i64(dbi, VkDescriptorBufferInfo_range, nbytes)
let wds = bytes(VkWriteDescriptorSet_sizeof)
vk_zero(wds, VkWriteDescriptorSet_sizeof)
vk_put_i32(wds, VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
vk_put_i64(wds, VkWriteDescriptorSet_dstSet, vk_get_i64(sets, 0))
vk_put_i32(wds, VkWriteDescriptorSet_dstBinding, 0)
vk_put_i32(wds, VkWriteDescriptorSet_descriptorCount, 1)
vk_put_i32(wds, VkWriteDescriptorSet_descriptorType, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
vk_put_ptr(wds, VkWriteDescriptorSet_pBufferInfo, dbi)
vk_update_descriptor_sets(dev, 1, wds, 0, null)
# ---- record, submit, wait
let cpi = bytes(VkCommandPoolCreateInfo_sizeof)
vk_zero(cpi, VkCommandPoolCreateInfo_sizeof)
vk_put_i32(cpi, VkCommandPoolCreateInfo_sType, VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO)
vk_put_i32(cpi, VkCommandPoolCreateInfo_queueFamilyIndex, family)
r = vk_create_command_pool(dev, cpi, null, out)
if r != VK_SUCCESS { fail("vkCreateCommandPool", r) }
let cmd_pool = handle(out)
let cbai = bytes(VkCommandBufferAllocateInfo_sizeof)
vk_zero(cbai, VkCommandBufferAllocateInfo_sizeof)
vk_put_i32(cbai, VkCommandBufferAllocateInfo_sType, VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO)
vk_put_i64(cbai, VkCommandBufferAllocateInfo_commandPool, cmd_pool)
vk_put_i32(cbai, VkCommandBufferAllocateInfo_level, VK_COMMAND_BUFFER_LEVEL_PRIMARY)
vk_put_i32(cbai, VkCommandBufferAllocateInfo_commandBufferCount, 1)
let cbs = bytes(8)
r = vk_allocate_command_buffers(dev, cbai, cbs)
if r != VK_SUCCESS { fail("vkAllocateCommandBuffers", r) }
let cb = vk_get_ptr(cbs, 0)
let cbbi = bytes(VkCommandBufferBeginInfo_sizeof)
vk_zero(cbbi, VkCommandBufferBeginInfo_sizeof)
vk_put_i32(cbbi, VkCommandBufferBeginInfo_sType, VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO)
vk_put_i32(cbbi, VkCommandBufferBeginInfo_flags, VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT)
r = vk_begin_command_buffer(cb, cbbi)
if r != VK_SUCCESS { fail("vkBeginCommandBuffer", r) }
vk_cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline)
vk_cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, layout, 0, 1, sets, 0, null)
let pc = bytes(12)
vk_put_i32(pc, 0, 0x3FC00000) # time = 1.5, as float bits
vk_put_i32(pc, 4, W)
vk_put_i32(pc, 8, H)
vk_cmd_push_constants(cb, layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, 12, pc)
vk_cmd_dispatch(cb, (W + 15) / 16, (H + 15) / 16, 1)
r = vk_end_command_buffer(cb)
if r != VK_SUCCESS { fail("vkEndCommandBuffer", r) }
let fci = bytes(VkFenceCreateInfo_sizeof)
vk_zero(fci, VkFenceCreateInfo_sizeof)
vk_put_i32(fci, VkFenceCreateInfo_sType, VK_STRUCTURE_TYPE_FENCE_CREATE_INFO)
let fences = bytes(8)
r = vk_create_fence(dev, fci, null, fences)
if r != VK_SUCCESS { fail("vkCreateFence", r) }
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(queue, 1, si, vk_get_i64(fences, 0))
if r != VK_SUCCESS { fail("vkQueueSubmit", r) }
let forever: long = -1
r = vk_wait_for_fences(dev, 1, fences, 1, forever)
if r != VK_SUCCESS { fail("vkWaitForFences", r) }
print(`vulkan: dispatched {(W + 15) / 16}x{(H + 15) / 16} groups and waited for the fence`)
# ---- read the picture back
let pp = bytes(8)
r = vk_map_memory(dev, mem, zero, nbytes, 0, pp)
if r != VK_SUCCESS { fail("vkMapMemory", r) }
let px = vk_get_ptr(pp, 0)
var out_path = "build/vk_compute.ppm"
if Os.has_env("VKC_OUT") { out_path = Os.env("VKC_OUT") }
let f = file_open(out_path, "wb")
let hdr = `P6\n{W} {H}\n255\n`
file_write(f, hdr, len(hdr))
let row = bytes(W * 3)
var lit = 0
for y in 0 .. H {
for x in 0 .. W {
let v = vk_get_i32(px, (y * W + x) * 4)
row[x * 3] = v & 255; row[x * 3 + 1] = (v >> 8) & 255; row[x * 3 + 2] = (v >> 16) & 255
if ((v >> 24) & 255) == 255 { lit += 1 }
}
file_write(f, row, W * 3)
}
file_close(f)
vk_unmap_memory(dev, mem)
print(`vulkan: {lit} of {W * H} pixels written by the shader -> {out_path}`)
vk_destroy_fence(dev, vk_get_i64(fences, 0), null)
vk_destroy_command_pool(dev, cmd_pool, null)
vk_destroy_descriptor_pool(dev, pool, null)
vk_destroy_pipeline(dev, pipeline, null)
vk_destroy_pipeline_layout(dev, layout, null)
vk_destroy_descriptor_set_layout(dev, vk_get_i64(set_layouts, 0), null)
vk_destroy_shader_module(dev, module, null)
vk_destroy_buffer(dev, buf, null)
vk_free_memory(dev, mem, null)
vk_destroy_device(dev, null)
vk_destroy_instance(inst, null)
if lit == W * H { print("VKCOMPUTE OK") } else { print("VKCOMPUTE INCOMPLETE") }
quit()
}
}

View file

@ -0,0 +1,35 @@
// vk_compute.slang — the first Slang shader Ludic runs: a compute pass that writes a
// picture into a storage buffer the CPU reads back. RGBA8 packed into one uint per
// pixel, so the result needs no image, no staging copy and no format conversion.
// slangc vk_compute.slang -target spirv -profile spirv_1_5 -entry main -stage compute -o vk_compute.spv
[[vk::binding(0, 0)]] RWStructuredBuffer<uint> pixels;
struct Params
{
float time;
uint width;
uint height;
};
[[vk::push_constant]] ConstantBuffer<Params> params;
uint pack(float3 c)
{
uint3 b = uint3(saturate(c) * 255.0 + 0.5);
return b.r | (b.g << 8) | (b.b << 16) | (255u << 24);
}
[shader("compute")]
[numthreads(16, 16, 1)]
void main(uint3 id : SV_DispatchThreadID)
{
if (id.x >= params.width || id.y >= params.height)
return;
float2 uv = (float2(id.xy) + 0.5) / float2(params.width, params.height);
// a sky over a ridge line, so a wrong row order or a swapped channel is obvious
float ridge = 0.55 + 0.08 * sin(uv.x * 9.0 + params.time) + 0.04 * sin(uv.x * 23.0);
float3 sky = lerp(float3(0.95, 0.72, 0.45), float3(0.20, 0.35, 0.65), uv.y / ridge);
float3 ground = lerp(float3(0.18, 0.28, 0.14), float3(0.08, 0.12, 0.07), (uv.y - ridge) / (1.0 - ridge));
float3 c = uv.y < ridge ? sky : ground;
pixels[id.y * params.width + id.x] = pack(c);
}

View file

@ -0,0 +1,159 @@
# vk_probe.ludic — what a machine's Vulkan can do, through the generated bindings.
#
# Opens the loader at run time, makes an instance (with portability enumeration
# where the loader offers it, which is how MoltenVK is found on a Mac), and for
# every GPU says whether it meets the Vulkan renderer's floor - Vulkan 1.3 with
# dynamic rendering, synchronization2, descriptor indexing, buffer device
# addresses, timeline semaphores and indirect count draws - and which optional
# features it would take. Then it opens a device with a graphics queue.
# Windows: bin/ludicc examples/rendering/vk_probe.ludic --headless
# (Vk.* links the Vulkan runtime)
# macOS: the same with vk_mac.ll, and VULKAN_SDK set to the SDK's macOS directory
program VkProbe {
property Marker { on: int = 1 }
model Anchor { Marker }
const API_1_0: int = 4194304 # VK_MAKE_API_VERSION(0, 1, 0, 0)
function api_text(v: int) -> string { return `{(v >> 22) & 127}.{(v >> 12) & 1023}.{v & 4095}` }
function yes(b: bool) -> string { if b { return "yes" }; return "no " }
# a name in an array of VkExtensionProperties
function has_ext(props: bytes, n: int, want: string) -> bool {
for i in 0 .. n {
let at = vk_at(props, i * VkExtensionProperties_sizeof + VkExtensionProperties_extensionName)
if string(at) == want { return true }
}
return false
}
handler Boot phase Start {
spawn Anchor {}
if Vk.open() == 0 { print("vulkan: no loader on this machine - it would run OpenGL"); quit() }
let ver = bytes(4)
vk_put_i32(ver, 0, API_1_0)
if vk_has("vkEnumerateInstanceVersion") == 1 { vk_enumerate_instance_version(ver) }
print(`vulkan: loader {api_text(vk_get_i32(ver, 0))}`)
let cnt = bytes(4)
vk_put_i32(cnt, 0, 0)
vk_enumerate_instance_extension_properties(null, cnt, null)
let nie = vk_get_i32(cnt, 0)
let iexts = bytes(nie * VkExtensionProperties_sizeof + 8)
vk_enumerate_instance_extension_properties(null, cnt, iexts)
let portability = has_ext(iexts, nie, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)
let colorspace = has_ext(iexts, nie, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)
let app = bytes(VkApplicationInfo_sizeof)
vk_zero(app, VkApplicationInfo_sizeof)
vk_put_i32(app, VkApplicationInfo_sType, VK_STRUCTURE_TYPE_APPLICATION_INFO)
vk_put_ptr(app, VkApplicationInfo_pApplicationName, "Ludic")
vk_put_ptr(app, VkApplicationInfo_pEngineName, "Ludic")
vk_put_i32(app, VkApplicationInfo_apiVersion, (1 << 22) | (3 << 12))
let names = bytes(16)
var nnames = 0
if portability { vk_put_ptr(names, 0, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); nnames = 1 }
let ici = bytes(VkInstanceCreateInfo_sizeof)
vk_zero(ici, VkInstanceCreateInfo_sizeof)
vk_put_i32(ici, VkInstanceCreateInfo_sType, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO)
vk_put_ptr(ici, VkInstanceCreateInfo_pApplicationInfo, app)
if portability { vk_put_i32(ici, VkInstanceCreateInfo_flags, VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR) }
vk_put_i32(ici, VkInstanceCreateInfo_enabledExtensionCount, nnames)
vk_put_ptr(ici, VkInstanceCreateInfo_ppEnabledExtensionNames, names)
let out = bytes(8)
let r = vk_create_instance(ici, null, out)
if r != VK_SUCCESS { print(`vulkan: vkCreateInstance failed, VkResult {r}`); quit() }
let inst = vk_get_ptr(out, 0)
print(`vulkan: instance (portability {yes(portability)}, HDR colour spaces {yes(colorspace)})`)
vk_put_i32(cnt, 0, 0)
vk_enumerate_physical_devices(inst, cnt, null)
let ndev = vk_get_i32(cnt, 0)
let devs = bytes(ndev * 8 + 8)
vk_enumerate_physical_devices(inst, cnt, devs)
let props = bytes(VkPhysicalDeviceProperties_sizeof)
for d in 0 .. ndev {
let pd = vk_get_ptr(devs, d * 8)
vk_get_physical_device_properties(pd, props)
let api = vk_get_i32(props, VkPhysicalDeviceProperties_apiVersion)
print(`vulkan: device {d}: {string(vk_at(props, VkPhysicalDeviceProperties_deviceName))} (type {vk_get_i32(props, VkPhysicalDeviceProperties_deviceType)}, api {api_text(api)})`)
let f13 = bytes(VkPhysicalDeviceVulkan13Features_sizeof)
vk_zero(f13, VkPhysicalDeviceVulkan13Features_sizeof)
vk_put_i32(f13, VkPhysicalDeviceVulkan13Features_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES)
let f12 = bytes(VkPhysicalDeviceVulkan12Features_sizeof)
vk_zero(f12, VkPhysicalDeviceVulkan12Features_sizeof)
vk_put_i32(f12, VkPhysicalDeviceVulkan12Features_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES)
vk_put_ptr(f12, VkPhysicalDeviceVulkan12Features_pNext, f13)
let f2 = bytes(VkPhysicalDeviceFeatures2_sizeof)
vk_zero(f2, VkPhysicalDeviceFeatures2_sizeof)
vk_put_i32(f2, VkPhysicalDeviceFeatures2_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2)
vk_put_ptr(f2, VkPhysicalDeviceFeatures2_pNext, f12)
vk_get_physical_device_features2(pd, f2)
let dyn = vk_get_i32(f13, VkPhysicalDeviceVulkan13Features_dynamicRendering) == 1
let sync2 = vk_get_i32(f13, VkPhysicalDeviceVulkan13Features_synchronization2) == 1
let bindless = vk_get_i32(f12, VkPhysicalDeviceVulkan12Features_descriptorIndexing) == 1 and vk_get_i32(f12, VkPhysicalDeviceVulkan12Features_runtimeDescriptorArray) == 1
let bda = vk_get_i32(f12, VkPhysicalDeviceVulkan12Features_bufferDeviceAddress) == 1
let timeline = vk_get_i32(f12, VkPhysicalDeviceVulkan12Features_timelineSemaphore) == 1
let indirect = vk_get_i32(f12, VkPhysicalDeviceVulkan12Features_drawIndirectCount) == 1
let major = (api >> 22) & 127
let minor = (api >> 12) & 1023
let is13 = major > 1 or (major == 1 and minor >= 3)
let floor = is13 and dyn and sync2 and bindless and bda and timeline and indirect
print(`vulkan: floor (1.3): {yes(floor)} | api {yes(is13)} dynamic rendering {yes(dyn)} sync2 {yes(sync2)} bindless {yes(bindless)} buffer addresses {yes(bda)} timeline {yes(timeline)} indirect count {yes(indirect)}`)
vk_put_i32(cnt, 0, 0)
vk_enumerate_device_extension_properties(pd, null, cnt, null)
let nde = vk_get_i32(cnt, 0)
let dexts = bytes(nde * VkExtensionProperties_sizeof + 8)
vk_enumerate_device_extension_properties(pd, null, cnt, dexts)
let rt = has_ext(dexts, nde, VK_KHR_RAY_QUERY_EXTENSION_NAME) and has_ext(dexts, nde, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME)
print(`vulkan: optional: ray query {yes(rt)} | mesh shaders {yes(has_ext(dexts, nde, VK_EXT_MESH_SHADER_EXTENSION_NAME))} | opacity micromaps {yes(has_ext(dexts, nde, VK_EXT_OPACITY_MICROMAP_EXTENSION_NAME))} | shading rate {yes(has_ext(dexts, nde, VK_KHR_FRAGMENT_SHADING_RATE_EXTENSION_NAME))} | memory budget {yes(has_ext(dexts, nde, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME))} | pipeline library {yes(has_ext(dexts, nde, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME))} | Reflex {yes(has_ext(dexts, nde, VK_NV_LOW_LATENCY_2_EXTENSION_NAME))}`)
vk_put_i32(cnt, 0, 0)
vk_get_physical_device_queue_family_properties(pd, cnt, null)
let nq = vk_get_i32(cnt, 0)
let qprops = bytes(nq * VkQueueFamilyProperties_sizeof + 8)
vk_get_physical_device_queue_family_properties(pd, cnt, qprops)
var gfx = -1
var compute_only = -1
var transfer_only = -1
for q in 0 .. nq {
let flags = vk_get_i32(qprops, q * VkQueueFamilyProperties_sizeof + VkQueueFamilyProperties_queueFlags)
if gfx < 0 and (flags & VK_QUEUE_GRAPHICS_BIT) != 0 { gfx = q }
if compute_only < 0 and (flags & VK_QUEUE_GRAPHICS_BIT) == 0 and (flags & VK_QUEUE_COMPUTE_BIT) != 0 { compute_only = q }
if transfer_only < 0 and (flags & (VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT)) == 0 and (flags & VK_QUEUE_TRANSFER_BIT) != 0 { transfer_only = q }
}
print(`vulkan: queues: {nq} families, graphics {gfx}, dedicated compute {compute_only}, dedicated transfer {transfer_only}`)
if gfx >= 0 {
let prio = bytes(4)
vk_put_i32(prio, 0, 0x3F800000)
let qci = bytes(VkDeviceQueueCreateInfo_sizeof)
vk_zero(qci, VkDeviceQueueCreateInfo_sizeof)
vk_put_i32(qci, VkDeviceQueueCreateInfo_sType, VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO)
vk_put_i32(qci, VkDeviceQueueCreateInfo_queueFamilyIndex, gfx)
vk_put_i32(qci, VkDeviceQueueCreateInfo_queueCount, 1)
vk_put_ptr(qci, VkDeviceQueueCreateInfo_pQueuePriorities, prio)
let dci = bytes(VkDeviceCreateInfo_sizeof)
vk_zero(dci, VkDeviceCreateInfo_sizeof)
vk_put_i32(dci, VkDeviceCreateInfo_sType, VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO)
vk_put_i32(dci, VkDeviceCreateInfo_queueCreateInfoCount, 1)
vk_put_ptr(dci, VkDeviceCreateInfo_pQueueCreateInfos, qci)
let dout = bytes(8)
let dr = vk_create_device(pd, dci, null, dout)
if dr != VK_SUCCESS { print(`vulkan: vkCreateDevice failed, VkResult {dr}`) }
else {
let dev = vk_get_ptr(dout, 0)
let qout = bytes(8)
vk_get_device_queue(dev, gfx, 0, qout)
print(`vulkan: device opened, graphics queue {yes(vk_get_ptr(qout, 0) != null)}`)
vk_destroy_device(dev, null)
}
}
}
vk_destroy_instance(inst, null)
print("VKPROBE OK")
quit()
}
}

21
runtime/native/vk.ludic Normal file
View file

@ -0,0 +1,21 @@
# ============================================================================
# vk.ludic — Vk.*: Vulkan for Ludic.
#
# Every command of Vulkan 1.0-1.4 and of the extensions the render3d Vulkan renderer
# is built around is bound in vk_api.ludic (generated by `ludic-dev vkgen` from the
# Vulkan registry), with every constant and every struct's size and field offsets.
# Vk.<snake_name>(...) calls one: Vk.create_instance(info, null, out). Structs are
# plain memory filled by field name:
# let ci = bytes(VkInstanceCreateInfo_sizeof)
# Vk.zero(ci, VkInstanceCreateInfo_sizeof)
# Vk.put_i32(ci, VkInstanceCreateInfo_sType, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO)
#
# Numbers: a C float is float bits in an int; uint64_t, VkDeviceSize and
# non-dispatchable handles are `long`. A negative long has to come from a `long`
# variable - an int literal passed straight to a long parameter is zero-extended.
#
# The loader is opened at run time by Vk.open() (vk_win.ll / vk_mac.ll), never
# linked: 0 means this machine has no Vulkan, and the program carries on without it.
# ============================================================================
import "vk_api.ludic"

5979
runtime/native/vk_api.ludic Normal file

File diff suppressed because it is too large Load diff

110
runtime/native/vk_mac.ll Normal file
View file

@ -0,0 +1,110 @@
; ============================================================================
; vk_mac.ll — the Vulkan loader, for macOS, written in LLVM IR.
;
; 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:
;
; lvk_open() -> 1 | 0 lvk_sym(name) -> ptr lvk_has(name) -> 1 | 0
;
; MoltenVK is a portability driver: an instance must ask for
; VK_KHR_portability_enumeration (and the ENUMERATE_PORTABILITY flag) to see it.
; ============================================================================
declare ptr @dlopen(ptr, i32)
declare ptr @dlsym(ptr, ptr)
declare ptr @getenv(ptr)
declare i32 @snprintf(ptr, i64, ptr, ...)
declare i32 @lvk_bind()
@.lvk_c0 = private unnamed_addr constant [18 x i8] c"libvulkan.1.dylib\00"
@.lvk_c1 = private unnamed_addr constant [35 x i8] c"@executable_path/libvulkan.1.dylib\00"
@.lvk_c2 = private unnamed_addr constant [33 x i8] c"/usr/local/lib/libvulkan.1.dylib\00"
@.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_lib = internal global ptr null
; RTLD_NOW (2) | RTLD_LOCAL (4)
define internal ptr @lvk_try(ptr %path) {
entry:
%h = call ptr @dlopen(ptr %path, i32 6)
ret ptr %h
}
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 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:
%buf = alloca [1024 x i8]
%w = call i32 (ptr, i64, ptr, ...) @snprintf(ptr %buf, i64 1024, ptr @.lvk_fmt, ptr %sdk)
%h4 = call ptr @lvk_try(ptr %buf)
%n4 = icmp eq ptr %h4, null
br i1 %n4, label %fail, label %got4
got4:
store ptr %h4, ptr @lvk_lib
br label %bind
bind:
%n = call i32 @lvk_bind()
br label %ok
ok:
ret i32 1
fail:
ret i32 0
}
define ptr @lvk_sym(ptr %name) {
entry:
%lib = load ptr, ptr @lvk_lib
%none = icmp eq ptr %lib, null
br i1 %none, label %no, label %look
look:
%p = call ptr @dlsym(ptr %lib, ptr %name)
ret ptr %p
no:
ret ptr null
}
define i32 @lvk_has(ptr %name) {
entry:
%p = call ptr @lvk_sym(ptr %name)
%there = icmp ne ptr %p, null
%r = zext i1 %there to i32
ret i32 %r
}

3392
runtime/native/vk_thunks.ll Normal file

File diff suppressed because it is too large Load diff

61
runtime/native/vk_win.ll Normal file
View file

@ -0,0 +1,61 @@
; ============================================================================
; vk_win.ll — the Vulkan loader, for Windows, written in LLVM IR.
;
; vulkan-1.dll ships with every GPU driver on Windows 10 and 11, but it is not
; guaranteed, so it is opened AT RUN TIME and never linked: a program that asks for
; Vulkan on a machine without it gets 0 from vk_open and falls back to OpenGL
; rather than failing to start.
;
; lvk_open() -> 1 | 0 open the loader and bind every command (vk_thunks.ll)
; lvk_sym(name) -> ptr one entry point by name, null when the loader lacks it
; lvk_has(name) -> 1 | 0 whether a command is there (an extension's, say)
;
; The commands themselves, their struct layouts and constants are generated from
; the Vulkan registry by `ludic-dev vkgen` into vk_api.ludic and vk_thunks.ll.
; ============================================================================
declare ptr @LoadLibraryA(ptr)
declare ptr @GetProcAddress(ptr, ptr)
declare i32 @lvk_bind()
@.lvk_dll = private unnamed_addr constant [13 x i8] c"vulkan-1.dll\00"
@lvk_lib = internal global ptr null
define i32 @lvk_open() {
entry:
%have = load ptr, ptr @lvk_lib
%open = icmp ne ptr %have, null
br i1 %open, label %ok, label %load
load:
%lib = call ptr @LoadLibraryA(ptr @.lvk_dll)
%none = icmp eq ptr %lib, null
br i1 %none, label %fail, label %bind
bind:
store ptr %lib, ptr @lvk_lib
%n = call i32 @lvk_bind()
br label %ok
ok:
ret i32 1
fail:
ret i32 0
}
define ptr @lvk_sym(ptr %name) {
entry:
%lib = load ptr, ptr @lvk_lib
%none = icmp eq ptr %lib, null
br i1 %none, label %no, label %look
look:
%p = call ptr @GetProcAddress(ptr %lib, ptr %name)
ret ptr %p
no:
ret ptr null
}
define i32 @lvk_has(ptr %name) {
entry:
%p = call ptr @lvk_sym(ptr %name)
%there = icmp ne ptr %p, null
%r = zext i1 %there to i32
ret i32 %r
}

View file

@ -909,6 +909,18 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
# 478 gl3.h entry points bound as externs gl_<snake_name>, plus the Ludic
# helpers (gl_open / gl_swap / gl_screenshot / gl_program / …). Labels are the
# declaration's parameter names, so `Gl.clear_color(red: 0.1, …)` works.
# Vk.* — Vulkan (runtime/native/vk.ludic + the generated vk_api.ludic): the
# registry's commands as externs vk_<snake_name>, and the loader and struct helpers.
if (ns == "Vk") and (bare == null) {
bare = "vk_" + meth
var vdecl = find_fn(bare)
if (vdecl == null) { vdecl = find_extern(bare) }
if (vdecl != null) {
let vnames = param_labels(vdecl)
var vi = 0
while vi < len(vnames) { push(labels, vnames[vi]); vi += 1 }
}
}
if (ns == "Gl") and (bare == null) {
bare = "gl_" + meth
var gdecl = find_fn(bare)

View file

@ -251,6 +251,8 @@ function p_postfix() -> Node {
if e.a.kind == E_ID and e.a.s == "Http" { g_uses_http = true }
# Gl.* — any Gl method splices the OpenGL runtime (and links the GL backend).
if e.a.kind == E_ID and e.a.s == "Gl" { g_uses_gl = true }
# Vk.* — any Vk method splices the Vulkan runtime (and links its loader)
if e.a.kind == E_ID and e.a.s == "Vk" { g_uses_vk = true }
# Tween.to/chain/delay/value/stop/parallel (#48): the fluent stateful handles
# live in tween.ludic, advanced by an engine-owned system each Update tick.
if e.a.kind == E_ID and e.a.s == "Tween" and (e.s == "to" or e.s == "chain" or e.s == "delay" or e.s == "value" or e.s == "stop" or e.s == "parallel") { g_uses_tween_rt = true }
@ -673,6 +675,7 @@ var g_uses_fx: bool = false # Fx.sparks/number/clear -> splice fx.ludic; f
var g_uses_audio: bool = false # Audio.* (#22) -> splice audio.ludic; a windowed build also links audio.ll + AVFoundation
var g_uses_http: bool = false # Http.* (#6) -> splice http.ludic; links http.ll + Foundation (macOS)
var g_uses_gl: bool = false # Gl.* -> splice gl.ludic (+ generated gl_api.ludic); links gl.ll + gl_thunks.ll + OpenGL
var g_uses_vk: bool = false # Vk.* -> splice vk.ludic (+ generated vk_api.ludic); links vk_thunks.ll + the platform loader
# issue #64: functions marked @System(Phase) in a prebuilt binary module — the
# compiler registers each with the host at load (it supplies the fn address,
# which Ludic source cannot take). Parallel arrays: fn name -> phase name.
@ -1168,6 +1171,13 @@ function maybe_splice_runtime() -> void {
do_import("runtime/native/gl.ludic")
cur_dir = saved
}
# Vk.*: splice the Vulkan surface (vk.ludic + the generated vk_api.ludic). The
# loader is opened at run time, so a program that never calls Vk.open needs none.
if g_uses_vk {
cur_dir = ""
do_import("runtime/native/vk.ludic")
cur_dir = saved
}
# Anim.play/Motion.to sugar (#48): the writes live in systems.ludic and use the
# reflection ABI, so splice it and force the world table even when the game does
# not otherwise trip uses_engine_systems.
@ -1239,6 +1249,7 @@ function parse_program() -> void {
g_toggled_layers = new []pointer
g_uses_regex = false
g_uses_gl = false
g_uses_vk = false
g_uses_bignum = false
g_uses_dict = false
g_uses_numeric = false

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -294,6 +294,15 @@ entry {
cmd = `{cmd} {gl} {glt} -framework OpenGL`
}
}
# Vk.* links the generated thunks and the loader that fills them at run time:
# vk_win.ll (vulkan-1.dll) on Windows, vk_mac.ll (libvulkan.1.dylib, MoltenVK) on
# macOS. Neither links a Vulkan library, so the program starts without one.
if g_uses_vk {
let vkt = join_path(home, "runtime/native/vk_thunks.ll")
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}`
}
# 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
# the C runtime's ordinary main() entry rather than WinMain.

View file

@ -48,7 +48,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
if mode == 2 {
if not shq(`{ludicc()} --headless {src} --emit-llvm -o {ll}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{http_link_flags(ll)}{pbf} -o {out}`) { return false }
if not shq(`{cc()} -O2 {ll}{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) }
return true
}
@ -58,7 +58,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
# canonical `ludicc -o` path links it only when Audio.* is used.
let cocoa = `{home}runtime/native/cocoa.ll`
let audio = `{home}runtime/native/audio.ll`
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{http_link_flags(ll)}{pbf} -o {out}`) { return false }
if not shq(`{cc()} -O2 {ll} {cocoa} {audio} -framework Cocoa -Wl,-needed_framework,GameController -Wl,-needed_framework,AVFoundation -Wl,-rpath,@loader_path{gl_link_flags(ll)}{vk_link_flags(ll)}{http_link_flags(ll)}{pbf} -o {out}`) { return false }
if not save { shell(`rm -f {ll}`) }
return true
}
@ -71,6 +71,15 @@ function gl_link_flags(ll: pointer) -> pointer {
return ` {home}runtime/native/gl.ll {home}runtime/native/gl_thunks.ll -framework OpenGL`
}
# A program that uses Vk.* references the @lvk_* thunks; link them and the loader that
# fills them at run time (vk_mac.ll opens libvulkan.1.dylib - MoltenVK - when it is
# there). No Vulkan library is linked, so the program still starts on a Mac without one.
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`
}
# A program that uses Http.* calls the @hs_* transport; link http.ll and Foundation only
# then, in both modes, as `ludicc -o` does (selfhost/main.ludic). Without it a Http.*
# program built through `ludic build` failed to link on every hs_* symbol.

View file

@ -33,6 +33,7 @@ program LudicDev {
import "docgen_gen.ludic"
import "docgen_check.ludic"
import "glgen.ludic"
import "vkgen.ludic"
import "assets.ludic"
import "release.ludic"
import "pkg.ludic"
@ -69,6 +70,7 @@ program LudicDev {
print(" docs-check [DIR] coverage/integrity guard over a generated docs site")
print(" docs-palette [--check] regenerate emit_color.ludic + palette.json from the palette table")
print(" glgen [--check] regenerate gl_api.ludic + gl_thunks.ll from the platform gl3.h")
print(" vkgen [--xml f] [--check] regenerate vk_api.ludic + vk_thunks.ll from the Vulkan registry")
print("")
print("release:")
print(" release [major|minor|patch] [--dry-run] [--publish]")
@ -106,6 +108,7 @@ program LudicDev {
if (cmd == "lint-asset") { return cmd_lint_asset() }
if (cmd == "docs-palette") { return cmd_docs_palette() }
if (cmd == "glgen") { return cmd_glgen() }
if (cmd == "vkgen") { return cmd_vkgen() }
if (cmd == "fetch-assets") { return cmd_fetch_assets() }
if (cmd == "docs-gen") { return cmd_docs_gen() }
if (cmd == "docs-check") { return cmd_docs_check() }

View file

@ -809,5 +809,13 @@ function cmd_dev_test() -> int {
} else { bad2("ludic-dev glgen --check", capture_line(`tail -1 {tmp_dir()}/glgen.out`)) }
} else { skip("ludic-dev glgen --check (needs the macOS OpenGL headers)") }
# and the Vulkan one: vk_api.ludic + vk_thunks.ll regenerated from the registry's vk.xml,
# which comes with the Vulkan SDK - so this guard runs wherever the SDK is installed
if shq("ls $VULKAN_SDK/share/vulkan/registry/vk.xml $HOME/VulkanSDK/*/macOS/share/vulkan/registry/vk.xml >/dev/null 2>&1") {
if shq(`bin/ludic-dev vkgen --check > {tmp_dir()}/vkgen.out 2>&1`) {
ok("ludic-dev vkgen regenerates vk_api.ludic + vk_thunks.ll byte-identically")
} else { bad2("ludic-dev vkgen --check", capture_line(`tail -1 {tmp_dir()}/vkgen.out`)) }
} else { skip("ludic-dev vkgen --check (needs the Vulkan SDK's vk.xml)") }
return report()
}

928
tools/ludic-cli/vkgen.ludic Normal file
View file

@ -0,0 +1,928 @@
# vkgen.ludic — the Vulkan binding generator, in Ludic (glgen.ludic's sibling).
#
# ludic-dev vkgen [--xml <vk.xml>] [--check]
# read the Vulkan registry and emit
# runtime/native/vk_api.ludic constants, struct sizes and field offsets, externs
# runtime/native/vk_thunks.ll one thunk per command, calling through a table
# that @lvk_bind fills from the platform loader
#
# Vulkan is too large to bind by hand: the renderer fills hundreds of structs, and a
# wrong offset raises no error - the driver reads garbage. So the layout is computed
# here from the registry, once, with the x64 / arm64 C rules (both LP64 for this
# purpose: pointers and handles 8 bytes, enums 4), and Ludic code writes fields by
# name: vk_put_i32(ci, VkDeviceCreateInfo_queueCreateInfoCount, 1), and sizes them
# with <Struct>_sizeof (not _size: VkBufferCreateInfo has a field called size).
#
# What is bound is Vulkan 1.0-1.4 core plus the extensions in VKG_EXTENSIONS - the
# ones the Vulkan renderer is designed around. Numbers follow the renderer's rules:
# a C float parameter is an IEEE bit pattern in an int (not fixed), a 64-bit value
# (uint64_t, VkDeviceSize, a non-dispatchable handle) is a `long`.
#
# The registry is found at --xml, else $VULKAN_SDK/share/vulkan/registry/vk.xml,
# else the newest ~/VulkanSDK/*/macOS/share/vulkan/registry/vk.xml.
const VKG_EXTENSIONS: string = "VK_KHR_surface VK_KHR_win32_surface VK_EXT_metal_surface VK_KHR_portability_enumeration VK_KHR_portability_subset VK_EXT_debug_utils VK_KHR_swapchain VK_EXT_swapchain_colorspace VK_EXT_hdr_metadata VK_KHR_present_id VK_KHR_present_wait VK_NV_low_latency2 VK_EXT_memory_budget VK_EXT_mesh_shader VK_KHR_deferred_host_operations VK_KHR_acceleration_structure VK_KHR_ray_query VK_EXT_opacity_micromap VK_KHR_fragment_shading_rate VK_KHR_pipeline_library VK_EXT_graphics_pipeline_library VK_EXT_descriptor_buffer VK_KHR_dynamic_rendering_local_read VK_EXT_pageable_device_local_memory VK_KHR_pipeline_binary VK_EXT_calibrated_timestamps VK_KHR_get_surface_capabilities2 VK_EXT_memory_priority"
# ---- a string -> int map --------------------------------------------------------------
# (the tools already define a set_has, so the spliced Dict runtime cannot come in here)
property VkMap { keys: pointers, vals: words, cap: int = 0, count: int = 0 }
function vkm_new() -> VkMap {
let m = new VkMap
m.cap = 16384
m.keys = bytes(m.cap * 8)
for i in 0 .. m.cap { m.keys[i] = null }
m.vals = words(m.cap)
return m
}
function vkm_slot(m: VkMap, key: pointer) -> int {
var h = 5381
let n = slen(key)
for i in 0 .. n { h = ((h * 33) + key[i]) & 0x7FFFFFFF }
var at = h % m.cap
while m.keys[at] != null and not (m.keys[at] == key) { at = (at + 1) % m.cap }
return at
}
function vkm_grow(m: VkMap) -> void {
let ok = m.keys; let ov = m.vals; let oc = m.cap
m.cap = oc * 2
m.keys = bytes(m.cap * 8)
for i in 0 .. m.cap { m.keys[i] = null }
m.vals = words(m.cap)
m.count = 0
for i in 0 .. oc { if ok[i] != null { vkm_set(m, ok[i], ov[i]) } }
}
function vkm_set(m: VkMap, key: pointer, v: int) -> void {
if (m.count + 1) * 10 > m.cap * 7 { vkm_grow(m) }
let at = vkm_slot(m, key)
if m.keys[at] == null { m.keys[at] = key; m.count += 1 }
m.vals[at] = v
}
function vkm_get_or(m: VkMap, key: pointer, dflt: int) -> int {
let at = vkm_slot(m, key)
if m.keys[at] == null { return dflt }
return m.vals[at]
}
# ---- xml helpers --------------------------------------------------------------
# The registry is regular enough to scan: every tag this generator reads opens and
# closes the way the spec's own generators expect.
# the value of attribute `name` inside the tag text `tag`, or null
function vkg_attr(tag: pointer, name: pointer) -> pointer {
let key = ` {name}="`
let at = s_index(tag, key, 0)
if at < 0 { return null }
let from = at + slen(key)
let to = s_index(tag, "\"", from)
if to < 0 { return null }
return str_sub(tag, from, to)
}
# string equality that treats a missing value (null) as equal to nothing: Ludic's `==`
# on strings reads through both sides, and an absent attribute is null
function vkg_eq(a: pointer, b: pointer) -> bool {
if a == null or b == null { return false }
return a == b
}
# whole-token membership in a comma-separated list ("vulkan,vulkansc")
function vkg_in_csv(list: pointer, want: pointer) -> bool {
if list == null { return false }
let n = slen(list)
var p = 0
while p <= n {
var q = p
while q < n and list[q] != ',' { q += 1 }
if str_sub(list, p, q) == want { return true }
if q >= n { break }
p = q + 1
}
return false
}
# a tag or element applies to desktop Vulkan when it names no api, or names "vulkan"
function vkg_api_ok(tag: pointer) -> bool {
let api = vkg_attr(tag, "api")
if api == null { return true }
return vkg_in_csv(api, "vulkan")
}
# text with <comment>...</comment> removed and every other tag replaced by a space
function vkg_strip(s: pointer) -> pointer {
let b = sb_new()
let n = slen(s)
var i = 0
while i < n {
if s[i] == '<' {
if s_starts_at(s, i, "<comment>") {
let e = s_index(s, "</comment>", i)
if e < 0 { break }
i = e + 10
continue
}
while i < n and s[i] != '>' { i += 1 }
sb_putc(b, 32)
i += 1
continue
}
sb_putc(b, s[i])
i += 1
}
return sb_str(b)
}
# the text between <tag> and </tag> after `from`, or null
function vkg_inner(s: pointer, tag: pointer) -> pointer {
# <name> or <name alias="...">: the registry puts attributes on inner tags too
var a = s_index(s, `<{tag}>`, 0)
let b = s_index(s, `<{tag} `, 0)
if a < 0 or (b >= 0 and b < a) { a = b }
if a < 0 { return null }
let gt = s_index(s, ">", a)
if gt < 0 { return null }
let from = gt + 1
let e = s_index(s, `</{tag}>`, from)
if e < 0 { return null }
return str_sub(s, from, e)
}
function vkg_unquote(v: pointer) -> pointer {
if v == null { return null }
if s_starts(v, "&quot;") and slen(v) >= 12 { return str_sub(v, 6, slen(v) - 6) }
return v
}
# ---- one declarator: <member>/<param> -> base type, pointer depth, name, array, bits
var vkg_d_base: pointer = null
var vkg_d_star: int = 0
var vkg_d_name: pointer = null
var vkg_d_count: int = 1 # product of every [N]; 1 when not an array
var vkg_d_array: bool = false
var vkg_d_bits: int = 0 # a bitfield width, 0 when not one
function vkg_decl(body: pointer) -> void {
vkg_d_base = vkg_inner(body, "type")
vkg_d_name = vkg_inner(body, "name")
vkg_d_star = 0
vkg_d_count = 1
vkg_d_array = false
vkg_d_bits = 0
let flat = vkg_strip(body)
let n = slen(flat)
var i = 0
while i < n {
if flat[i] == '*' { vkg_d_star += 1 }
i += 1
}
# everything after the name: [N], [ENUM], :bits
let ne = s_index(body, "</name>", 0)
if ne < 0 { return }
let tail = vkg_strip(str_sub(body, ne + 7, slen(body)))
let tn = slen(tail)
i = 0
while i < tn {
if tail[i] == '[' {
var j = i + 1
while j < tn and tail[j] != ']' { j += 1 }
let dim = s_trim(str_sub(tail, i + 1, j))
vkg_d_array = true
vkg_d_count = vkg_d_count * vkg_const_int(dim)
i = j + 1
continue
}
if tail[i] == ':' {
var j = i + 1
while j < tn and tail[j] >= '0' and tail[j] <= '9' { j += 1 }
vkg_d_bits = Text.to_int(str_sub(tail, i + 1, j))
i = j
continue
}
i += 1
}
}
# ---- the type table ---------------------------------------------------------------
const VK_T_OPAQUE: int = 0 # size unknown: only ever behind a pointer
const VK_T_SCALAR: int = 1 # a C scalar, size in vkg_ty_size
const VK_T_PTR: int = 2 # a pointer: typedef'd pointers, function pointers, ObjC ids
const VK_T_HANDLE: int = 3 # a dispatchable handle (a pointer)
const VK_T_NDHANDLE: int = 4 # a non-dispatchable handle (uint64_t)
const VK_T_ENUM: int = 5 # 4 bytes
const VK_T_STRUCT: int = 6
const VK_T_UNION: int = 7
const VK_T_ALIAS: int = 8 # another type's name, in vkg_ty_alias
var vkg_types: VkMap = null
var vkg_ty_name: []pointer = null
var vkg_ty_kind: []int = null
var vkg_ty_size: []int = null # -1 not laid out yet, -2 being laid out
var vkg_ty_align: []int = null
var vkg_ty_alias: []pointer = null
var vkg_ty_moff: []int = null # a struct's members: first index into vkg_m_*
var vkg_ty_mcnt: []int = null
var vkg_ty_float: []bool = null # a float scalar (bound as float bits)
var vkg_m_base: []pointer = null
var vkg_m_star: []int = null
var vkg_m_name: []pointer = null
var vkg_m_count: []int = null
var vkg_m_array: []bool = null
var vkg_m_bits: []int = null
var vkg_m_off: []int = null
function vkg_ty_add(name: pointer, kind: int, size: int) -> int {
let i = len(vkg_ty_name)
push(vkg_ty_name, name); push(vkg_ty_kind, kind); push(vkg_ty_size, size); push(vkg_ty_align, size)
push(vkg_ty_alias, null); push(vkg_ty_moff, 0); push(vkg_ty_mcnt, 0); push(vkg_ty_float, false)
vkm_set(vkg_types, name, i + 1)
return i
}
function vkg_ty_find(name: pointer) -> int {
if name == null { return -1 }
return vkm_get_or(vkg_types, name, 0) - 1
}
# follow aliases to the type that has a layout
function vkg_ty_real(i0: int) -> int {
var i = i0
var guard = 0
while i >= 0 and vkg_ty_kind[i] == VK_T_ALIAS and guard < 16 { i = vkg_ty_find(vkg_ty_alias[i]); guard += 1 }
return i
}
function vkg_scalars() -> void {
vkg_ty_add("void", VK_T_OPAQUE, 0)
let one = "char int8_t uint8_t"
let two = "int16_t uint16_t"
let four = "int int32_t uint32_t DWORD"
let eight = "int64_t uint64_t size_t double"
for k in 0 .. 4 {
var set = one; var sz = 1
if k == 1 { set = two; sz = 2 }
if k == 2 { set = four; sz = 4 }
if k == 3 { set = eight; sz = 8 }
let parts = Text.split(set, " ")
for p in 0 .. len(parts) { vkg_ty_add(parts[p], VK_T_SCALAR, sz) }
}
let f = vkg_ty_add("float", VK_T_SCALAR, 4)
vkg_ty_float[f] = true
let ptrs = Text.split("HINSTANCE HWND HMONITOR HANDLE LPCWSTR", " ")
for p in 0 .. len(ptrs) { vkg_ty_add(ptrs[p], VK_T_PTR, 8) }
}
# the end of the element that opened at `at` in the <types> block: the last
# </type> before the next <type opens (struct bodies hold inner <type> elements)
function vkg_elem_end(x: pointer, at: int, stop: int) -> int {
var next = s_index(x, "<type ", at + 6)
if next < 0 or next > stop { next = stop }
var last = -1
var p = s_index(x, "</type>", at)
while p >= 0 and p < next { last = p; p = s_index(x, "</type>", p + 7) }
return last
}
function vkg_parse_types(x: pointer) -> void {
let start = s_index(x, "<types", 0)
let stop = s_index(x, "</types>", start)
var at = s_index(x, "<type ", start)
while at >= 0 and at < stop {
let te = s_index(x, ">", at)
let tag = str_sub(x, at, te + 1)
let selfclose = x[te - 1] == '/'
var body: pointer = ""
var next_at = te + 1
if not selfclose {
let e = vkg_elem_end(x, at, stop)
if e > te { body = str_sub(x, te + 1, e); next_at = e + 7 }
}
if vkg_api_ok(tag) { vkg_type_one(tag, body) }
at = s_index(x, "<type ", next_at)
}
}
function vkg_type_one(tag: pointer, body: pointer) -> void {
let cat = vkg_attr(tag, "category")
var name = vkg_attr(tag, "name")
if name == null { name = vkg_inner(body, "name") }
if name == null { return }
let alias = vkg_attr(tag, "alias")
if alias != null {
if vkg_ty_find(name) < 0 { let i = vkg_ty_add(name, VK_T_ALIAS, -1); vkg_ty_alias[i] = alias }
return
}
if vkg_ty_find(name) >= 0 { return }
if cat == null {
# requires="..." platform names this generator does not already know
vkg_ty_add(name, VK_T_OPAQUE, 0)
return
}
if cat == "struct" or cat == "union" {
var kind = VK_T_STRUCT
if cat == "union" { kind = VK_T_UNION }
let i = vkg_ty_add(name, kind, -1)
vkg_ty_moff[i] = len(vkg_m_name)
var cnt = 0
var m = s_index(body, "<member", 0)
while m >= 0 {
let me = s_index(body, ">", m)
let mtag = str_sub(body, m, me + 1)
let close = s_index(body, "</member>", me)
if close < 0 { break }
if vkg_api_ok(mtag) {
vkg_decl(str_sub(body, me + 1, close))
push(vkg_m_base, vkg_d_base); push(vkg_m_star, vkg_d_star); push(vkg_m_name, vkg_d_name)
push(vkg_m_count, vkg_d_count); push(vkg_m_array, vkg_d_array); push(vkg_m_bits, vkg_d_bits)
push(vkg_m_off, -1)
cnt += 1
}
m = s_index(body, "<member", close)
}
vkg_ty_mcnt[i] = cnt
return
}
if cat == "handle" {
if s_contains(body, "NON_DISPATCHABLE") { vkg_ty_add(name, VK_T_NDHANDLE, 8) } else { vkg_ty_add(name, VK_T_HANDLE, 8) }
return
}
if cat == "enum" { vkg_ty_add(name, VK_T_ENUM, 4); return }
if cat == "bitmask" {
if s_contains(body, "VkFlags64") { vkg_ty_add(name, VK_T_SCALAR, 8) } else { vkg_ty_add(name, VK_T_SCALAR, 4) }
return
}
if cat == "funcpointer" { vkg_ty_add(name, VK_T_PTR, 8); return }
if cat == "basetype" {
let flat = vkg_strip(body)
if s_contains(flat, "*") or s_contains(body, "__OBJC__") { vkg_ty_add(name, VK_T_PTR, 8); return }
let base = vkg_inner(body, "type")
if base != null and s_contains(flat, "typedef") {
let i = vkg_ty_add(name, VK_T_ALIAS, -1)
vkg_ty_alias[i] = base
return
}
vkg_ty_add(name, VK_T_OPAQUE, 0)
return
}
# include / define: nothing to bind
}
# ---- layout -------------------------------------------------------------------------
# C rules: each member at the next multiple of its alignment, the struct padded to its
# largest; consecutive bitfields share a unit of their declared type while they fit.
function vkg_layout(i0: int) -> bool {
let i = vkg_ty_real(i0)
if i < 0 { return false }
let k = vkg_ty_kind[i]
if k != VK_T_STRUCT and k != VK_T_UNION { return vkg_ty_size[i] > 0 }
if vkg_ty_size[i] > 0 { return true }
if vkg_ty_size[i] == -2 or vkg_ty_size[i] == -3 { return false }
vkg_ty_size[i] = -2
var off = 0
var maxal = 1
var unit_at = -1; var unit_sz = 0; var unit_used = 0
let m0 = vkg_ty_moff[i]
for m in m0 .. m0 + vkg_ty_mcnt[i] {
var sz = 8; var al = 8
if vkg_m_star[m] == 0 {
let t = vkg_ty_real(vkg_ty_find(vkg_m_base[m]))
if t < 0 or not vkg_layout(t) { vkg_ty_size[i] = -3; return false }
sz = vkg_ty_size[t]; al = vkg_ty_align[t]
}
if al > maxal { maxal = al }
if vkg_m_bits[m] > 0 {
if unit_at >= 0 and unit_sz == sz and unit_used + vkg_m_bits[m] <= sz * 8 {
vkg_m_off[m] = unit_at
unit_used += vkg_m_bits[m]
continue
}
off = (off + al - 1) / al * al
unit_at = off; unit_sz = sz; unit_used = vkg_m_bits[m]
vkg_m_off[m] = off
off += sz
continue
}
unit_at = -1
let total = sz * vkg_m_count[m]
if k == VK_T_UNION {
vkg_m_off[m] = 0
if total > off { off = total }
} else {
off = (off + al - 1) / al * al
vkg_m_off[m] = off
off += total
}
}
vkg_ty_size[i] = (off + maxal - 1) / maxal * maxal
vkg_ty_align[i] = maxal
return true
}
# ---- constants and enums -------------------------------------------------------------
var vkg_enums: VkMap = null # name -> index + 1 into vkg_e_*
var vkg_e_name: []pointer = null
var vkg_e_ty: []pointer = null # "int" | "long" | "string"
var vkg_e_val: []pointer = null # the literal as emitted
var vkg_wide: VkMap = null # enum type names with bitwidth="64"
function vkg_enum_add(name: pointer, ty: pointer, val: pointer) -> void {
if name == null or val == null { return }
if vkm_get_or(vkg_enums, name, 0) > 0 { return }
push(vkg_e_name, name); push(vkg_e_ty, ty); push(vkg_e_val, val)
vkm_set(vkg_enums, name, len(vkg_e_name))
}
function vkg_const_int(dim: pointer) -> int {
if slen(dim) > 0 and dim[0] >= '0' and dim[0] <= '9' { return Text.to_int(dim) }
let i = vkm_get_or(vkg_enums, dim, 0)
if i == 0 { err(`vkgen: unknown array size {dim}\n`); return 1 }
return Text.to_int(vkg_e_val[i - 1])
}
# 2^b as decimal text (b < 64), for 64-bit flag bits
function vkg_pow2(b: int) -> pointer {
if b < 31 { return string(1 << b) }
# decimal doubling on a digit string, least significant digit first
let d = words(24)
for z in 0 .. 24 { d[z] = 0 }
d[0] = 1
for s in 0 .. b {
var carry = 0
for z in 0 .. 24 { let v = d[z] * 2 + carry; d[z] = v % 10; carry = v / 10 }
}
var top = 23
while top > 0 and d[top] == 0 { top -= 1 }
let sb = sb_new()
var z = top
while z >= 0 { sb_putc(sb, '0' + d[z]); z -= 1 }
return sb_str(sb)
}
# one <enum .../> of a block or a require: its value as literal text and type
var vkg_v_ty: pointer = null
function vkg_enum_value(tag: pointer, wide: bool, extnum: int) -> pointer {
vkg_v_ty = "int"
if wide { vkg_v_ty = "long" }
let alias = vkg_attr(tag, "alias")
if alias != null {
let i = vkm_get_or(vkg_enums, alias, 0)
if i == 0 { return null }
vkg_v_ty = vkg_e_ty[i - 1]
return vkg_e_val[i - 1]
}
let bp = vkg_attr(tag, "bitpos")
if bp != null {
let b = Text.to_int(bp)
if b == 31 and not wide { return "-2147483648" }
if b >= 31 { vkg_v_ty = "long" }
return vkg_pow2(b)
}
let off = vkg_attr(tag, "offset")
if off != null {
var en = extnum
let ex = vkg_attr(tag, "extnumber")
if ex != null { en = Text.to_int(ex) }
let v = 1000000000 + (en - 1) * 1000 + Text.to_int(off)
if vkg_eq(vkg_attr(tag, "dir"), "-") { return string(0 - v) }
return string(v)
}
let raw = vkg_attr(tag, "value")
if raw == null { return null }
if s_starts(raw, "&quot;") { vkg_v_ty = "string"; return `"{vkg_unquote(raw)}"` }
if raw == "(~0U)" { vkg_v_ty = "int"; return "-1" }
if raw == "(~1U)" { vkg_v_ty = "int"; return "-2" }
if raw == "(~2U)" { vkg_v_ty = "int"; return "-3" }
if raw == "(~0ULL)" { vkg_v_ty = "long"; return "-1" }
if raw == "1000.0F" { vkg_v_ty = "int"; return "1148846080" } # float bits of 1000.0
# a float constant other than the one above has no integer meaning to bind
if s_contains(raw, ".") { return null }
var t = raw
if s_starts(t, "0x") or s_starts(t, "-") or (slen(t) > 0 and t[0] >= '0' and t[0] <= '9') {
while slen(t) > 0 and (t[slen(t) - 1] == 'U' or t[slen(t) - 1] == 'L') { t = str_sub(t, 0, slen(t) - 1) }
return t
}
return null
}
# every <enums> block: API Constants and each enum type's own values
function vkg_parse_enum_blocks(x: pointer) -> void {
var at = s_index(x, "<enums ", 0)
while at >= 0 {
let te = s_index(x, ">", at)
let tag = str_sub(x, at, te + 1)
let close = s_index(x, "</enums>", te)
if close < 0 { break }
let ename = vkg_attr(tag, "name")
let wide = vkg_eq(vkg_attr(tag, "bitwidth"), "64")
if wide and ename != null { vkm_set(vkg_wide, ename, 1) }
var e = s_index(x, "<enum ", te)
while e >= 0 and e < close {
let ee = s_index(x, ">", e)
let etag = str_sub(x, e, ee + 1)
if vkg_api_ok(etag) {
let v = vkg_enum_value(etag, wide, 0)
vkg_enum_add(vkg_attr(etag, "name"), vkg_v_ty, v)
}
e = s_index(x, "<enum ", ee)
}
at = s_index(x, "<enums ", close)
}
}
# ---- what is bound ------------------------------------------------------------------
var vkg_want_cmd: VkMap = null
var vkg_cmd_order: []pointer = null
var vkg_want_type: VkMap = null
var vkg_type_order: []pointer = null
function vkg_take_require(x: pointer, from: int, to: int, extnum: int) -> void {
var r = s_index(x, "<require", from)
while r >= 0 and r < to {
let re = s_index(x, ">", r)
let rtag = str_sub(x, r, re + 1)
var rclose = s_index(x, "</require>", re)
if x[re - 1] == '/' { rclose = re }
if rclose < 0 or rclose > to { rclose = to }
if vkg_api_ok(rtag) {
var p = s_index(x, "<", re + 1)
while p >= 0 and p < rclose {
let pe = s_index(x, ">", p)
let ptag = str_sub(x, p, pe + 1)
let nm = vkg_attr(ptag, "name")
if vkg_api_ok(ptag) and nm != null {
if s_starts(ptag, "<command ") and vkm_get_or(vkg_want_cmd, nm, 0) == 0 { vkm_set(vkg_want_cmd, nm, 1); push(vkg_cmd_order, nm) }
if s_starts(ptag, "<type ") and vkm_get_or(vkg_want_type, nm, 0) == 0 { vkm_set(vkg_want_type, nm, 1); push(vkg_type_order, nm) }
if s_starts(ptag, "<enum ") {
let ext = vkg_attr(ptag, "extends")
var wide = false
if ext != null { wide = vkm_get_or(vkg_wide, ext, 0) == 1 }
let v = vkg_enum_value(ptag, wide, extnum)
vkg_enum_add(nm, vkg_v_ty, v)
}
}
p = s_index(x, "<", pe + 1)
}
}
r = s_index(x, "<require", rclose)
}
}
function vkg_parse_selection(x: pointer) -> void {
var at = s_index(x, "<feature ", 0)
while at >= 0 {
let te = s_index(x, ">", at)
let tag = str_sub(x, at, te + 1)
let close = s_index(x, "</feature>", te)
if close < 0 { break }
if vkg_api_ok(tag) { vkg_take_require(x, te, close, 0) }
at = s_index(x, "<feature ", close)
}
let wanted = Text.split(VKG_EXTENSIONS, " ")
at = s_index(x, "<extension ", 0)
while at >= 0 {
let te = s_index(x, ">", at)
let tag = str_sub(x, at, te + 1)
var close = s_index(x, "</extension>", te)
if x[te - 1] == '/' { close = te }
if close < 0 { break }
let nm = vkg_attr(tag, "name")
var take = false
for w in 0 .. len(wanted) { if vkg_eq(wanted[w], nm) { take = true } }
if take and vkg_in_csv(vkg_attr(tag, "supported"), "vulkan") {
vkg_take_require(x, te, close, Text.to_int(vkg_attr(tag, "number")))
}
at = s_index(x, "<extension ", close)
}
}
# ---- commands -----------------------------------------------------------------------
var vkg_cmds: VkMap = null
var vkg_c_name: []pointer = null
var vkg_c_rbase: []pointer = null
var vkg_c_rstar: []int = null
var vkg_c_poff: []int = null
var vkg_c_pcnt: []int = null
var vkg_p_base: []pointer = null
var vkg_p_star: []int = null
var vkg_p_name: []pointer = null
var vkg_p_array: []bool = null
var vkg_c_alias: VkMap = null # alias command name -> index + 1 of the command it names
var vkg_alias_from: []pointer = null
var vkg_alias_to: []pointer = null
function vkg_parse_commands(x: pointer) -> void {
let start = s_index(x, "<commands", 0)
let stop = s_index(x, "</commands>", start)
var at = s_index(x, "<command", start + 9)
while at >= 0 and at < stop {
let te = s_index(x, ">", at)
let tag = str_sub(x, at, te + 1)
if x[te - 1] == '/' {
let a = vkg_attr(tag, "alias")
if a != null { push(vkg_alias_from, vkg_attr(tag, "name")); push(vkg_alias_to, a) }
at = s_index(x, "<command", te)
continue
}
let close = s_index(x, "</command>", te)
if close < 0 { break }
let body = str_sub(x, te + 1, close)
if vkg_api_ok(tag) {
let proto = vkg_inner(body, "proto")
vkg_decl(proto)
let name = vkg_d_name
if name != null and vkm_get_or(vkg_cmds, name, 0) == 0 {
push(vkg_c_name, name); push(vkg_c_rbase, vkg_d_base); push(vkg_c_rstar, vkg_d_star)
push(vkg_c_poff, len(vkg_p_name))
var cnt = 0
var p = s_index(body, "<param", 0)
while p >= 0 {
let pe = s_index(body, ">", p)
let ptag = str_sub(body, p, pe + 1)
let pc = s_index(body, "</param>", pe)
if pc < 0 { break }
# <implicitexternsyncparams> holds <param> prose, with no <type>: not a parameter
let pbody = str_sub(body, pe + 1, pc)
if vkg_api_ok(ptag) and s_contains(pbody, "<type>") {
vkg_decl(pbody)
push(vkg_p_base, vkg_d_base); push(vkg_p_star, vkg_d_star); push(vkg_p_name, vkg_d_name); push(vkg_p_array, vkg_d_array)
cnt += 1
}
p = s_index(body, "<param", pc)
}
push(vkg_c_pcnt, cnt)
vkm_set(vkg_cmds, name, len(vkg_c_name))
}
}
at = s_index(x, "<command", close)
}
for k in 0 .. len(vkg_alias_from) {
let t = vkm_get_or(vkg_cmds, vkg_alias_to[k], 0)
if t > 0 { vkm_set(vkg_c_alias, vkg_alias_from[k], t) }
}
}
# the LLVM type of a C parameter or return; null when this generator cannot bind it
function vkg_ir(base: pointer, star: int, array: bool) -> pointer {
if star > 0 or array { return "ptr" }
if base == null { return null }
if base == "void" { return "void" }
let t = vkg_ty_real(vkg_ty_find(base))
if t < 0 { return null }
let k = vkg_ty_kind[t]
if k == VK_T_HANDLE or k == VK_T_PTR { return "ptr" }
if k == VK_T_NDHANDLE { return "i64" }
if k == VK_T_ENUM { return "i32" }
if k == VK_T_SCALAR {
let sz = vkg_ty_size[t]
if sz == 1 { return "i8" }
if sz == 2 { return "i16" }
if sz == 4 { if vkg_ty_float[t] { return "float" }; return "i32" }
if sz == 8 { if vkg_ty_name[t] == "double" { return null }; return "i64" }
}
return null
}
# what carries it across the Ludic boundary
function vkg_abi(ir: pointer) -> pointer {
if ir == "ptr" or ir == "i64" or ir == "void" { return ir }
return "i32"
}
function vkg_ludic(ir: pointer) -> pointer {
if ir == "ptr" { return "pointer" }
if ir == "i64" { return "long" }
if ir == "void" { return "void" }
return "int"
}
# CamelCase -> snake_case, with digits counted as the end of a word ("Win32Surface")
function vkg_snake(n: pointer) -> pointer {
let b = sb_new()
let m = slen(n)
for i in 0 .. m {
let c = n[i]
if glg_upper(c) and i > 0 {
let prev = n[i - 1]
var cut = glg_lower(prev) or glg_digit(prev)
if glg_upper(prev) and i + 1 < m and glg_lower(n[i + 1]) { cut = true }
if cut { sb_putc(b, '_') }
}
if glg_upper(c) { sb_putc(b, c + 32) } else { sb_putc(b, c) }
}
return sb_str(b)
}
function vkg_pname(raw: pointer, i: int) -> pointer {
if raw == null { return `a{string(i)}` }
let n = vkg_snake(raw)
if glg_reserved(n) { return n + "_" }
return n
}
# the command a wanted name resolves to (itself, or the core command it aliases)
function vkg_cmd_of(name: pointer) -> int {
let i = vkm_get_or(vkg_cmds, name, 0)
if i > 0 { return i - 1 }
return vkm_get_or(vkg_c_alias, name, 0) - 1
}
function vkg_cmd_ok(c: int) -> bool {
if vkg_ir(vkg_c_rbase[c], vkg_c_rstar[c], false) == null { return false }
for p in vkg_c_poff[c] .. vkg_c_poff[c] + vkg_c_pcnt[c] {
let ir = vkg_ir(vkg_p_base[p], vkg_p_star[p], vkg_p_array[p])
if ir == null or ir == "void" { return false }
}
return true
}
# ---- emit vk_api.ludic ---------------------------------------------------------------
var vkg_n_layouts: int = 0
var vkg_n_cmds: int = 0
function vkg_emit_ludic(path: pointer) -> bool {
let b = sb_new()
sb_puts(b, "# ============================================================================\n")
sb_puts(b, "# vk_api.ludic — Vulkan 1.0-1.4 and the renderer's extensions, bound for Ludic.\n")
sb_puts(b, "# GENERATED by `ludic-dev vkgen` from the Vulkan registry (vk.xml). Do not edit.\n")
sb_puts(b, "# Constants; every struct's size (<Struct>_sizeof) and field offsets (<Struct>_<field>,\n")
sb_puts(b, "# a bitfield at the offset of its storage unit); one extern per command. A C float\n")
sb_puts(b, "# is float bits in an int; uint64_t, VkDeviceSize and non-dispatchable handles are long.\n")
sb_puts(b, "# ============================================================================\n\n")
for i in 0 .. len(vkg_e_name) {
sb_puts(b, `const {vkg_e_name[i]}: {vkg_e_ty[i]} = {vkg_e_val[i]}\n`)
}
sb_puts(b, "\n")
vkg_n_layouts = 0
for k in 0 .. len(vkg_type_order) {
let name = vkg_type_order[k]
let t = vkg_ty_real(vkg_ty_find(name))
if t < 0 { continue }
if vkg_ty_kind[t] != VK_T_STRUCT and vkg_ty_kind[t] != VK_T_UNION { continue }
if not vkg_layout(t) { continue }
vkg_n_layouts += 1
sb_puts(b, `const {name}_sizeof: int = {string(vkg_ty_size[t])}\n`)
let m0 = vkg_ty_moff[t]
for m in m0 .. m0 + vkg_ty_mcnt[t] {
if vkg_m_name[m] != null { sb_puts(b, `const {name}_{vkg_m_name[m]}: int = {string(vkg_m_off[m])}\n`) }
}
}
sb_puts(b, "\n# the loader (vk_win.ll / vk_mac.ll) and memory access for filling structs (vk_thunks.ll)\n")
sb_puts(b, "extern function vk_open() -> int = \"lvk_open\"\n")
sb_puts(b, "extern function vk_has(name: pointer) -> int = \"lvk_has\"\n")
sb_puts(b, "extern function vk_zero(p: pointer, n: int) = \"lvk_zero\"\n")
sb_puts(b, "extern function vk_at(p: pointer, off: int) -> pointer = \"lvk_at\"\n")
sb_puts(b, "extern function vk_put_i32(p: pointer, off: int, v: int) = \"lvk_put_i32\"\n")
sb_puts(b, "extern function vk_put_i64(p: pointer, off: int, v: long) = \"lvk_put_i64\"\n")
sb_puts(b, "extern function vk_put_ptr(p: pointer, off: int, v: pointer) = \"lvk_put_ptr\"\n")
sb_puts(b, "extern function vk_get_i32(p: pointer, off: int) -> int = \"lvk_get_i32\"\n")
sb_puts(b, "extern function vk_get_i64(p: pointer, off: int) -> long = \"lvk_get_i64\"\n")
sb_puts(b, "extern function vk_get_ptr(p: pointer, off: int) -> pointer = \"lvk_get_ptr\"\n\n")
vkg_n_cmds = 0
for k in 0 .. len(vkg_cmd_order) {
let want = vkg_cmd_order[k]
let c = vkg_cmd_of(want)
if c < 0 or not vkg_cmd_ok(c) { continue }
vkg_n_cmds += 1
let bare = str_sub(want, 2, slen(want))
let ps = sb_new()
var n = 0
for p in vkg_c_poff[c] .. vkg_c_poff[c] + vkg_c_pcnt[c] {
if n > 0 { sb_puts(ps, ", ") }
sb_puts(ps, `{vkg_pname(vkg_p_name[p], n)}: {vkg_ludic(vkg_ir(vkg_p_base[p], vkg_p_star[p], vkg_p_array[p]))}`)
n += 1
}
let rt = vkg_ludic(vkg_ir(vkg_c_rbase[c], vkg_c_rstar[c], false))
var rets = ""
if rt != "void" { rets = " -> " + rt }
sb_puts(b, `extern function vk_{vkg_snake(bare)}({sb_str(ps)}){rets} = "lvk_{bare}"\n`)
}
return write_file(path, sb_str(b))
}
# ---- emit vk_thunks.ll ---------------------------------------------------------------
function vkg_emit_thunks(path: pointer) -> bool {
let b = sb_new()
let bind = sb_new()
sb_puts(b, "; ============================================================================\n")
sb_puts(b, "; vk_thunks.ll — one thunk per bound Vulkan command. GENERATED by `ludic-dev vkgen`.\n")
sb_puts(b, "; Each @lvk_<Name> takes the Ludic ABI (i32 / i64 / ptr; a float as its bits) and\n")
sb_puts(b, "; calls through @lvk_p_<Name>, which @lvk_bind fills from the platform loader's\n")
sb_puts(b, "; @lvk_sym (vk_win.ll: GetProcAddress, vk_mac.ll: dlsym). A command the loader\n")
sb_puts(b, "; lacks stays null; ask vk_has(name) before calling an extension's command.\n")
sb_puts(b, "; ============================================================================\n\n")
sb_puts(b, "declare ptr @lvk_sym(ptr)\n")
sb_puts(b, "declare void @llvm.memset.p0.i64(ptr, i8, i64, i1)\n\n")
sb_puts(bind, "define i32 @lvk_bind() {\nentry:\n")
var found = 0
for k in 0 .. len(vkg_cmd_order) {
let want = vkg_cmd_order[k]
let c = vkg_cmd_of(want)
if c < 0 or not vkg_cmd_ok(c) { continue }
let bare = str_sub(want, 2, slen(want))
sb_puts(b, `@.lvkn_{bare} = private unnamed_addr constant [{string(slen(want) + 1)} x i8] c"{want}\\00"\n`)
sb_puts(b, `@lvk_p_{bare} = internal global ptr null\n`)
let cret = vkg_ir(vkg_c_rbase[c], vkg_c_rstar[c], false)
let lret = vkg_abi(cret)
let sig = sb_new(); let body = sb_new(); let call = sb_new()
var n = 0
for p in vkg_c_poff[c] .. vkg_c_poff[c] + vkg_c_pcnt[c] {
let ct = vkg_ir(vkg_p_base[p], vkg_p_star[p], vkg_p_array[p])
let lt = vkg_abi(ct)
let a = `%a{string(n)}`
if n > 0 { sb_puts(sig, ", "); sb_puts(call, ", ") }
sb_puts(sig, `{lt} {a}`)
if ct == lt { sb_puts(call, `{ct} {a}`) }
else if ct == "float" {
sb_puts(body, ` %c{string(n)} = bitcast i32 {a} to float\n`)
sb_puts(call, `float %c{string(n)}`)
} else {
sb_puts(body, ` %c{string(n)} = trunc i32 {a} to {ct}\n`)
sb_puts(call, `{ct} %c{string(n)}`)
}
n += 1
}
sb_puts(b, `define {lret} @lvk_{bare}({sb_str(sig)}) `)
sb_puts(b, "{\nentry:\n")
sb_puts(b, ` %fp = load ptr, ptr @lvk_p_{bare}\n`)
sb_puts(b, sb_str(body))
let invoke = `call {cret} %fp({sb_str(call)})`
if cret == "void" { sb_puts(b, ` {invoke}\n ret void\n`) }
else if cret == lret { sb_puts(b, ` %r = {invoke}\n ret {lret} %r\n`) }
else if cret == "float" { sb_puts(b, ` %r = {invoke}\n %z = bitcast float %r to i32\n ret i32 %z\n`) }
else { sb_puts(b, ` %r = {invoke}\n %z = zext {cret} %r to i32\n ret i32 %z\n`) }
sb_puts(b, "}\n")
let f = string(found)
sb_puts(bind, ` %s{f} = call ptr @lvk_sym(ptr @.lvkn_{bare})\n store ptr %s{f}, ptr @lvk_p_{bare}\n`)
found += 1
}
sb_puts(bind, ` ret i32 {string(found)}\n`)
sb_puts(bind, "}\n")
sb_puts(b, "\n")
sb_puts(b, sb_str(bind))
sb_puts(b, "\ndefine void @lvk_zero(ptr %p, i32 %n) {\nentry:\n %w = sext i32 %n to i64\n call void @llvm.memset.p0.i64(ptr %p, i8 0, i64 %w, i1 false)\n ret void\n}\n")
sb_puts(b, "define ptr @lvk_at(ptr %p, i32 %o) {\nentry:\n %q = getelementptr i8, ptr %p, i32 %o\n ret ptr %q\n}\n")
let kinds = Text.split("i32 i64 ptr", " ")
for k in 0 .. 3 {
let t = kinds[k]
sb_puts(b, `define void @lvk_put_{t}(ptr %p, i32 %o, {t} %v) `)
sb_puts(b, "{\nentry:\n")
sb_puts(b, ` %q = getelementptr i8, ptr %p, i32 %o\n store {t} %v, ptr %q, align 1\n ret void\n`)
sb_puts(b, "}\n")
sb_puts(b, `define {t} @lvk_get_{t}(ptr %p, i32 %o) `)
sb_puts(b, "{\nentry:\n")
sb_puts(b, ` %q = getelementptr i8, ptr %p, i32 %o\n %v = load {t}, ptr %q, align 1\n ret {t} %v\n`)
sb_puts(b, "}\n")
}
return write_file(path, sb_str(b))
}
# ---- the task ------------------------------------------------------------------------
function vkg_find_xml() -> pointer {
var i = 2
while i < arg_count() {
if arg(i) == "--xml" and i + 1 < arg_count() { return arg(i + 1) }
i += 1
}
let sdk = getenv_or("VULKAN_SDK", "")
if sdk != "" and file_exists(`{sdk}/share/vulkan/registry/vk.xml`) { return `{sdk}/share/vulkan/registry/vk.xml` }
let found = Text.trim(capture("ls -d $HOME/VulkanSDK/*/macOS/share/vulkan/registry/vk.xml /c/VulkanSDK/*/share/vulkan/registry/vk.xml 2>/dev/null | tail -1"))
if found != "" { return found }
return null
}
function cmd_vkgen() -> int {
var check = false
var i = 2
while i < arg_count() { if arg(i) == "--check" { check = true }; i += 1 }
let xpath = vkg_find_xml()
if xpath == null { err("vkgen: no vk.xml (install the Vulkan SDK, set VULKAN_SDK, or pass --xml)\n"); return 1 }
let x = read_file(xpath)
if x == null { err(`vkgen: cannot read {xpath}\n`); return 1 }
vkg_types = vkm_new(); vkg_enums = vkm_new(); vkg_wide = vkm_new()
vkg_want_cmd = vkm_new(); vkg_want_type = vkm_new(); vkg_cmds = vkm_new(); vkg_c_alias = vkm_new()
vkg_ty_name = new []pointer; vkg_ty_kind = new []int; vkg_ty_size = new []int; vkg_ty_align = new []int
vkg_ty_alias = new []pointer; vkg_ty_moff = new []int; vkg_ty_mcnt = new []int; vkg_ty_float = new []bool
vkg_m_base = new []pointer; vkg_m_star = new []int; vkg_m_name = new []pointer; vkg_m_count = new []int
vkg_m_array = new []bool; vkg_m_bits = new []int; vkg_m_off = new []int
vkg_e_name = new []pointer; vkg_e_ty = new []pointer; vkg_e_val = new []pointer
vkg_cmd_order = new []pointer; vkg_type_order = new []pointer
vkg_c_name = new []pointer; vkg_c_rbase = new []pointer; vkg_c_rstar = new []int; vkg_c_poff = new []int; vkg_c_pcnt = new []int
vkg_p_base = new []pointer; vkg_p_star = new []int; vkg_p_name = new []pointer; vkg_p_array = new []bool
vkg_alias_from = new []pointer; vkg_alias_to = new []pointer
vkg_scalars()
vkg_parse_enum_blocks(x)
vkg_parse_types(x)
vkg_parse_selection(x)
vkg_parse_commands(x)
var skipped = 0
for k in 0 .. len(vkg_cmd_order) {
let c = vkg_cmd_of(vkg_cmd_order[k])
if c < 0 or not vkg_cmd_ok(c) { skipped += 1; err(`vkgen: not bound: {vkg_cmd_order[k]}\n`) }
}
var api_out = "runtime/native/vk_api.ludic"
var thunk_out = "runtime/native/vk_thunks.ll"
if check { api_out = tmp_path("vk_api.ludic"); thunk_out = tmp_path("vk_thunks.ll") }
if not vkg_emit_ludic(api_out) { err("vkgen: cannot write vk_api.ludic\n"); return 1 }
if not vkg_emit_thunks(thunk_out) { err("vkgen: cannot write vk_thunks.ll\n"); return 1 }
if check {
if not shq(`cmp -s {api_out} runtime/native/vk_api.ludic`) { err("vk_api.ludic drifted from vk.xml (run: ludic-dev vkgen)\n"); return 1 }
if not shq(`cmp -s {thunk_out} runtime/native/vk_thunks.ll`) { err("vk_thunks.ll drifted from vk.xml (run: ludic-dev vkgen)\n"); return 1 }
}
print(`OK {string(len(vkg_e_name))} constants, {string(vkg_n_layouts)} struct layouts, {string(vkg_n_cmds)} commands ({string(skipped)} not bound) from {xpath}`)
return 0
}