Merge feat/vulkan: Vk.* and render3d's gpu.ludic seam

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 09:52:22 +03:00
commit d9c3d1a602
35 changed files with 53120 additions and 41284 deletions

View file

@ -0,0 +1,26 @@
bump: minor
type: feat
Vulkan for Ludic, and the start of a second renderer beside OpenGL.
- **`Vk.*`**: every command of Vulkan 1.0-1.4 and of the extensions a modern renderer is
built around (swapchain, HDR colour spaces, ray query and acceleration structures, opacity
micromaps, mesh shaders, variable rate shading, memory budget, pipeline libraries, NVIDIA
low latency, portability for MoltenVK), with every constant and every struct's size
(`<Struct>_sizeof`) and field offsets (`<Struct>_<field>`). Generated from the Vulkan
registry by **`ludic-dev vkgen`** into `runtime/native/vk_api.ludic` and `vk_thunks.ll`;
structs are plain memory filled by name with `Vk.put_i32` / `put_i64` / `put_ptr`. Every
size and offset was checked against the SDK's C headers (3128 facts). A C float is float
bits in an int; 64-bit values and non-dispatchable handles are `long`.
- **The loader is opened at run time**, never linked: `vk_win.ll` (`vulkan-1.dll`) and
`vk_mac.ll` (`libvulkan.1.dylib`, MoltenVK). `Vk.open()` returning 0 means no Vulkan, and
the program carries on. `ludicc` and `ludic build` link both files for any program that
uses `Vk.*`.
- `examples/rendering/vk_probe.ludic` reports what a machine's Vulkan can do;
`vk_compute.ludic` runs a Slang compute shader (`vk_compute.slang`) and reads the picture
back - on an RTX 3070 Ti and on an M4 Pro through MoltenVK, clean under the validation layer.
- **render3d `gpu.ludic`**: the seam between the renderer and a graphics API. Render state
and uniforms go through `gpu_*` / `u_*` and nowhere else (OpenGL frames unchanged);
`R3D_GFX=gl|vk` or `gpu_request` chooses a backend, falling back to OpenGL with a reason;
`gpu_caps_probe()` detects, on Windows, the Vulkan 1.3 floor, ray tracing, mesh shaders,
the NVIDIA RTX generation, Reflex and HDR, for a settings screen to grey out what a machine
cannot use (`R3D_CAPS=rtx50|rtx40|rtx30|amd|intel|none` pretends, for tests).

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()
}
}

View file

