feat(render3d): Vulkan textures, samplers and buffers

gpu_vk_res.ludic gains what gpu.ludic's texture and buffer handles stand for on Vulkan:

- gvk_tex_storage / _upload / _mips / _read / _release: an image and view per handle, a full mip
  chain when pixels come with it (the renderer asks for mipmaps after the upload) and one level
  for a target, every level in SHADER_READ_ONLY between uses. Uploads are converted to what the
  image stores: a missing alpha filled opaque (three-channel formats are stored with four), 16-bit
  PNG samples byte-swapped when the unpack state says so, 32-bit float HDR halved into half
  floats. Mips are blitted down level by level; a read-back brings level 0 home.
- gvk_sampler: one VkSampler per filter / wrap / compare / anisotropy combination, made when first
  asked for, with GL's defaults where the renderer set nothing.
- gvk_buf_*: vertex, index and instance buffers behind one handle, kept when an upload fits.
  Host-visible while the backend comes up.

gpu_vk.ludic switches on anisotropic sampling where the device has it and reads its limit.

examples/rendering/vk_resources.ludic checks it all: VKRES OK, validation-clean, every allocation
freed, on the RTX 3070 Ti (16x anisotropy) and on MoltenVK. One run on the Mac crashed while a
headless game run was using the GPU and did not come back in two reruns. OpenGL frames
byte-identical at the five viewpoints; 59 self-tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 11:57:28 +03:00
parent eb428ba5f3
commit c660ac881c
3 changed files with 572 additions and 2 deletions

View file

@ -16,6 +16,7 @@ var gvk_family: int = -1
var gvk_mp: bytes = null # VkPhysicalDeviceMemoryProperties
var gvk_device_name: string = ""
var gvk_why: string = "" # why the device did not come up, for the fallback notice
var gvk_max_aniso: int = 0x3F800000 # the device's anisotropy limit as float bits; 1.0 when it has none
function gvk_fail(what: string, r: int) -> bool {
gvk_why = `{what} failed (VkResult {r})`
@ -113,6 +114,9 @@ function gvk_init() -> bool {
if Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_descriptorIndexing) != 1 { missing = missing + " descriptorIndexing" }
if Vk.get_i32(f12, VkPhysicalDeviceVulkan12Features_timelineSemaphore) != 1 { missing = missing + " timelineSemaphore" }
if len(missing) > 0 { gvk_why = `{gvk_device_name} lacks{missing}`; return false }
# anisotropic filtering is a 1.0 feature every desktop GPU has; ask for it when it is there
let aniso = Vk.get_i32(f2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_samplerAnisotropy) == 1
if aniso { gvk_max_aniso = Vk.get_i32(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_maxSamplerAnisotropy) }
let want13 = bytes(VkPhysicalDeviceVulkan13Features_sizeof)
Vk.zero(want13, VkPhysicalDeviceVulkan13Features_sizeof)
Vk.put_i32(want13, VkPhysicalDeviceVulkan13Features_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_3_FEATURES)
@ -124,6 +128,11 @@ function gvk_init() -> bool {
Vk.put_ptr(want12, VkPhysicalDeviceVulkan12Features_pNext, want13)
Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_descriptorIndexing, 1)
Vk.put_i32(want12, VkPhysicalDeviceVulkan12Features_timelineSemaphore, 1)
let want2 = bytes(VkPhysicalDeviceFeatures2_sizeof)
Vk.zero(want2, VkPhysicalDeviceFeatures2_sizeof)
Vk.put_i32(want2, VkPhysicalDeviceFeatures2_sType, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2)
Vk.put_ptr(want2, VkPhysicalDeviceFeatures2_pNext, want12)
if aniso { Vk.put_i32(want2, VkPhysicalDeviceFeatures2_features + VkPhysicalDeviceFeatures_samplerAnisotropy, 1) }
Vk.put_i32(cnt, 0, 0)
Vk.enumerate_device_extension_properties(gvk_pd, null, cnt, null)
@ -142,7 +151,7 @@ function gvk_init() -> bool {
let dci = bytes(VkDeviceCreateInfo_sizeof)
Vk.zero(dci, VkDeviceCreateInfo_sizeof)
Vk.put_i32(dci, VkDeviceCreateInfo_sType, VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO)
Vk.put_ptr(dci, VkDeviceCreateInfo_pNext, want12)
Vk.put_ptr(dci, VkDeviceCreateInfo_pNext, want2)
Vk.put_i32(dci, VkDeviceCreateInfo_queueCreateInfoCount, 1)
Vk.put_ptr(dci, VkDeviceCreateInfo_pQueueCreateInfos, qci)
if gvk_ext_in(dexts, nde, "VK_KHR_portability_subset") {
@ -160,10 +169,17 @@ function gvk_init() -> bool {
if not gvk_cmd_init() { return false }
gvk_ready = true
print(`r3d: vulkan on {gvk_device_name}`)
print(`r3d: vulkan on {gvk_device_name}, anisotropy up to {gvk_aniso_x(gvk_max_aniso)}x`)
return true
}
# a positive float, as its bits, to a whole number (16.0 -> 16) for a message
function gvk_aniso_x(bits: int) -> int {
let e = ((bits >> 23) & 255) - 127
if e < 0 { return 0 }
return ((bits & 0x7FFFFF) | 0x800000) >> (23 - e)
}
# ---- memory -------------------------------------------------------------------------------
# The first memory type a resource allows with every property wanted; -1 if there is none.
function gvk_mem_type(allowed: int, want: int) -> int {