@ -66,14 +66,14 @@ function ac_prog_new(vs: string, fs: string, defs: string) -> AcProg {
let a = new AcProg
a.prog = r3d_program(vs, fs, defs)
let p = a.prog
a.l_model = gl_uniform(p, "u_model"); a.l_skin = gl_uniform(p, "u_skinned"); a.l_lvp = gl_uniform(p, "u_light_vp")
a.l_bones = gl_uniform(p, "u_bones[0]"); if a.l_bones < 0 { a.l_bones = gl_uniform(p, "u_bones") }
a.l_tint = gl_uniform(p, "u_tint"); a.l_rough = gl_uniform(p, "u_rough_scale"); a.l_emis = gl_uniform(p, "u_emissive")
a.l_view = gl_uniform(p, "u_view"); a.l_proj = gl_uniform(p, "u_proj"); a.l_mh = gl_uniform(p, "u_model_h")
a.l_out = gl_uniform(p, "u_outline"); a.l_ocol = gl_uniform(p, "u_outline_col")
a.l_model = gpu_uniform(p, "u_model"); a.l_skin = gpu_uniform(p, "u_skinned"); a.l_lvp = gpu_uniform(p, "u_light_vp")
a.l_bones = gpu_uniform(p, "u_bones[0]"); if a.l_bones < 0 { a.l_bones = gpu_uniform(p, "u_bones") }
a.l_tint = gpu_uniform(p, "u_tint"); a.l_rough = gpu_uniform(p, "u_rough_scale"); a.l_emis = gpu_uniform(p, "u_emissive")
a.l_view = gpu_uniform(p, "u_view"); a.l_proj = gpu_uniform(p, "u_proj"); a.l_mh = gpu_uniform(p, "u_model_h")
a.l_out = gpu_uniform(p, "u_outline"); a.l_ocol = gpu_uniform(p, "u_outline_col")
# the samplers never move: units 0, 1, 2
gl_use_program(p)
gl_uniform1i(gl_uniform(p, "u_diff"), 0); gl_uniform1i(gl_uniform(p, "u_nrm"), 1); gl_uniform1i(gl_uniform(p, "u_arm"), 2)
u_i(gpu_uniform(p, "u_diff"), 0); u_i(gpu_uniform(p, "u_nrm"), 1); u_i(gpu_uniform(p, "u_arm"), 2)
return a
}
function actor_init() -> void {
@ -161,14 +161,14 @@ function actor_draw_one(a: Actor, ap: AcProg, shadow: bool) -> void {
gl_use_program(p)
u_mat4(ap.l_model, a.mat)
var skinned = F_ZERO
if a.skin != null { skinned = F_ONE; gl_uniform_matrix4fv(ap.l_bones, a.skin.n_joints, 0, a.skin.bones) }
else if a.model.skin != null { skinned = F_ONE; gl_uniform_matrix4fv(ap.l_bones, a.model.skin.n_joints, 0, a.model.skin.bones) }
if a.skin != null { skinned = F_ONE; u_mat4n(ap.l_bones, a.skin.n_joints, a.skin.bones) }
else if a.model.skin != null { skinned = F_ONE; u_mat4n(ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
u_f(ap.l_skin, skinned)
if not shadow {
u_f(ap.l_emis, a.emissive)
u_f(ap.l_rough, a.rough)
}
gl_disable(GL_CULL_FACE)
gpu_cull(false)
let model = a.model
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
@ -248,7 +248,7 @@ function actor_draw() -> void {
actor_draw_one(a, ap, false)
}
actor_draw_outlines()
gl_enable(GL_CULL_FACE)
gpu_cull(true)
}
# The rim around a highlighted actor: the model again, its vertices pushed out along their
# normals (skin.vert, OUTLINE) and its front faces culled, so only the far side of the
@ -258,8 +258,8 @@ function actor_draw_outlines() -> void {
var any = ac_oq_n > 0
for i in 0 .. len(ac_actors) { if ac_actors[i].outline != 0 and ac_visible(ac_actors[i], false) { any = true; break } }
if not any { return }
gl_enable(GL_CULL_FACE)
gl_cull_face(GL_FRONT)
gpu_cull(true)
gpu_cull_face(GL_FRONT)
for i in 0 .. len(ac_actors) {
let a = ac_actors[i]
if a.outline == 0 or not ac_visible(a, false) { continue }
@ -284,13 +284,13 @@ function actor_draw_outlines() -> void {
for k in 0 .. len(q.model.prims) { mesh_draw(q.model.prims[k].mesh) }
}
ac_oq_closed = true
gl_cull_face(GL_BACK)
gpu_cull_face(GL_BACK)
}
function actor_draw_outline_one(a: Actor, ap: AcProg) -> void {
u_mat4(ap.l_model, a.mat)
var skinned = F_ZERO
if a.skin != null { skinned = F_ONE; gl_uniform_matrix4fv(ap.l_bones, a.skin.n_joints, 0, a.skin.bones) }
else if a.model.skin != null { skinned = F_ONE; gl_uniform_matrix4fv(ap.l_bones, a.model.skin.n_joints, 0, a.model.skin.bones) }
if a.skin != null { skinned = F_ONE; u_mat4n(ap.l_bones, a.skin.n_joints, a.skin.bones) }
else if a.model.skin != null { skinned = F_ONE; u_mat4n(ap.l_bones, a.model.skin.n_joints, a.model.skin.bones) }
u_f(ap.l_skin, skinned)
let model = a.model
for i in 0 .. len(model.prims) {

View file

@ -190,16 +190,16 @@ function daylight_hand(x: int, y: int, z: int, dx: int, dy: int, dz: int, cone:
v3_set(hand_color, r, g, b)
}
function daylight_bind(prog: int) -> void {
u_v3(gl_uniform(prog, "u_hand_pos"), hand_pos)
u_v3(gl_uniform(prog, "u_hand_color"), hand_color)
u_v3(gl_uniform(prog, "u_hand_dir"), hand_dir)
u_f(gl_uniform(prog, "u_hand_cone"), hand_cone)
u_v3(gl_uniform(prog, "u_ibl_scale"), day_ibl)
u_v3(gl_uniform(prog, "u_moon_dir"), day_moon_dir)
u_f(gl_uniform(prog, "u_moon_phase"), day_moon_phase)
u_f(gl_uniform(prog, "u_moon_illum"), day_moon_illum)
u_f(gl_uniform(prog, "u_moon_haze"), day_overcast)
u_f(gl_uniform(prog, "u_daylight"), day_light)
u_v3(gl_uniform(prog, "u_fire_pos"), fire_pos)
u_v3(gl_uniform(prog, "u_fire_color"), fire_color)
u_v3(gpu_uniform(prog, "u_hand_pos"), hand_pos)
u_v3(gpu_uniform(prog, "u_hand_color"), hand_color)
u_v3(gpu_uniform(prog, "u_hand_dir"), hand_dir)
u_f(gpu_uniform(prog, "u_hand_cone"), hand_cone)
u_v3(gpu_uniform(prog, "u_ibl_scale"), day_ibl)
u_v3(gpu_uniform(prog, "u_moon_dir"), day_moon_dir)
u_f(gpu_uniform(prog, "u_moon_phase"), day_moon_phase)
u_f(gpu_uniform(prog, "u_moon_illum"), day_moon_illum)
u_f(gpu_uniform(prog, "u_moon_haze"), day_overcast)
u_f(gpu_uniform(prog, "u_daylight"), day_light)
u_v3(gpu_uniform(prog, "u_fire_pos"), fire_pos)
u_v3(gpu_uniform(prog, "u_fire_color"), fire_color)
}

View file

@ -196,11 +196,4 @@ function m4_xform_point(o: words, m: words, x: int, y: int, z: int) -> int {
return f_add(f_add(f_mul(m[3], x), f_mul(m[7], y)), f_add(f_mul(m[11], z), m[15]))
}
# ---- uniforms ------------------------------------------------------------------
function u_mat4(loc: int, m: words) -> void { gl_uniform_matrix4fv(loc, 1, 0, m) }
function u_f(loc: int, v: int) -> void { let t = gl_scratch(); t[0] = v; gl_uniform1fv(loc, 1, t) }
function u_f2(loc: int, x: int, y: int) -> void { let t = gl_scratch(); t[0] = x; t[1] = y; gl_uniform2fv(loc, 1, t) }
function u_f3(loc: int, x: int, y: int, z: int) -> void { let t = gl_scratch(); t[0] = x; t[1] = y; t[2] = z; gl_uniform3fv(loc, 1, t) }
function u_f4(loc: int, x: int, y: int, z: int, w: int) -> void { let t = gl_scratch(); t[0] = x; t[1] = y; t[2] = z; t[3] = w; gl_uniform4fv(loc, 1, t) }
function u_v3(loc: int, v: words) -> void { gl_uniform3fv(loc, 1, v) }
function u_i(loc: int, v: int) -> void { gl_uniform1i(loc, v) }
# ---- uniforms: see gpu.ludic (gpu_uniform and the u_* setters) ----

View file

@ -0,0 +1,320 @@
# ============================================================================
# gpu.ludic — the seam between the renderer and a graphics API.
#
# render3d was written straight against OpenGL, so there was nowhere to put a second
# API. This file is where that stops: the renderer asks for what it wants here, and
# the backend decides how to say it. OpenGL is the default and the fallback; Vulkan
# is the second backend (docs: maroon-lake docs/plan/37-vulkan.md).
#
# The port is incremental. What has moved behind the seam so far:
# - the choice of backend (R3D_GFX=gl|vk, or gpu_request before r3d_init)
# - the fixed-function render state: depth test/func/write, blending, face
# culling, colour writes, alpha-to-coverage, depth bias, scissor
# - uniforms: looked up by program and name (gpu_uniform), set by the u_* setters
#
# Render state is cached. A pipeline API bakes this state into an object picked by
# key; OpenGL gets the same effect by only telling the driver what changed. The
# cache is only right if NOTHING else in the renderer touches that state, so no
# gl_enable / gl_disable / gl_depth_func / gl_depth_mask / gl_blend_func /
# gl_cull_face / gl_color_mask / gl_scissor / gl_polygon_offset may appear outside
# this file, and neither may any gl_uniform* call.
# ============================================================================
const GPU_GL: int = 1
const GPU_VK: int = 2
var gpu_kind: int = 0 # the backend running; 0 until gpu_select
var gpu_wanted: int = 0 # what was asked for (setting or R3D_GFX)
var gpu_fallback_reason: string = null # why the wanted backend is not the one running
# Ask for a backend before r3d_init ("gl", "opengl", "vk", "vulkan"). The environment
# (R3D_GFX) wins over it, so a test or a take can force one whatever the setting says.
function gpu_request(name: string) -> void { gpu_wanted = gpu_kind_of(name) }
function gpu_kind_of(name: string) -> int {
if name == "vk" or name == "vulkan" { return GPU_VK }
if name == "gl" or name == "opengl" { return GPU_GL }
return GPU_GL
}
function gpu_name(kind: int) -> string {
if kind == GPU_VK { return "vulkan" }
return "opengl"
}
# Settle the backend. Anything that cannot run lands on OpenGL with a reason a
# caller can show the player (gpu_fallback_reason).
function gpu_select() -> int {
if gpu_kind != 0 { return gpu_kind }
if Os.has_env("R3D_GFX") { gpu_wanted = gpu_kind_of(Os.env("R3D_GFX")) }
if gpu_wanted == 0 { gpu_wanted = GPU_GL }
gpu_kind = GPU_GL
if gpu_wanted == GPU_VK {
gpu_fallback_reason = "the Vulkan renderer is not built yet"
print(`r3d: vulkan requested: {gpu_fallback_reason}; using opengl`)
}
return gpu_kind
}
function gpu_backend() -> string { return gpu_name(gpu_kind) }
function gpu_is_gl() -> bool { return gpu_kind != GPU_VK }
# ---- render state ----------------------------------------------------------------
# -1 = not known yet: the first set always reaches the driver, so the cache never
# assumes a default the context might not have.
var gpu_s_depth_test: int = -1
var gpu_s_depth_func: int = -1
var gpu_s_depth_write: int = -1
var gpu_s_blend: int = -1
var gpu_s_blend_src: int = -1
var gpu_s_blend_dst: int = -1
var gpu_s_cull: int = -1
var gpu_s_cull_face: int = -1
var gpu_s_color_write: int = -1
var gpu_s_a2c: int = -1
function gpu_b(on: bool) -> int { if on { return 1 }; return 0 }
# Forget the cache: after anything outside the renderer may have changed GL state
# (a context rebuilt, a foreign library drawing into the frame).
function gpu_state_forget() -> void {
gpu_s_depth_test = -1; gpu_s_depth_func = -1; gpu_s_depth_write = -1
gpu_s_blend = -1; gpu_s_blend_src = -1; gpu_s_blend_dst = -1
gpu_s_cull = -1; gpu_s_cull_face = -1; gpu_s_color_write = -1; gpu_s_a2c = -1
}
function gpu_gl_cap(cap: int, on: int) -> void { if on == 1 { gl_enable(cap) } else { gl_disable(cap) } }
function gpu_depth_test(on: bool) -> void {
let v = gpu_b(on)
if v == gpu_s_depth_test { return }
gpu_s_depth_test = v
gpu_gl_cap(GL_DEPTH_TEST, v)
}
# GL_LESS, GL_LEQUAL, GL_EQUAL, GL_ALWAYS, ... (the comparison names are the same in every API)
function gpu_depth_func(f: int) -> void {
if f == gpu_s_depth_func { return }
gpu_s_depth_func = f
gl_depth_func(f)
}
function gpu_depth_write(on: bool) -> void {
let v = gpu_b(on)
if v == gpu_s_depth_write { return }
gpu_s_depth_write = v
gl_depth_mask(v)
}
function gpu_blend(on: bool) -> void {
let v = gpu_b(on)
if v == gpu_s_blend { return }
gpu_s_blend = v
gpu_gl_cap(GL_BLEND, v)
}
function gpu_blend_func(src: int, dst: int) -> void {
if src == gpu_s_blend_src and dst == gpu_s_blend_dst { return }
gpu_s_blend_src = src; gpu_s_blend_dst = dst
gl_blend_func(src, dst)
}
function gpu_cull(on: bool) -> void {
let v = gpu_b(on)
if v == gpu_s_cull { return }
gpu_s_cull = v
gpu_gl_cap(GL_CULL_FACE, v)
}
# GL_BACK or GL_FRONT
function gpu_cull_face(face: int) -> void {
if face == gpu_s_cull_face { return }
gpu_s_cull_face = face
gl_cull_face(face)
}
# all four channels together: nothing in the renderer writes a partial mask
function gpu_color_write(on: bool) -> void {
let v = gpu_b(on)
if v == gpu_s_color_write { return }
gpu_s_color_write = v
gl_color_mask(v, v, v, v)
}
function gpu_alpha_to_coverage(on: bool) -> void {
let v = gpu_b(on)
if v == gpu_s_a2c { return }
gpu_s_a2c = v
gpu_gl_cap(GL_SAMPLE_ALPHA_TO_COVERAGE, v)
}
# Depth bias for the shadow casters; (0, 0) turns it off. factor/units are fixed, as
# gl_polygon_offset takes them. A pipeline API bakes the bias into the pipeline.
var gpu_s_bias: int = -1
function gpu_depth_bias(factor: fixed, units: fixed) -> void {
var v = 1
if factor == 0.0 and units == 0.0 { v = 0 }
if v != gpu_s_bias { gpu_s_bias = v; gpu_gl_cap(GL_POLYGON_OFFSET_FILL, v) }
if v == 1 { gl_polygon_offset(factor, units) }
}
# A scissor rectangle in top-down pixels (x, y from the top-left of the drawable), or
# off. Every API but OpenGL counts rows from the top; the GL backend flips it.
var gpu_s_scissor: int = -1
function gpu_scissor(x: int, y_top: int, w: int, h: int) -> void {
if gpu_s_scissor != 1 { gpu_s_scissor = 1; gl_enable(GL_SCISSOR_TEST) }
gl_scissor(x, gl_h - y_top - h, w, h)
}
function gpu_scissor_off() -> void {
if gpu_s_scissor == 0 { return }
gpu_s_scissor = 0
gl_disable(GL_SCISSOR_TEST)
}
# ---- uniforms --------------------------------------------------------------------
# A uniform is found by its program and its name, and set through a handle. On OpenGL
# the handle is the location. On a pipeline API it will name a slot in the program's
# uniform block, so the u_* setters below are the only code that knows which. Arrays
# are looked up by their first element ("u_bones[0]"), as every GL driver accepts.
function gpu_uniform(prog: int, name: string) -> int { return gl_get_uniform_location(prog, name) }
var gpu_u_tmp: words = null
function gpu_tmp() -> words { if gpu_u_tmp == null { gpu_u_tmp = words(4) }; return gpu_u_tmp }
# float bits (IEEE singles in an int), like every other number in the renderer
function u_f(loc: int, v: int) -> void { let t = gpu_tmp(); t[0] = v; gl_uniform1fv(loc, 1, t) }
function u_f2(loc: int, x: int, y: int) -> void { let t = gpu_tmp(); t[0] = x; t[1] = y; gl_uniform2fv(loc, 1, t) }
function u_f3(loc: int, x: int, y: int, z: int) -> void { let t = gpu_tmp(); t[0] = x; t[1] = y; t[2] = z; gl_uniform3fv(loc, 1, t) }
function u_f4(loc: int, x: int, y: int, z: int, w: int) -> void { let t = gpu_tmp(); t[0] = x; t[1] = y; t[2] = z; t[3] = w; gl_uniform4fv(loc, 1, t) }
function u_v3(loc: int, v: words) -> void { gl_uniform3fv(loc, 1, v) }
function u_fv(loc: int, n: int, v: words) -> void { gl_uniform1fv(loc, n, v) }
function u_mat4(loc: int, m: words) -> void { gl_uniform_matrix4fv(loc, 1, 0, m) }
function u_mat4n(loc: int, n: int, m: words) -> void { gl_uniform_matrix4fv(loc, n, 0, m) }
function u_i(loc: int, v: int) -> void { gl_uniform1i(loc, v) }
# ---- what this machine can do ----------------------------------------------------
# The advanced graphics features are Windows features: the Vulkan renderer, ray tracing,
# DLSS, Reflex, HDR output, mesh-shader ground cover. gpu_caps_probe() asks Vulkan what
# the GPU offers - on Windows only - and a game's settings screen greys out whatever this
# machine cannot use, with the most specific reason. Detected every start, never saved:
# a settings file carried to another machine must not carry a stale "supported".
const GF_VULKAN: int = 0
const GF_RT_SHADOWS: int = 1
const GF_RT_REFLECTIONS: int = 2
const GF_RT_AO: int = 3
const GF_DLSS: int = 4
const GF_DLSS_RR: int = 5
const GF_DLSS_FG: int = 6
const GF_DLSS5: int = 7
const GF_REFLEX: int = 8
const GF_HDR: int = 9
const GF_MESH_GRASS: int = 10
const GF_COUNT: int = 11
var gpu_cap_probed: bool = false
var gpu_cap_windows: bool = false # the advanced features exist on this platform at all
var gpu_cap_vulkan: bool = false # a Vulkan loader, an instance and a device
var gpu_cap_floor: bool = false # Vulkan 1.3 with everything the renderer's floor needs
var gpu_cap_rt: bool = false # ray query + acceleration structures
var gpu_cap_mesh: bool = false # VK_EXT_mesh_shader
var gpu_cap_nvidia: bool = false
var gpu_cap_rtx: int = 0 # the RTX generation (20, 30, 40, 50); 0 = not RTX
var gpu_cap_reflex: bool = false # VK_NV_low_latency2
var gpu_cap_hdr: bool = false # the instance offers HDR colour spaces
var gpu_cap_device: string = ""
# 20 for "NVIDIA GeForce RTX 2080", 50 for "RTX 5090"; 30 for a workstation "RTX A4000"
function gpu_rtx_generation(name: string) -> int {
let at = Text.index_of(name, "RTX ")
if at < 0 { return 0 }
let p: pointer = name
let c = p[at + 4]
if c >= '0' and c <= '9' { return (c - '0') * 10 }
return 30
}
function gpu_ext_in(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
}
# R3D_CAPS=rtx50|rtx40|rtx30|amd|intel|none pretends to be a Windows machine with that GPU,
# so the settings screen can be shot and tested anywhere
function gpu_caps_fake(kind: string) -> void {
gpu_cap_windows = true
if kind == "none" { return }
gpu_cap_vulkan = true; gpu_cap_floor = true; gpu_cap_hdr = true
if kind == "amd" or kind == "intel" { gpu_cap_rt = true; gpu_cap_mesh = true; gpu_cap_device = `test {kind} GPU`; return }
gpu_cap_nvidia = true; gpu_cap_rt = true; gpu_cap_mesh = true; gpu_cap_reflex = true
gpu_cap_rtx = gpu_rtx_generation(`RTX {kind[3 .. 5]}`)
gpu_cap_device = `test NVIDIA GeForce RTX {kind[3 .. 5]}`
}
function gpu_caps_probe() -> void {
if gpu_cap_probed { return }
gpu_cap_probed = true
if Os.has_env("R3D_CAPS") { gpu_caps_fake(Os.env("R3D_CAPS")); return }
gpu_cap_windows = Os.platform() == "windows"
if not gpu_cap_windows { return }
if Vk.open() == 0 { return }
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)
gpu_cap_hdr = gpu_ext_in(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_i32(app, VkApplicationInfo_apiVersion, (1 << 22) | (3 << 12))
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)
let out = bytes(8)
if Vk.create_instance(ici, null, out) != VK_SUCCESS { return }
let inst = Vk.get_ptr(out, 0)
Vk.put_i32(cnt, 0, 0)
Vk.enumerate_physical_devices(inst, cnt, null)
let nd = Vk.get_i32(cnt, 0)
let devs = bytes(nd * 8 + 8)
Vk.enumerate_physical_devices(inst, cnt, devs)
let props = bytes(VkPhysicalDeviceProperties_sizeof)
# the renderer runs on the first discrete GPU, else the first one listed
var pick = -1
for d in 0 .. nd {
Vk.get_physical_device_properties(Vk.get_ptr(devs, d * 8), props)
if pick < 0 and Vk.get_i32(props, VkPhysicalDeviceProperties_deviceType) == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU { pick = d }
}
if pick < 0 and nd > 0 { pick = 0 }
if pick >= 0 {
let pd = Vk.get_ptr(devs, pick * 8)
gpu_cap_vulkan = true
Vk.get_physical_device_properties(pd, props)
gpu_cap_device = string(Vk.at(props, VkPhysicalDeviceProperties_deviceName))
gpu_cap_nvidia = Vk.get_i32(props, VkPhysicalDeviceProperties_vendorID) == 4318
if gpu_cap_nvidia { gpu_cap_rtx = gpu_rtx_generation(gpu_cap_device) }
let api = Vk.get_i32(props, VkPhysicalDeviceProperties_apiVersion)
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 major = (api >> 22) & 127
let minor = (api >> 12) & 1023
gpu_cap_floor = (major > 1 or (major == 1 and minor >= 3)) and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_dynamicRendering) == 1 and Vk.get_i32(f13, VkPhysicalDeviceVulkan13Features_synchronization2) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_descriptorIndexing) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_bufferDeviceAddress) == 1 and Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_timelineSemaphore) == 1
Vk.put_i32(cnt, 0, 0)
Vk.enumerate_device_extension_properties(pd, null, cnt, null)
let ne = Vk.get_i32(cnt, 0)
let dexts = bytes(ne * VkExtensionProperties_sizeof + 8)
Vk.enumerate_device_extension_properties(pd, null, cnt, dexts)
gpu_cap_rt = gpu_ext_in(dexts, ne, VK_KHR_RAY_QUERY_EXTENSION_NAME) and gpu_ext_in(dexts, ne, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME)
gpu_cap_mesh = gpu_ext_in(dexts, ne, VK_EXT_MESH_SHADER_EXTENSION_NAME)
gpu_cap_reflex = gpu_ext_in(dexts, ne, VK_NV_LOW_LATENCY_2_EXTENSION_NAME)
}
Vk.destroy_instance(inst, null)
print(`r3d: gpu caps: {gpu_cap_device} vulkan {gpu_cap_vulkan} floor {gpu_cap_floor} rt {gpu_cap_rt} mesh {gpu_cap_mesh} rtx {gpu_cap_rtx} reflex {gpu_cap_reflex} hdr {gpu_cap_hdr}`)
}
# Whether the renderer actually draws a feature yet. The Vulkan renderer is being built;
# until a feature lands, choosing it is saved and shown, and says it takes effect later.
function gpu_feature_implemented(f: int) -> bool { return false }

View file

@ -84,7 +84,7 @@ function grass_count_at(d: int) -> int {
function grass_tiles(size: int, d_min: int, d_max: int) -> void {
let p = grass_prog
let cells = size / GRASS_CELL
gl_uniform1i(gl_uniform(p, "u_tile_cells"), cells)
u_i(gpu_uniform(p, "u_tile_cells"), cells)
let sz = fi(size)
let half = f_mul(sz, F_HALF)
let reach = f_add(d_max, f_mul(half, fl(1.5)))
@ -108,8 +108,8 @@ function grass_tiles(size: int, d_min: int, d_max: int) -> void {
if cam_sphere_visible(cx, cy, cz, f_add(corner_r, fi(6))) {
let per = grass_count_at(dnear)
if per > 0 {
u_f2(gl_uniform(p, "u_tile"), ox, oz)
gl_uniform1i(gl_uniform(p, "u_per_cell"), per)
u_f2(gpu_uniform(p, "u_tile"), ox, oz)
u_i(gpu_uniform(p, "u_per_cell"), per)
mesh_draw_instanced(grass_mesh, per * cells * cells)
grass_draws += 1
}
@ -124,42 +124,42 @@ function grass_draw() -> void {
if not grass_on or ter_reflect or grass_prog == 0 { return }
let p = grass_prog
gl_use_program(p)
u_mat4(gl_uniform(p, "u_view"), cam_view)
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
u_mat4(gl_uniform(p, "u_vp"), cam_vp_clean)
u_f(gl_uniform(p, "u_wind"), grass_wind)
u_f(gl_uniform(p, "u_rough_scale"), F_ONE)
u_v3(gl_uniform(p, "u_tint"), sc_blade_tint)
u_v3(gl_uniform(p, "u_blade_base"), sc_blade_base)
u_v3(gl_uniform(p, "u_blade_tip"), sc_blade_tip)
u_f(gl_uniform(p, "u_cull"), grass_radius)
u_f(gl_uniform(p, "u_model_h"), F_ZERO)
u_f(gl_uniform(p, "u_s0"), grass_s0)
u_f(gl_uniform(p, "u_d0"), grass_d0)
u_f(gl_uniform(p, "u_radius"), grass_radius)
gl_uniform1i(gl_uniform(p, "u_dbg"), grass_dbg)
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_mat4(gpu_uniform(p, "u_vp"), cam_vp_clean)
u_f(gpu_uniform(p, "u_wind"), grass_wind)
u_f(gpu_uniform(p, "u_rough_scale"), F_ONE)
u_v3(gpu_uniform(p, "u_tint"), sc_blade_tint)
u_v3(gpu_uniform(p, "u_blade_base"), sc_blade_base)
u_v3(gpu_uniform(p, "u_blade_tip"), sc_blade_tip)
u_f(gpu_uniform(p, "u_cull"), grass_radius)
u_f(gpu_uniform(p, "u_model_h"), F_ZERO)
u_f(gpu_uniform(p, "u_s0"), grass_s0)
u_f(gpu_uniform(p, "u_d0"), grass_d0)
u_f(gpu_uniform(p, "u_radius"), grass_radius)
u_i(gpu_uniform(p, "u_dbg"), grass_dbg)
var orthotex = ter_ortho_tex
var oon = F_ONE
if orthotex == 0 { orthotex = ter_height_tex; oon = F_ZERO }
r3d_bind_2d(p, "u_ortho", 4, orthotex)
u_f(gl_uniform(p, "u_ortho_on"), oon)
u_f(gpu_uniform(p, "u_ortho_on"), oon)
var lake = fl(-100000.0)
if ter_lake_ex != 0 { lake = ter_lake_level }
u_f(gl_uniform(p, "u_lake_level"), lake)
u_f(gpu_uniform(p, "u_lake_level"), lake)
var sea = lake
if ter_sea_set { sea = ter_sea_level }
u_f(gl_uniform(p, "u_sea_level"), sea)
u_f4(gl_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_f(gl_uniform(p, "u_snow_line"), ter_snow_line)
u_f(gpu_uniform(p, "u_sea_level"), sea)
u_f4(gpu_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_f(gpu_uniform(p, "u_snow_line"), ter_snow_line)
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
u_f(gl_uniform(p, "u_spec_scale"), fl(0.15))
gl_disable(GL_CULL_FACE)
u_f(gpu_uniform(p, "u_spec_scale"), fl(0.15))
gpu_cull(false)
grass_draws = 0
gl_bind_vertex_array(grass_mesh.vao)
grass_tiles(16, F_ZERO, fi(300))
grass_tiles(64, fi(300), fi(1200))
grass_tiles(256, fi(1200), grass_radius)
gl_enable(GL_CULL_FACE)
gpu_cull(true)
}

View file

@ -159,12 +159,12 @@ function ov_begin() -> void {
if not ov_ready { return }
gl_bind_framebuffer(GL_FRAMEBUFFER, gl_screen_fbo())
gl_viewport(0, 0, gl_w, gl_h)
gl_disable(GL_DEPTH_TEST)
gl_disable(GL_CULL_FACE)
gl_enable(GL_BLEND)
gl_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
gpu_depth_test(false)
gpu_cull(false)
gpu_blend(true)
gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
gl_use_program(ov_prog)
u_f2(gl_uniform(ov_prog, "u_screen"), fi(gl_w), fi(gl_h))
u_f2(gpu_uniform(ov_prog, "u_screen"), fi(gl_w), fi(gl_h))
ov_n = 0; ov_nr = 0; ov_range_start = 0
ov_tex = ov_white
ov_clip_w = 0
@ -204,24 +204,24 @@ function ov_flush() -> void {
r3d_bind_2d(ov_prog, "u_tex", 0, t)
var is_font = F_ZERO
if t == ov_font { is_font = F_ONE }
u_f(gl_uniform(ov_prog, "u_is_font"), is_font)
u_f(gpu_uniform(ov_prog, "u_is_font"), is_font)
last = t
}
if ov_ranges[o + 5] > 0 {
if not clipped { gl_enable(GL_SCISSOR_TEST); clipped = true }
gl_scissor(ov_ranges[o + 3], gl_h - ov_ranges[o + 4] - ov_ranges[o + 6], ov_ranges[o + 5], ov_ranges[o + 6])
} else if clipped { gl_disable(GL_SCISSOR_TEST); clipped = false }
clipped = true
gpu_scissor(ov_ranges[o + 3], ov_ranges[o + 4], ov_ranges[o + 5], ov_ranges[o + 6])
} else if clipped { gpu_scissor_off(); clipped = false }
gl_draw_arrays(GL_TRIANGLES, ov_ranges[o + 1] * 6, ov_ranges[o + 2] * 6)
}
if clipped { gl_disable(GL_SCISSOR_TEST) }
if clipped { gpu_scissor_off() }
gl_bind_vertex_array(0)
ov_n = 0; ov_nr = 0; ov_range_start = 0
}
function ov_end() -> void {
if not ov_open { return }
ov_flush()
gl_disable(GL_BLEND)
gl_enable(GL_DEPTH_TEST)
gpu_blend(false)
gpu_depth_test(true)
ov_open = false
}
function ov_use_tex(t: int) -> void {

View file

@ -144,17 +144,17 @@ function post_measure() -> void {
if post_p_adapt == 0 { post_p_adapt = r3d_program("fullscreen.vert", "adapt.frag", "") }
let next = 1 - post_adapt_i
target_bind(post_adapt_t[next])
gl_disable(GL_DEPTH_TEST)
gpu_depth_test(false)
gl_use_program(post_p_adapt)
r3d_bind_2d(post_p_adapt, "u_scene", 0, post_hdr.color)
r3d_bind_2d(post_p_adapt, "u_prev", 1, post_adapt_t[post_adapt_i].color)
u_f(gl_uniform(post_p_adapt, "u_lod"), fi(post_mips - 1))
u_f(gl_uniform(post_p_adapt, "u_key"), post_key)
u_f(gl_uniform(post_p_adapt, "u_max"), post_exposure_max)
u_f(gl_uniform(post_p_adapt, "u_rate"), fl(0.08))
u_f(gpu_uniform(post_p_adapt, "u_lod"), fi(post_mips - 1))
u_f(gpu_uniform(post_p_adapt, "u_key"), post_key)
u_f(gpu_uniform(post_p_adapt, "u_max"), post_exposure_max)
u_f(gpu_uniform(post_p_adapt, "u_rate"), fl(0.08))
var reset = F_ZERO
if post_adapt_reset { reset = F_ONE; post_adapt_reset = false }
u_f(gl_uniform(post_p_adapt, "u_reset"), reset)
u_f(gpu_uniform(post_p_adapt, "u_reset"), reset)
mesh_draw(post_fs)
post_adapt_i = next
gl_bind_texture(GL_TEXTURE_2D, post_hdr.color)
@ -164,11 +164,11 @@ function post_measure() -> void {
function post_begin_scene() -> void {
target_bind(post_hdr)
if post_ms_fbo != 0 { gl_bind_framebuffer(GL_FRAMEBUFFER, post_ms_fbo); gl_enable(GL_MULTISAMPLE) }
gl_enable(GL_DEPTH_TEST)
gl_depth_func(GL_LESS)
gl_depth_mask(1)
gl_enable(GL_CULL_FACE)
gl_cull_face(GL_BACK)
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
gpu_depth_write(true)
gpu_cull(true)
gpu_cull_face(GL_BACK)
gl_clear_color(0.0, 0.0, 0.0, 1.0)
gl_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
}
@ -217,30 +217,30 @@ function post_capture_prev() -> void {
}
function post_ssao_pass() -> void {
gl_disable(GL_DEPTH_TEST)
gl_disable(GL_BLEND)
gpu_depth_test(false)
gpu_blend(false)
target_bind(post_ao)
gl_use_program(post_p_ao)
r3d_bind_2d(post_p_ao, "u_depth", 0, post_hdr.depth)
r3d_bind_2d(post_p_ao, "u_prev_color", 1, post_prev.color)
u_f(gl_uniform(post_p_ao, "u_frame"), fi(post_frame % 64))
u_mat4(gl_uniform(post_p_ao, "u_inv_proj"), cam_inv_proj)
u_mat4(gl_uniform(post_p_ao, "u_proj"), cam_proj)
u_f2(gl_uniform(post_p_ao, "u_texel"), fr(1, post_w), fr(1, post_h))
u_f(gl_uniform(post_p_ao, "u_radius"), post_ao_radius)
u_f(gl_uniform(post_p_ao, "u_intensity"), post_ao_intensity)
u_f(gpu_uniform(post_p_ao, "u_frame"), fi(post_frame % 64))
u_mat4(gpu_uniform(post_p_ao, "u_inv_proj"), cam_inv_proj)
u_mat4(gpu_uniform(post_p_ao, "u_proj"), cam_proj)
u_f2(gpu_uniform(post_p_ao, "u_texel"), fr(1, post_w), fr(1, post_h))
u_f(gpu_uniform(post_p_ao, "u_radius"), post_ao_radius)
u_f(gpu_uniform(post_p_ao, "u_intensity"), post_ao_intensity)
mesh_draw(post_fs)
target_bind(post_ao_blur)
gl_use_program(post_p_ao_blur)
r3d_bind_2d(post_p_ao_blur, "u_ao", 0, post_ao.color)
r3d_bind_2d(post_p_ao_blur, "u_depth", 1, post_hdr.depth)
u_f2(gl_uniform(post_p_ao_blur, "u_texel"), fr(1, post_ao.w), fr(1, post_ao.h))
u_f2(gpu_uniform(post_p_ao_blur, "u_texel"), fr(1, post_ao.w), fr(1, post_ao.h))
mesh_draw(post_fs)
}
function post_bloom_pass() -> void {
gl_disable(GL_DEPTH_TEST)
gl_disable(GL_BLEND)
gpu_depth_test(false)
gpu_blend(false)
var src = post_color
var sw = post_w; var sh = post_h
gl_use_program(post_p_down)
@ -248,60 +248,60 @@ function post_bloom_pass() -> void {
let t = post_bloom[i]
target_bind(t)
r3d_bind_2d(post_p_down, "u_src", 0, src)
u_f2(gl_uniform(post_p_down, "u_texel"), fr(1, sw), fr(1, sh))
u_f2(gpu_uniform(post_p_down, "u_texel"), fr(1, sw), fr(1, sh))
var th = f_neg1()
if i == 0 { th = fl(1.2) }
u_f(gl_uniform(post_p_down, "u_threshold"), th)
u_f(gpu_uniform(post_p_down, "u_threshold"), th)
mesh_draw(post_fs)
src = t.color; sw = t.w; sh = t.h
}
gl_use_program(post_p_up)
gl_enable(GL_BLEND)
gl_blend_func(GL_ONE, GL_ONE)
gpu_blend(true)
gpu_blend_func(GL_ONE, GL_ONE)
var i = BLOOM_LEVELS - 1
while i > 0 {
let from = post_bloom[i]
let to = post_bloom[i - 1]
target_bind(to)
r3d_bind_2d(post_p_up, "u_src", 0, from.color)
u_f2(gl_uniform(post_p_up, "u_texel"), fr(1, from.w), fr(1, from.h))
u_f(gl_uniform(post_p_up, "u_radius"), F_ONE)
u_f2(gpu_uniform(post_p_up, "u_texel"), fr(1, from.w), fr(1, from.h))
u_f(gpu_uniform(post_p_up, "u_radius"), F_ONE)
mesh_draw(post_fs)
i -= 1
}
gl_disable(GL_BLEND)
gpu_blend(false)
}
function post_tonemap(color_tex: int) -> void {
if post_auto { post_measure() }
target_bind(post_ldr)
gl_disable(GL_DEPTH_TEST)
gpu_depth_test(false)
gl_use_program(post_p_tone)
r3d_bind_2d(post_p_tone, "u_hdr", 0, color_tex)
r3d_bind_2d(post_p_tone, "u_bloom", 1, post_bloom[0].color)
r3d_bind_2d(post_p_tone, "u_ao", 2, post_ao_blur.color)
u_f(gl_uniform(post_p_tone, "u_ao_strength"), post_ao_strength)
u_f(gl_uniform(post_p_tone, "u_gi_strength"), post_gi_strength)
u_f(gl_uniform(post_p_tone, "u_exposure"), post_exposure)
u_f(gpu_uniform(post_p_tone, "u_ao_strength"), post_ao_strength)
u_f(gpu_uniform(post_p_tone, "u_gi_strength"), post_gi_strength)
u_f(gpu_uniform(post_p_tone, "u_exposure"), post_exposure)
var auto = F_ZERO
if post_auto and post_adapt_t != null { auto = F_ONE; r3d_bind_2d(post_p_tone, "u_adapt", 3, post_adapt_t[post_adapt_i].color) }
u_f(gl_uniform(post_p_tone, "u_auto"), auto)
u_f(gl_uniform(post_p_tone, "u_bloom_strength"), post_bloom_strength)
u_f(gl_uniform(post_p_tone, "u_vignette"), post_vignette)
u_f(gl_uniform(post_p_tone, "u_saturation"), post_saturation)
u_f(gl_uniform(post_p_tone, "u_contrast"), post_contrast)
u_f3(gl_uniform(post_p_tone, "u_wb"), fl(1.02), F_ONE, fl(0.97))
u_f3(gl_uniform(post_p_tone, "u_lift"), fl(0.004), fl(0.004), fl(0.012))
u_f3(gl_uniform(post_p_tone, "u_gain"), fl(0.99), fl(0.995), fl(1.0))
u_f(gpu_uniform(post_p_tone, "u_auto"), auto)
u_f(gpu_uniform(post_p_tone, "u_bloom_strength"), post_bloom_strength)
u_f(gpu_uniform(post_p_tone, "u_vignette"), post_vignette)
u_f(gpu_uniform(post_p_tone, "u_saturation"), post_saturation)
u_f(gpu_uniform(post_p_tone, "u_contrast"), post_contrast)
u_f3(gpu_uniform(post_p_tone, "u_wb"), fl(1.02), F_ONE, fl(0.97))
u_f3(gpu_uniform(post_p_tone, "u_lift"), fl(0.004), fl(0.004), fl(0.012))
u_f3(gpu_uniform(post_p_tone, "u_gain"), fl(0.99), fl(0.995), fl(1.0))
mesh_draw(post_fs)
# sharpen + grain onto the screen
gl_bind_framebuffer(GL_FRAMEBUFFER, gl_screen)
gl_viewport(0, 0, gl_w, gl_h)
gl_use_program(post_p_sharp)
r3d_bind_2d(post_p_sharp, "u_src", 0, post_ldr.color)
u_f2(gl_uniform(post_p_sharp, "u_texel"), fr(1, post_w), fr(1, post_h))
u_f(gl_uniform(post_p_sharp, "u_amount"), post_sharpen)
u_f(gl_uniform(post_p_sharp, "u_grain"), post_grain)
u_f(gl_uniform(post_p_sharp, "u_time"), r3d_time)
u_f2(gpu_uniform(post_p_sharp, "u_texel"), fr(1, post_w), fr(1, post_h))
u_f(gpu_uniform(post_p_sharp, "u_amount"), post_sharpen)
u_f(gpu_uniform(post_p_sharp, "u_grain"), post_grain)
u_f(gpu_uniform(post_p_sharp, "u_time"), r3d_time)
mesh_draw(post_fs)
}

View file

@ -75,7 +75,7 @@ function r3d_program(vs: string, fs: string, defines: string) -> int {
function r3d_bind_tex(prog: int, name: string, unit: int, target: int, tex: int) -> void {
gl_active_texture(GL_TEXTURE0 + unit)
gl_bind_texture(target, tex)
gl_uniform1i(gl_get_uniform_location(prog, name), unit)
u_i(gpu_uniform(prog, name), unit)
}
function r3d_bind_2d(prog: int, name: string, unit: int, tex: int) -> void { r3d_bind_tex(prog, name, unit, GL_TEXTURE_2D, tex) }

View file

@ -3,6 +3,7 @@
# Import this one file; the game supplies scene_draw() / scene_draw_casters().
# ============================================================================
import "fmath.ludic"
import "gpu.ludic"
import "prof.ludic"
import "programs.ludic"
import "texture.ludic"

View file

@ -33,15 +33,15 @@ function r3d_prepass_off() -> bool {
return r3d_prepass_env == 1
}
function fog_bind(prog: int) -> void {
u_f(gl_uniform(prog, "u_clip_y"), r3d_clip_y)
u_f(gl_uniform(prog, "u_spec_scale"), F_ONE)
u_f(gl_uniform(prog, "u_fog_density"), r3d_fog_density)
u_f(gl_uniform(prog, "u_fog_height_falloff"), r3d_fog_falloff)
u_f(gl_uniform(prog, "u_fog_base"), r3d_fog_base)
u_f(gpu_uniform(prog, "u_clip_y"), r3d_clip_y)
u_f(gpu_uniform(prog, "u_spec_scale"), F_ONE)
u_f(gpu_uniform(prog, "u_fog_density"), r3d_fog_density)
u_f(gpu_uniform(prog, "u_fog_height_falloff"), r3d_fog_falloff)
u_f(gpu_uniform(prog, "u_fog_base"), r3d_fog_base)
var cs = r3d_cloud_shadow
if Os.has_env("R3D_NOCLOUD") { cs = F_ZERO }
u_f(gl_uniform(prog, "u_cloud_shadow"), cs)
u_f(gl_uniform(prog, "u_time"), r3d_time)
u_f(gpu_uniform(prog, "u_cloud_shadow"), cs)
u_f(gpu_uniform(prog, "u_time"), r3d_time)
}
# profiling switches (environment): R3D_NOSHADOW R3D_NOGI R3D_MSAA=n R3D_NOBLADES R3D_NOCARDS R3D_NOTREES R3D_NEAR=m
@ -72,6 +72,7 @@ function r3d_env_flags() -> void {
}
function r3d_init(w: int, h: int, title: string) -> bool {
r3d_env_flags()
gpu_select()
if not gl_open(w, h, title) { print("r3d: no OpenGL context"); return false }
if Os.has_env("R3D_NOVSYNC") { gl_vsync(0) }
var renderer: string = gl_get_string(GL_RENDERER)
@ -97,21 +98,21 @@ function r3d_init(w: int, h: int, title: string) -> bool {
}
function r3d_draw_sky() -> void {
gl_depth_func(GL_LEQUAL)
gl_depth_mask(0)
gl_disable(GL_CULL_FACE)
gpu_depth_func(GL_LEQUAL)
gpu_depth_write(false)
gpu_cull(false)
let p = r3d_sky_prog
gl_use_program(p)
r3d_bind_2d(p, "u_sky", 0, sky_tex)
sky_bind_rot(p)
sky_bind_lighting(p)
u_mat4(gl_uniform(p, "u_inv_vp"), cam_inv_vp)
u_f(gl_uniform(p, "u_sky_gain"), fl(0.95))
u_f(gl_uniform(p, "u_sky_sat"), fl(1.35))
u_f(gl_uniform(p, "u_time"), r3d_time)
u_mat4(gpu_uniform(p, "u_inv_vp"), cam_inv_vp)
u_f(gpu_uniform(p, "u_sky_gain"), fl(0.95))
u_f(gpu_uniform(p, "u_sky_sat"), fl(1.35))
u_f(gpu_uniform(p, "u_time"), r3d_time)
mesh_draw(sky_fullscreen)
gl_depth_mask(1)
gl_depth_func(GL_LESS)
gpu_depth_write(true)
gpu_depth_func(GL_LESS)
}
# the drawable changed size: the camera's aspect and every screen-sized target follow
@ -171,9 +172,9 @@ function r3d_frame(time: int) -> void {
# target, and depth drawn after it lands in the sun buffer, not the scene's.
if not r3d_prepass_off() {
prof_begin("foliage prepass")
gl_color_mask(0, 0, 0, 0)
gpu_color_write(false)
scatter_draw_depth()
gl_color_mask(1, 1, 1, 1)
gpu_color_write(true)
prof_end()
sc_prepass = true
}
@ -206,8 +207,8 @@ function r3d_frame(time: int) -> void {
prof_begin("water surface")
post_capture_scene()
target_bind(post_hdr)
gl_enable(GL_DEPTH_TEST)
gl_depth_func(GL_LESS)
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
water_draw(post_depth_copy.depth)
prof_end()
}

View file

@ -464,10 +464,10 @@ function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor {
gl_viewport(0, 0, aw, th)
gl_clear_color(0.0, 0.0, 0.0, 0.0)
gl_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
gl_enable(GL_DEPTH_TEST)
gl_depth_func(GL_LESS)
gl_disable(GL_CULL_FACE)
gl_disable(GL_BLEND)
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
gpu_cull(false)
gpu_blend(false)
# the model's prims temporarily take the identity instance
for i in 0 .. len(model.prims) { scatter_attach(model.prims[i].mesh.vao, sc_ident_buf) }
let view = m4_new(); let proj = m4_new()
@ -485,9 +485,9 @@ function impostor_bake(model: Model, tiles: int, tw: int, th: int) -> Impostor {
v3_set(eye, f_mul(f_sin(a), f_mul(r, fi(4))), f_add(cy, f_mul(r, fl(0.5))), f_neg(f_mul(f_cos(a), f_mul(r, fi(4)))))
m4_look_at(view, eye, at, up)
m4_ortho(proj, f_neg(r), r, f_neg(hh), hh, fl(0.1), f_mul(r, fi(9)))
u_mat4(gl_uniform(bake, "u_view"), view)
u_mat4(gl_uniform(bake, "u_proj"), proj)
u_f(gl_uniform(bake, "u_wind"), F_ZERO)
u_mat4(gpu_uniform(bake, "u_view"), view)
u_mat4(gpu_uniform(bake, "u_proj"), proj)
u_f(gpu_uniform(bake, "u_wind"), F_ZERO)
gl_viewport(t * tw, 0, tw, th)
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
@ -816,45 +816,45 @@ function layer_draw_model(l: Layer, model: Model, vb: int, cnt: int, card: bool,
gl_use_program(p)
# over the prepass: only the fragment the prepass kept, at exactly its depth (a texel
# it cut would otherwise pass LEQUAL over the terrain behind and draw the quad solid)
if p == sc_prog_fol_eq { gl_depth_func(GL_EQUAL) }
if p == sc_prog_fol_eq { gpu_depth_func(GL_EQUAL) }
var ground = F_ZERO
if l.grounded { ground = F_ONE }
u_f(gl_uniform(p, "u_ground"), ground)
u_f(gpu_uniform(p, "u_ground"), ground)
if l.grounded { terrain_bind_height(p) }
u_f(gl_uniform(p, "u_time"), r3d_time)
u_f(gl_uniform(p, "u_wind"), l.wind)
u_f(gpu_uniform(p, "u_time"), r3d_time)
u_f(gpu_uniform(p, "u_wind"), l.wind)
var mh = F_ZERO
if not card and l.foliage { mh = model.height }
u_f(gl_uniform(p, "u_model_h"), mh)
if not card and l.foliage and not shadow and sc_a2c { gl_enable(GL_SAMPLE_ALPHA_TO_COVERAGE) }
u_f(gpu_uniform(p, "u_model_h"), mh)
if not card and l.foliage and not shadow and sc_a2c { gpu_alpha_to_coverage(true) }
if card {
u_f(gl_uniform(p, "u_card_w"), f_mul(l.atlas.radius, F_TWO)); u_f(gl_uniform(p, "u_card_h"), l.atlas.height)
u_f(gpu_uniform(p, "u_card_w"), f_mul(l.atlas.radius, F_TWO)); u_f(gpu_uniform(p, "u_card_h"), l.atlas.height)
r3d_bind_2d(p, "u_diff", 0, l.atlas.albedo)
r3d_bind_2d(p, "u_nrm", 1, l.atlas.normal)
gl_enable(GL_SAMPLE_ALPHA_TO_COVERAGE)
gpu_alpha_to_coverage(true)
}
if shadow { u_mat4(gl_uniform(p, "u_light_vp"), light_vp) }
if shadow { u_mat4(gpu_uniform(p, "u_light_vp"), light_vp) }
else {
u_mat4(gl_uniform(p, "u_view"), cam_view)
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
u_v3(gl_uniform(p, "u_tint"), l.tint)
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_v3(gpu_uniform(p, "u_tint"), l.tint)
if sc_dbg_lod and sc_dbg_level >= 0 {
if sc_dbg_tint == null { sc_dbg_tint = v3_new(F_ONE, F_ONE, F_ONE) }
let k = sc_dbg_level
var r = F_ZERO; var g = F_ZERO; var b = F_ZERO
if k == 0 { r = fi(3) } else if k == 1 { g = fi(3) } else if k == 2 { b = fi(3) } else { r = fi(3); g = fi(3) }
v3_set(sc_dbg_tint, r, g, b)
u_v3(gl_uniform(p, "u_tint"), sc_dbg_tint)
u_v3(gpu_uniform(p, "u_tint"), sc_dbg_tint)
}
u_f(gl_uniform(p, "u_rough_scale"), l.rough)
if l.blade { u_v3(gl_uniform(p, "u_blade_base"), sc_blade_base); u_v3(gl_uniform(p, "u_blade_tip"), sc_blade_tip) }
u_f(gl_uniform(p, "u_cull"), l.cull)
u_f(gpu_uniform(p, "u_rough_scale"), l.rough)
if l.blade { u_v3(gpu_uniform(p, "u_blade_base"), sc_blade_base); u_v3(gpu_uniform(p, "u_blade_tip"), sc_blade_tip) }
u_f(gpu_uniform(p, "u_cull"), l.cull)
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
if l.foliage { u_f(gl_uniform(p, "u_spec_scale"), fl(0.05)) }
if l.foliage { u_f(gpu_uniform(p, "u_spec_scale"), fl(0.05)) }
}
gl_disable(GL_CULL_FACE)
gpu_cull(false)
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
scatter_attach(pr.mesh.vao, vb)
@ -864,8 +864,8 @@ function layer_draw_model(l: Layer, model: Model, vb: int, cnt: int, card: bool,
}
mesh_draw_instanced(pr.mesh, cnt)
}
gl_disable(GL_SAMPLE_ALPHA_TO_COVERAGE)
if p == sc_prog_fol_eq { gl_depth_func(GL_LESS) }
gpu_alpha_to_coverage(false)
if p == sc_prog_fol_eq { gpu_depth_func(GL_LESS) }
}
function layer_draw_far(l: Layer, shadow: bool, light_vp: words) -> void {
@ -874,31 +874,31 @@ function layer_draw_far(l: Layer, shadow: bool, light_vp: words) -> void {
if shadow { p = sc_imp_prog_shadow }
gl_use_program(p)
let im = l.imp
u_f(gl_uniform(p, "u_radius"), im.radius)
u_f(gl_uniform(p, "u_height"), im.height)
u_f(gl_uniform(p, "u_tiles"), fi(im.tiles))
u_f(gpu_uniform(p, "u_radius"), im.radius)
u_f(gpu_uniform(p, "u_height"), im.height)
u_f(gpu_uniform(p, "u_tiles"), fi(im.tiles))
r3d_bind_2d(p, "u_atlas_albedo", 0, im.albedo)
if shadow {
u_mat4(gl_uniform(p, "u_light_vp"), light_vp)
u_v3(gl_uniform(p, "u_face_dir"), sun_dir)
if r3d_debug_shadow and not sc_printed { sc_printed = true; print(`imp shadow prog {p} face_dir loc {gl_uniform(p, "u_face_dir")} sun {f_fx(sun_dir[0])} {f_fx(sun_dir[1])} {f_fx(sun_dir[2])} cam {f_fx(cam_pos[0])} {f_fx(cam_pos[1])} {f_fx(cam_pos[2])} n_far {l.n_far}`) }
u_v3(gl_uniform(p, "u_cam_pos"), cam_pos)
u_mat4(gpu_uniform(p, "u_light_vp"), light_vp)
u_v3(gpu_uniform(p, "u_face_dir"), sun_dir)
if r3d_debug_shadow and not sc_printed { sc_printed = true; print(`imp shadow prog {p} face_dir loc {gpu_uniform(p, "u_face_dir")} sun {f_fx(sun_dir[0])} {f_fx(sun_dir[1])} {f_fx(sun_dir[2])} cam {f_fx(cam_pos[0])} {f_fx(cam_pos[1])} {f_fx(cam_pos[2])} n_far {l.n_far}`) }
u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
} else {
u_mat4(gl_uniform(p, "u_view"), cam_view)
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
u_v3(gl_uniform(p, "u_tint"), l.tint)
if sc_dbg_lod { if sc_dbg_tint == null { sc_dbg_tint = v3_new(F_ONE, F_ONE, F_ONE) }; v3_set(sc_dbg_tint, fi(3), F_ZERO, fi(3)); u_v3(gl_uniform(p, "u_tint"), sc_dbg_tint) }
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_v3(gpu_uniform(p, "u_tint"), l.tint)
if sc_dbg_lod { if sc_dbg_tint == null { sc_dbg_tint = v3_new(F_ONE, F_ONE, F_ONE) }; v3_set(sc_dbg_tint, fi(3), F_ZERO, fi(3)); u_v3(gpu_uniform(p, "u_tint"), sc_dbg_tint) }
r3d_bind_2d(p, "u_atlas_normal", 1, im.normal)
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
if l.foliage { u_f(gl_uniform(p, "u_spec_scale"), fl(0.05)) }
if l.foliage { u_f(gpu_uniform(p, "u_spec_scale"), fl(0.05)) }
}
gl_disable(GL_CULL_FACE)
if not shadow and sc_a2c { gl_enable(GL_SAMPLE_ALPHA_TO_COVERAGE) }
gpu_cull(false)
if not shadow and sc_a2c { gpu_alpha_to_coverage(true) }
scatter_attach(sc_card.vao, l.imp_buf)
mesh_draw_instanced(sc_card, l.n_far)
gl_disable(GL_SAMPLE_ALPHA_TO_COVERAGE)
gpu_alpha_to_coverage(false)
}
# Cast from the impostor card, always, for every instance in the layer. The lit pass may
@ -910,14 +910,14 @@ function layer_draw_shadow(l: Layer, light_vp: words) -> void {
let p = sc_imp_prog_shadow
gl_use_program(p)
let im = l.imp
u_f(gl_uniform(p, "u_radius"), im.radius)
u_f(gl_uniform(p, "u_height"), im.height)
u_f(gl_uniform(p, "u_tiles"), fi(im.tiles))
u_f(gpu_uniform(p, "u_radius"), im.radius)
u_f(gpu_uniform(p, "u_height"), im.height)
u_f(gpu_uniform(p, "u_tiles"), fi(im.tiles))
r3d_bind_2d(p, "u_atlas_albedo", 0, im.albedo)
u_mat4(gl_uniform(p, "u_light_vp"), light_vp)
u_v3(gl_uniform(p, "u_face_dir"), sun_dir)
u_v3(gl_uniform(p, "u_cam_pos"), cam_pos)
gl_disable(GL_CULL_FACE)
u_mat4(gpu_uniform(p, "u_light_vp"), light_vp)
u_v3(gpu_uniform(p, "u_face_dir"), sun_dir)
u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
gpu_cull(false)
scatter_attach(sc_card.vao, l.sh_buf)
mesh_draw_instanced(sc_card, l.n_sh)
}
@ -935,15 +935,15 @@ function layer_draw_depth(l: Layer, model: Model, vb: int, cnt: int) -> void {
gl_use_program(p)
var ground = F_ZERO
if l.grounded { ground = F_ONE }
u_f(gl_uniform(p, "u_ground"), ground)
u_f(gpu_uniform(p, "u_ground"), ground)
if l.grounded { terrain_bind_height(p) }
u_f(gl_uniform(p, "u_time"), r3d_time)
u_f(gl_uniform(p, "u_wind"), l.wind)
u_f(gl_uniform(p, "u_model_h"), model.height)
u_mat4(gl_uniform(p, "u_view"), cam_view)
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
u_f(gl_uniform(p, "u_clip_y"), r3d_clip_y)
gl_disable(GL_CULL_FACE)
u_f(gpu_uniform(p, "u_time"), r3d_time)
u_f(gpu_uniform(p, "u_wind"), l.wind)
u_f(gpu_uniform(p, "u_model_h"), model.height)
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_f(gpu_uniform(p, "u_clip_y"), r3d_clip_y)
gpu_cull(false)
for i in 0 .. len(model.prims) {
let pr = model.prims[i]
scatter_attach(pr.mesh.vao, vb)
@ -968,7 +968,7 @@ function scatter_draw_depth() -> void {
layer_draw_depth(l, l.model, l.buf, l.n_near)
}
}
gl_enable(GL_CULL_FACE)
gpu_cull(true)
}
function scatter_draw() -> void {
for i in 0 .. len(sc_layers) {
@ -981,7 +981,7 @@ function scatter_draw() -> void {
layer_draw_near(l, false, null, false)
layer_draw_far(l, false, null)
}
gl_enable(GL_CULL_FACE)
gpu_cull(true)
}
# Nothing here is keyed off the cascade. Every skip that used to be — ground cover past
# the 250 m cascade, blades past the nearest, the scanned mesh past the second — made a
@ -1039,10 +1039,10 @@ function carpet_bake(layers: []Layer, count: int, tile: int, res: int) -> int {
gl_viewport(0, 0, res, res)
gl_clear_color(0.0, 0.0, 0.0, 0.0)
gl_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
gl_enable(GL_DEPTH_TEST)
gl_depth_func(GL_LESS)
gl_disable(GL_CULL_FACE)
gl_disable(GL_BLEND)
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
gpu_cull(false)
gpu_blend(false)
# the clumps, and eight wrapped copies so the tile's edges continue
let half = f_mul(tile, F_HALF)
let n9 = count * 9
@ -1076,14 +1076,14 @@ function carpet_bake(layers: []Layer, count: int, tile: int, res: int) -> int {
m4_ortho(proj, f_neg(half), half, f_neg(half), half, fl(0.1), fi(12))
let bake = sc_bake_card_prog
gl_use_program(bake)
u_mat4(gl_uniform(bake, "u_view"), view)
u_mat4(gl_uniform(bake, "u_proj"), proj)
u_f(gl_uniform(bake, "u_wind"), F_ZERO)
u_f(gl_uniform(bake, "u_time"), F_ZERO)
u_mat4(gpu_uniform(bake, "u_view"), view)
u_mat4(gpu_uniform(bake, "u_proj"), proj)
u_f(gpu_uniform(bake, "u_wind"), F_ZERO)
u_f(gpu_uniform(bake, "u_time"), F_ZERO)
for li in 0 .. len(layers) {
let l = layers[li]
if l.atlas == null { continue }
u_f(gl_uniform(bake, "u_card_w"), f_mul(l.atlas.radius, F_TWO)); u_f(gl_uniform(bake, "u_card_h"), l.atlas.height)
u_f(gpu_uniform(bake, "u_card_w"), f_mul(l.atlas.radius, F_TWO)); u_f(gpu_uniform(bake, "u_card_h"), l.atlas.height)
r3d_bind_2d(bake, "u_diff", 0, l.atlas.albedo)
r3d_bind_2d(bake, "u_arm", 2, l.atlas.normal)
for i in 0 .. len(l.model.prims) {

View file

@ -116,11 +116,10 @@ function shadow_pass() -> void {
var near = cam_near
gl_bind_framebuffer(GL_FRAMEBUFFER, sh_fbo)
gl_viewport(0, 0, SHADOW_RES, SHADOW_RES)
gl_enable(GL_DEPTH_TEST)
gl_depth_func(GL_LESS)
gl_enable(GL_POLYGON_OFFSET_FILL)
gl_polygon_offset(2.0, 4.0)
gl_disable(GL_CULL_FACE)
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
gpu_depth_bias(2.0, 4.0)
gpu_cull(false)
for c in 0 .. SHADOW_CASCADES {
sh_cascade = c
shadow_fit(c, near, sh_split[c])
@ -135,7 +134,7 @@ function shadow_pass() -> void {
scene_draw_casters(vp)
}
}
gl_disable(GL_POLYGON_OFFSET_FILL)
gpu_depth_bias(0.0, 0.0)
gl_bind_framebuffer(GL_FRAMEBUFFER, 0)
if r3d_debug_shadow { shadow_dump() }
if r3d_debug_shadow and not sh_printed2 {
@ -222,8 +221,8 @@ function shadow_dump() -> void {
var sh_printed: bool = false
# a uniform array's location: some drivers only answer to the "[0]" spelling
function sh_loc(prog: int, name: string) -> int {
var loc = gl_uniform(prog, name + "[0]")
if loc < 0 { loc = gl_uniform(prog, name) }
var loc = gpu_uniform(prog, name + "[0]")
if loc < 0 { loc = gpu_uniform(prog, name) }
return loc
}
function shadow_bind(prog: int) -> void {
@ -234,17 +233,17 @@ function shadow_bind(prog: int) -> void {
if ts == 0 { ts = ter_height_tex; ts_on = F_ZERO }
r3d_bind_2d(prog, "u_tershadow", 6, ts)
terrain_bind_height(prog)
u_f(gl_uniform(prog, "u_ts_on"), ts_on)
u_f(gl_uniform(prog, "u_ts_half"), fi(TERRAIN_HALF))
u_f2(gl_uniform(prog, "u_ts_origin"), ter_ox, ter_oz)
var loc = gl_uniform(prog, "u_cascade_vp[0]")
if loc < 0 { loc = gl_uniform(prog, "u_cascade_vp") }
if r3d_debug_shadow and not sh_printed { sh_printed = true; print(`cascade vp loc {loc} / {gl_uniform(prog, "u_cascade_vp")} split loc {gl_uniform(prog, "u_cascade_split")} shadow loc {gl_uniform(prog, "u_shadow")}`) }
gl_uniform_matrix4fv(loc, SHADOW_CASCADES, 0, sh_vp)
gl_uniform1fv(sh_loc(prog, "u_cascade_split"), SHADOW_CASCADES, sh_split)
if r3d_debug_shadow and not sh_printed3 { sh_printed3 = true; print(`range {f_fx(sh_range[0])} {f_fx(sh_range[1])} {f_fx(sh_range[2])} {f_fx(sh_range[3])} texel*1000 {f_fx(f_mul(sh_texel[0], fi(1000)))} {f_fx(f_mul(sh_texel[1], fi(1000)))} {f_fx(f_mul(sh_texel[2], fi(1000)))} {f_fx(f_mul(sh_texel[3], fi(1000)))} locs {gl_uniform(prog, "u_cascade_range")} {gl_uniform(prog, "u_cascade_texel")}`) }
gl_uniform1fv(sh_loc(prog, "u_cascade_range"), SHADOW_CASCADES, sh_range)
u_f(gpu_uniform(prog, "u_ts_on"), ts_on)
u_f(gpu_uniform(prog, "u_ts_half"), fi(TERRAIN_HALF))
u_f2(gpu_uniform(prog, "u_ts_origin"), ter_ox, ter_oz)
var loc = gpu_uniform(prog, "u_cascade_vp[0]")
if loc < 0 { loc = gpu_uniform(prog, "u_cascade_vp") }
if r3d_debug_shadow and not sh_printed { sh_printed = true; print(`cascade vp loc {loc} / {gpu_uniform(prog, "u_cascade_vp")} split loc {gpu_uniform(prog, "u_cascade_split")} shadow loc {gpu_uniform(prog, "u_shadow")}`) }
u_mat4n(loc, SHADOW_CASCADES, sh_vp)
u_fv(sh_loc(prog, "u_cascade_split"), SHADOW_CASCADES, sh_split)
if r3d_debug_shadow and not sh_printed3 { sh_printed3 = true; print(`range {f_fx(sh_range[0])} {f_fx(sh_range[1])} {f_fx(sh_range[2])} {f_fx(sh_range[3])} texel*1000 {f_fx(f_mul(sh_texel[0], fi(1000)))} {f_fx(f_mul(sh_texel[1], fi(1000)))} {f_fx(f_mul(sh_texel[2], fi(1000)))} {f_fx(f_mul(sh_texel[3], fi(1000)))} locs {gpu_uniform(prog, "u_cascade_range")} {gpu_uniform(prog, "u_cascade_texel")}`) }
u_fv(sh_loc(prog, "u_cascade_range"), SHADOW_CASCADES, sh_range)
if Os.has_env("R3D_FORCE") { sh_force = Text.to_int(Os.env("R3D_FORCE")) }
gl_uniform1i(gl_uniform(prog, "u_force_cascade"), sh_force)
gl_uniform1fv(sh_loc(prog, "u_cascade_texel"), SHADOW_CASCADES, sh_texel)
u_i(gpu_uniform(prog, "u_force_cascade"), sh_force)
u_fv(sh_loc(prog, "u_cascade_texel"), SHADOW_CASCADES, sh_texel)
}

View file

@ -204,9 +204,9 @@ function skin_pose(sk: Skin) -> void {
# the joint matrices onto a program's u_bones[]
function skin_bind(sk: Skin, prog: int) -> void {
var loc = gl_uniform(prog, "u_bones[0]")
if loc < 0 { loc = gl_uniform(prog, "u_bones") }
gl_uniform_matrix4fv(loc, sk.n_joints, 0, sk.bones)
var loc = gpu_uniform(prog, "u_bones[0]")
if loc < 0 { loc = gpu_uniform(prog, "u_bones") }
u_mat4n(loc, sk.n_joints, sk.bones)
}
# the same skeleton posed on its own: shares the rest data, owns the pose and the matrices

View file

@ -39,7 +39,7 @@ function sky_set_rot(yaw: int) -> void {
sky_yaw = yaw
sky_rot_s = f_sin(yaw); sky_rot_c = f_cos(yaw)
}
function sky_bind_rot(prog: int) -> void { u_f2(gl_uniform(prog, "u_sky_rot"), sky_rot_s, sky_rot_c) }
function sky_bind_rot(prog: int) -> void { u_f2(gpu_uniform(prog, "u_sky_rot"), sky_rot_s, sky_rot_c) }
# direction for an equirect uv (matches equirectUV in lighting.glsl)
function sky_dir_from_uv(o: words, u: int, v: int) -> void {
@ -77,9 +77,9 @@ function sky_convolve(prog: int, target_tex: int, layer: int, w: int, h: int, ro
gl_use_program(prog)
r3d_bind_2d(prog, "u_sky", 0, sky_tex)
sky_bind_rot(prog)
u_f(gl_uniform(prog, "u_sun_clip"), hdr_clip)
u_f(gl_uniform(prog, "u_rough"), rough)
u_f(gl_uniform(prog, "u_sky_w"), fi(sky_w))
u_f(gpu_uniform(prog, "u_sun_clip"), hdr_clip)
u_f(gpu_uniform(prog, "u_rough"), rough)
u_f(gpu_uniform(prog, "u_sky_w"), fi(sky_w))
mesh_draw(sky_fullscreen)
gl_bind_framebuffer(GL_FRAMEBUFFER, 0)
let ids = gl_scratch()
@ -88,7 +88,7 @@ function sky_convolve(prog: int, target_tex: int, layer: int, w: int, h: int, ro
}
function sky_precompute() -> void {
gl_disable(GL_DEPTH_TEST)
gpu_depth_test(false)
if sky_irradiance != 0 {
let ids = gl_scratch()
ids[0] = sky_irradiance; gl_delete_textures(1, ids)
@ -126,9 +126,9 @@ function sky_bind_lighting(prog: int) -> void {
r3d_bind_2d(prog, "u_irradiance", 12, sky_irradiance)
r3d_bind_tex(prog, "u_prefilter", 13, GL_TEXTURE_2D_ARRAY, sky_prefilter)
r3d_bind_2d(prog, "u_brdf", 14, sky_brdf)
u_v3(gl_uniform(prog, "u_sun_dir"), sun_dir)
u_v3(gl_uniform(prog, "u_sun_color"), sun_color)
u_v3(gl_uniform(prog, "u_cam_pos"), cam_pos)
u_f(gl_uniform(prog, "u_prefilter_levels"), fi(SKY_PREFILTER_LEVELS))
u_v3(gpu_uniform(prog, "u_sun_dir"), sun_dir)
u_v3(gpu_uniform(prog, "u_sun_color"), sun_color)
u_v3(gpu_uniform(prog, "u_cam_pos"), cam_pos)
u_f(gpu_uniform(prog, "u_prefilter_levels"), fi(SKY_PREFILTER_LEVELS))
daylight_bind(prog)
}

View file

@ -227,21 +227,21 @@ function terrain_generate() -> void {
gl_bind_framebuffer(GL_FRAMEBUFFER, fbo)
gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ter_height_tex, 0)
gl_viewport(0, 0, TERRAIN_RES, TERRAIN_RES)
gl_disable(GL_DEPTH_TEST)
gpu_depth_test(false)
gl_use_program(p)
u_f(gl_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(p, "u_half"), fi(TERRAIN_HALF))
if ter_dem_tex != 0 {
r3d_bind_2d(p, "u_dem", 0, ter_dem_tex)
u_f(gl_uniform(p, "u_dem_min"), ter_dem_min)
u_f(gl_uniform(p, "u_dem_max"), ter_dem_max)
u_f(gl_uniform(p, "u_dem_base"), ter_dem_base)
u_f2(gl_uniform(p, "u_origin"), ter_ox, ter_oz)
u_f4(gl_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_f(gl_uniform(p, "u_lake_level"), ter_lake_level)
u_f(gl_uniform(p, "u_sea_level"), ter_sea_gen())
u_f4(gl_uniform(p, "u_isle"), ter_isle_cx, ter_isle_cz, ter_isle_r, ter_isle_fall)
u_f(gl_uniform(p, "u_isle_mode"), fi(ter_isle_mode))
u_f(gl_uniform(p, "u_dem_blur"), ter_dem_blur)
u_f(gpu_uniform(p, "u_dem_min"), ter_dem_min)
u_f(gpu_uniform(p, "u_dem_max"), ter_dem_max)
u_f(gpu_uniform(p, "u_dem_base"), ter_dem_base)
u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)
u_f4(gpu_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
u_f(gpu_uniform(p, "u_lake_level"), ter_lake_level)
u_f(gpu_uniform(p, "u_sea_level"), ter_sea_gen())
u_f4(gpu_uniform(p, "u_isle"), ter_isle_cx, ter_isle_cz, ter_isle_r, ter_isle_fall)
u_f(gpu_uniform(p, "u_isle_mode"), fi(ter_isle_mode))
u_f(gpu_uniform(p, "u_dem_blur"), ter_dem_blur)
}
mesh_draw(sky_fullscreen)
# second pass: R = height, GBA = the smooth surface normal, baked once (ternormal.frag)
@ -251,7 +251,7 @@ function terrain_generate() -> void {
let pn = r3d_program("fullscreen.vert", "ternormal.frag", "")
gl_use_program(pn)
r3d_bind_2d(pn, "u_src", 0, raw)
u_f(gl_uniform(pn, "u_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(pn, "u_half"), fi(TERRAIN_HALF))
mesh_draw(sky_fullscreen)
gl_delete_program(pn)
ids0 = gl_scratch(); ids0[0] = raw; gl_delete_textures(1, ids0)
@ -271,8 +271,8 @@ function terrain_generate() -> void {
# The height field for shaders that place things on the ground (model.vert's u_ground)
function terrain_bind_height(p: int) -> void {
r3d_bind_2d(p, "u_ts_height", 5, ter_height_tex)
u_f(gl_uniform(p, "u_ts_half"), fi(TERRAIN_HALF))
u_f2(gl_uniform(p, "u_ts_origin"), ter_ox, ter_oz)
u_f(gpu_uniform(p, "u_ts_half"), fi(TERRAIN_HALF))
u_f2(gpu_uniform(p, "u_ts_origin"), ter_ox, ter_oz)
}
# Bake the height-field sun shadow (see tershadow.frag). Cheap enough to redo whenever
@ -286,12 +286,12 @@ function terrain_bake_shadow() -> void {
gl_bind_framebuffer(GL_FRAMEBUFFER, fbo)
gl_framebuffer_texture2d(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ter_shadow_tex, 0)
gl_viewport(0, 0, TERRAIN_SHADOW_RES, TERRAIN_SHADOW_RES)
gl_disable(GL_DEPTH_TEST)
gl_disable(GL_BLEND)
gpu_depth_test(false)
gpu_blend(false)
gl_use_program(p)
r3d_bind_2d(p, "u_height", 0, ter_height_tex)
u_f(gl_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_v3(gl_uniform(p, "u_sun"), sun_dir)
u_f(gpu_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_v3(gpu_uniform(p, "u_sun"), sun_dir)
mesh_draw(sky_fullscreen)
gl_bind_framebuffer(GL_FRAMEBUFFER, 0)
let ids = gl_scratch()
@ -452,31 +452,31 @@ function terrain_bind_prog(p: int) -> void {
r3d_bind_2d(p, "u_ortho", 4, orthotex)
var oon = F_ZERO
if ter_ortho_tex != 0 { oon = F_ONE }
u_f(gl_uniform(p, "u_ortho_on"), oon)
u_f(gpu_uniform(p, "u_ortho_on"), oon)
r3d_bind_2d(p, "u_rock_d", 7, ter_tex[6]); r3d_bind_2d(p, "u_rock_n", 8, ter_tex[7]); r3d_bind_2d(p, "u_rock_a", 9, ter_tex[8])
r3d_bind_2d(p, "u_snow_d", 10, ter_tex[9])
if ter_carpet != 0 { r3d_bind_2d(p, "u_carpet", 11, ter_carpet); u_f(gl_uniform(p, "u_carpet_on"), F_ONE) }
else { r3d_bind_2d(p, "u_carpet", 11, ter_tex[0]); u_f(gl_uniform(p, "u_carpet_on"), F_ZERO) }
u_f(gl_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_f(gl_uniform(p, "u_texel"), fr(1, TERRAIN_RES))
u_f(gl_uniform(p, "u_snow_line"), ter_snow_line)
if ter_carpet != 0 { r3d_bind_2d(p, "u_carpet", 11, ter_carpet); u_f(gpu_uniform(p, "u_carpet_on"), F_ONE) }
else { r3d_bind_2d(p, "u_carpet", 11, ter_tex[0]); u_f(gpu_uniform(p, "u_carpet_on"), F_ZERO) }
u_f(gpu_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_f(gpu_uniform(p, "u_texel"), fr(1, TERRAIN_RES))
u_f(gpu_uniform(p, "u_snow_line"), ter_snow_line)
var lake = fl(-100000.0)
if ter_lake_ex != 0 { lake = ter_lake_level }
u_f(gl_uniform(p, "u_lake_level"), lake)
u_f(gpu_uniform(p, "u_lake_level"), lake)
# the shoreline, forest and scree gates read the sea; unset it is what they always read
var sea = lake
if ter_sea_set { sea = ter_sea_level }
u_f(gl_uniform(p, "u_sea_level"), sea)
u_mat4(gl_uniform(p, "u_view"), cam_view)
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
u_f2(gl_uniform(p, "u_origin"), ter_ox, ter_oz)
u_f(gl_uniform(p, "u_far_split"), ter_far_split)
u_f(gl_uniform(p, "u_far_band"), ter_far_band)
u_f(gpu_uniform(p, "u_sea_level"), sea)
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)
u_f(gpu_uniform(p, "u_far_split"), ter_far_split)
u_f(gpu_uniform(p, "u_far_band"), ter_far_band)
sky_bind_lighting(p)
shadow_bind(p)
fog_bind(p)
u_v3(gl_uniform(p, "u_cam_pos"), cam_pos)
u_f(gl_uniform(p, "u_grid"), fi(CD_G))
u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
u_f(gpu_uniform(p, "u_grid"), fi(CD_G))
# The ground reads its sun visibility out of the buffer tersun.frag filled, and has no
# use for the cascade array shadow_bind just put on this unit; leaving both bound under
# one unit is undefined ground, so the array comes off first.
@ -523,24 +523,24 @@ function terrain_sun_pass(w: int, h: int, depth: int) -> Target {
let st = gl_check_framebuffer_status(GL_FRAMEBUFFER)
if st != GL_FRAMEBUFFER_COMPLETE { print(`r3d: sun-visibility framebuffer incomplete {st}`) }
}
gl_enable(GL_DEPTH_TEST)
gl_depth_func(GL_LESS)
gl_depth_mask(1)
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
gpu_depth_write(true)
# the depth is the frame's own and was cleared with it; only the visibility is cleared
gl_clear_color(1.0, 1.0, 1.0, 1.0) # unshadowed where nothing is drawn
gl_clear(GL_COLOR_BUFFER_BIT)
let p = ter_sun_prog
gl_use_program(p)
r3d_bind_2d(p, "u_height", 0, ter_height_tex)
u_f(gl_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_mat4(gl_uniform(p, "u_view"), cam_view)
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
u_f2(gl_uniform(p, "u_origin"), ter_ox, ter_oz)
u_v3(gl_uniform(p, "u_cam_pos"), cam_pos)
u_f(gl_uniform(p, "u_grid"), fi(CD_G))
u_f(gl_uniform(p, "u_far_split"), ter_far_split)
u_f(gl_uniform(p, "u_far_band"), ter_far_band)
u_f(gl_uniform(p, "u_clip_y"), r3d_clip_y)
u_f(gpu_uniform(p, "u_half"), fi(TERRAIN_HALF))
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_f2(gpu_uniform(p, "u_origin"), ter_ox, ter_oz)
u_v3(gpu_uniform(p, "u_cam_pos"), cam_pos)
u_f(gpu_uniform(p, "u_grid"), fi(CD_G))
u_f(gpu_uniform(p, "u_far_split"), ter_far_split)
u_f(gpu_uniform(p, "u_far_band"), ter_far_band)
u_f(gpu_uniform(p, "u_clip_y"), r3d_clip_y)
shadow_bind(p)
sky_bind_lighting(p)
ter_sun_pass = true
@ -575,11 +575,11 @@ function terrain_draw() -> void {
target_bind(post_hdr)
if post_ms_fbo != 0 { gl_bind_framebuffer(GL_FRAMEBUFFER, post_ms_fbo) }
}
gl_enable(GL_DEPTH_TEST)
gpu_depth_test(true)
# the prepass already laid this geometry's depth down: only the frontmost fragment of
# each pixel has anything to shade, and it meets that depth exactly
gl_depth_func(GL_LEQUAL)
gl_depth_mask(1)
gpu_depth_func(GL_LEQUAL)
gpu_depth_write(true)
terrain_bind_prog(ter_prog)
terrain_bind_prog(ter_prog_far)
terrain_bind_prog(ter_prog_near)
@ -591,7 +591,7 @@ function terrain_draw() -> void {
if ter_wire { gl_polygon_mode(GL_FRONT_AND_BACK, GL_LINE) }
cdlod_select(CD_LEVELS - 1, 0, 0)
if ter_wire { gl_polygon_mode(GL_FRONT_AND_BACK, GL_FILL) }
gl_depth_func(GL_LESS)
gpu_depth_func(GL_LESS)
if r3d_debug and not ter_printed { ter_printed = true; print(`cdlod patches drawn: {cd_draws} (far {cd_far_draws}, near {cd_near_draws}, band {cd_draws - cd_far_draws - cd_near_draws})`) }
}
var ter_wire: bool = false
@ -709,10 +709,10 @@ function cdlod_draw(level: int, ix: int, iz: int) -> void {
if ter_sun_pass {
let t = gl_scratch()
t[0] = x0; t[1] = z0; t[2] = size
gl_uniform3fv(gl_uniform(ter_sun_prog, "u_node"), 1, t)
u_v3(gpu_uniform(ter_sun_prog, "u_node"), t)
var st0 = F_ZERO
if level > 0 { st0 = cd_range[level - 1] }
u_f2(gl_uniform(ter_sun_prog, "u_morph"), f_lerp(st0, cd_range[level], fl(0.7)), cd_range[level])
u_f2(gpu_uniform(ter_sun_prog, "u_morph"), f_lerp(st0, cd_range[level], fl(0.7)), cd_range[level])
gl_draw_elements(GL_TRIANGLES, cd_mesh.count, GL_UNSIGNED_INT, null)
return
}
@ -730,11 +730,11 @@ function cdlod_draw(level: int, ix: int, iz: int) -> void {
if p != ter_prog_cur { gl_use_program(p); ter_prog_cur = p }
let t = gl_scratch()
t[0] = x0; t[1] = z0; t[2] = size
gl_uniform3fv(gl_uniform(p, "u_node"), 1, t)
u_v3(gpu_uniform(p, "u_node"), t)
var start = F_ZERO
if level > 0 { start = cd_range[level - 1] }
start = f_lerp(start, cd_range[level], fl(0.7))
u_f2(gl_uniform(p, "u_morph"), start, cd_range[level])
u_f2(gpu_uniform(p, "u_morph"), start, cd_range[level])
gl_draw_elements(GL_TRIANGLES, cd_mesh.count, GL_UNSIGNED_INT, null)
cd_draws += 1
}

View file

@ -67,11 +67,11 @@ function water_reflection_pass() -> void {
v3_copy(cam_pos, eye)
r3d_clip_y = f_sub(water_level, fl(0.05))
target_bind(water_refl)
gl_enable(GL_DEPTH_TEST)
gl_depth_func(GL_LESS)
gl_depth_mask(1)
gl_enable(GL_CULL_FACE)
gl_cull_face(GL_FRONT) # the mirror flips the winding
gpu_depth_test(true)
gpu_depth_func(GL_LESS)
gpu_depth_write(true)
gpu_cull(true)
gpu_cull_face(GL_FRONT) # the mirror flips the winding
gl_clear_color(0.0, 0.0, 0.0, 1.0)
gl_clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
sc_freeze = true
@ -84,7 +84,7 @@ function water_reflection_pass() -> void {
ter_reflect = false
sc_skip_blade = sb
sc_freeze = false
gl_cull_face(GL_BACK)
gpu_cull_face(GL_BACK)
if Os.has_env("R3D_DUMP_REFL") and not water_dumped { water_dumped = true; tex_dump(water_refl.color, water_refl.w, water_refl.h, "build/dbg_refl.ppm") }
# restore
r3d_clip_y = 0xCF000000
@ -138,14 +138,14 @@ function water_draw(depth_tex: int) -> void {
if not water_on or wb_n == 0 { return }
let p = water_prog
gl_use_program(p)
u_mat4(gl_uniform(p, "u_view"), cam_view)
u_mat4(gl_uniform(p, "u_proj"), cam_proj)
u_mat4(gl_uniform(p, "u_inv_vp"), cam_inv_vp)
u_mat4(gpu_uniform(p, "u_view"), cam_view)
u_mat4(gpu_uniform(p, "u_proj"), cam_proj)
u_mat4(gpu_uniform(p, "u_inv_vp"), cam_inv_vp)
# gl_FragCoord here runs over the scene target, which is post_w x post_h — not the
# window. They are the same size only at a render scale of 1; at anything less, taking
# the window's size sent the refraction and depth reads into the wrong corner of the
# frame, and the lake showed a squashed copy of it instead of its own bed.
u_f2(gl_uniform(p, "u_screen"), fi(post_w), fi(post_h))
u_f2(gpu_uniform(p, "u_screen"), fi(post_w), fi(post_h))
var ron = F_ZERO
if water_refl != null and wb_primary >= 0 {
# bind on its own unit first: generating the mip chain re-binds the texture on the active unit,
@ -162,26 +162,26 @@ function water_draw(depth_tex: int) -> void {
# Opaque. The surface composites the refracted bed itself, so there is nothing for
# hardware blending to do — and an alpha was what left see-through gaps in the foam
# and a clear band at the shore wide enough to give the plane away.
gl_disable(GL_BLEND)
gl_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
gpu_blend(false)
gpu_blend_func(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
# the surface writes depth: the ambient-occlusion and temporal passes read the frame's depth,
# and the bed 9 m below the shore would otherwise darken a band along the water line
gl_depth_mask(1)
gl_disable(GL_CULL_FACE)
gpu_depth_write(true)
gpu_cull(false)
# every body is the same plane at its own level and bounds; only the mirrored one samples
# the reflection - another body reading it would show the wrong world upside down
for i in 0 .. wb_n {
u_f(gl_uniform(p, "u_level"), wb_level[i])
u_f2(gl_uniform(p, "u_center"), wb_cx[i], wb_cz[i])
u_f2(gl_uniform(p, "u_extent"), wb_ex[i], wb_ez[i])
u_f(gpu_uniform(p, "u_level"), wb_level[i])
u_f2(gpu_uniform(p, "u_center"), wb_cx[i], wb_cz[i])
u_f2(gpu_uniform(p, "u_extent"), wb_ex[i], wb_ez[i])
var r = F_ZERO
if i == wb_primary { r = ron }
u_f(gl_uniform(p, "u_refl_on"), r)
u_f(gpu_uniform(p, "u_refl_on"), r)
# every body but the mirrored sea is clipped to the ellipse inside its bounds
var clip = F_ONE
if i == wb_primary { clip = F_ZERO }
u_f(gl_uniform(p, "u_clip_ellipse"), clip)
u_f(gpu_uniform(p, "u_clip_ellipse"), clip)
mesh_draw(water_mesh)
}
gl_disable(GL_BLEND)
gpu_blend(false)
}

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
}