A freed texture or buffer id goes on a spare list the next one takes; tex_note_size keeps sizes by id. model_release frees prims, meshes, material names, the skin and leaves gltf_cached. actor_release takes an actor off the stage and actor_new reuses its record (ECS's Things come and go all day). steady.ludic: an actor round at 0 bytes over 2000. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
750 lines
44 KiB
Text
750 lines
44 KiB
Text
# gpu_vk_res.ludic — the Vulkan backend's resources in the renderer's vocabulary: the formats,
|
|
# and next the textures, buffers, samplers and pipelines gpu.ludic's handles stand for.
|
|
#
|
|
# gpu.ludic speaks OpenGL's names for formats, filters and blend factors, so this half of the
|
|
# backend does too, and is only compiled inside render3d (which names Gl.*). gpu_vk.ludic - the
|
|
# device, memory and one-shot commands - is pure Vulkan and runs on its own (vk_device.ludic).
|
|
|
|
# ---- formats ------------------------------------------------------------------------------
|
|
# The renderer names formats the way OpenGL does (they are gpu.ludic's vocabulary); these turn
|
|
# one into the Vulkan image format that holds it. Three-channel formats have no widely
|
|
# supported Vulkan image format, so they are stored with four and expanded on upload.
|
|
# block-compressed formats, named here (the GL header generated for render3d carries no BPTC):
|
|
# the values are GL's, so they sit in the same vocabulary as every other internal format
|
|
const R3D_BC7: int = 0x8E8C
|
|
const R3D_BC7_SRGB: int = 0x8E8D
|
|
const R3D_BC5: int = 0x8DBD
|
|
const R3D_BC4: int = 0x8DBB
|
|
function gvk_is_compressed(ifmt: int) -> bool { return ifmt == R3D_BC7 or ifmt == R3D_BC7_SRGB or ifmt == R3D_BC5 or ifmt == R3D_BC4 }
|
|
function gvk_format(ifmt: int) -> int {
|
|
if ifmt == R3D_BC7 { return VK_FORMAT_BC7_UNORM_BLOCK }
|
|
if ifmt == R3D_BC7_SRGB { return VK_FORMAT_BC7_SRGB_BLOCK }
|
|
if ifmt == R3D_BC5 { return VK_FORMAT_BC5_UNORM_BLOCK }
|
|
if ifmt == R3D_BC4 { return VK_FORMAT_BC4_UNORM_BLOCK }
|
|
if ifmt == GL_RGBA8 or ifmt == GL_RGB8 { return VK_FORMAT_R8G8B8A8_UNORM }
|
|
if ifmt == GL_RGB10_A2 { return VK_FORMAT_A2B10G10R10_UNORM_PACK32 } # the HDR10 screen and LDR image
|
|
if ifmt == GL_SRGB8_ALPHA8 or ifmt == GL_SRGB8 { return VK_FORMAT_R8G8B8A8_SRGB }
|
|
if ifmt == GL_R8 { return VK_FORMAT_R8_UNORM }
|
|
if ifmt == GL_RG8 { return VK_FORMAT_R8G8_UNORM }
|
|
if ifmt == GL_R16 { return VK_FORMAT_R16_UNORM }
|
|
if ifmt == GL_RG16 { return VK_FORMAT_R16G16_UNORM }
|
|
if ifmt == GL_RGBA16 or ifmt == GL_RGB16 { return VK_FORMAT_R16G16B16A16_UNORM }
|
|
if ifmt == GL_RG16F { return VK_FORMAT_R16G16_SFLOAT }
|
|
if ifmt == GL_RGBA16F or ifmt == GL_RGB16F { return VK_FORMAT_R16G16B16A16_SFLOAT }
|
|
if ifmt == GL_R32F { return VK_FORMAT_R32_SFLOAT }
|
|
if ifmt == GL_RGBA32F { return VK_FORMAT_R32G32B32A32_SFLOAT }
|
|
if ifmt == GL_DEPTH_COMPONENT32F or ifmt == GL_DEPTH_COMPONENT24 { return VK_FORMAT_D32_SFLOAT }
|
|
return VK_FORMAT_UNDEFINED
|
|
}
|
|
function gvk_is_depth(ifmt: int) -> bool { return ifmt == GL_DEPTH_COMPONENT32F or ifmt == GL_DEPTH_COMPONENT24 }
|
|
# channels the Vulkan image stores per texel
|
|
function gvk_channels(ifmt: int) -> int {
|
|
if ifmt == GL_R8 or ifmt == GL_R16 or ifmt == GL_R32F or gvk_is_depth(ifmt) { return 1 }
|
|
if ifmt == GL_RG8 or ifmt == GL_RG16 or ifmt == GL_RG16F { return 2 }
|
|
return 4
|
|
}
|
|
# bytes per channel the Vulkan image stores
|
|
function gvk_channel_bytes(ifmt: int) -> int {
|
|
if ifmt == GL_RGBA8 or ifmt == GL_RGB8 or ifmt == GL_SRGB8_ALPHA8 or ifmt == GL_SRGB8 or ifmt == GL_R8 or ifmt == GL_RG8 { return 1 }
|
|
if ifmt == GL_R32F or ifmt == GL_RGBA32F or gvk_is_depth(ifmt) { return 4 }
|
|
return 2
|
|
}
|
|
|
|
# ---- textures -----------------------------------------------------------------------------
|
|
# A texture handle indexes these lists. gpu.ludic's gpu_tx record keeps what the renderer said
|
|
# about it (size, format, filters, wraps, compare, mips, anisotropy); these keep the Vulkan
|
|
# objects. Pixels uploaded with the image get a full mip chain, allocated up front, because the
|
|
# renderer asks for mipmaps after the upload; an image made without pixels is a target and gets
|
|
# one level. Every level sits in SHADER_READ_ONLY_OPTIMAL between uses.
|
|
|
|
# a texture freed for good: its image goes, and its id is handed out again by gvk_tex_new - once, and
|
|
# only if it held an image, so a texture freed twice cannot give two new ones the same id
|
|
function gvk_tex_give_back(render3d_st: mut Render3dState, tex: int) -> void {
|
|
if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 0 { return }
|
|
gvk_tex_release(render3d_st, tex)
|
|
if render3d_st.gvk_tex_spare == null { render3d_st.gvk_tex_spare = new []int }
|
|
push(render3d_st.gvk_tex_spare, tex)
|
|
}
|
|
function gvk_tex_new(render3d_st: mut Render3dState) -> int {
|
|
let zero: long = 0
|
|
if render3d_st.gvk_tex_image == null {
|
|
render3d_st.gvk_tex_image = new []long; render3d_st.gvk_tex_view = new []long; render3d_st.gvk_tex_mem = new []long
|
|
render3d_st.gvk_tex_levels = new []int; render3d_st.gvk_tex_layers = new []int; render3d_st.gvk_tex_vkfmt = new []int
|
|
render3d_st.gvk_tex_dims_w = new []int; render3d_st.gvk_tex_dims_h = new []int
|
|
render3d_st.gvk_tex_glfmt = new []int; render3d_st.gvk_tex_array = new []int; render3d_st.gvk_tex_gen = new []int
|
|
render3d_st.gvk_tex_smp_sig = new []int; render3d_st.gvk_tex_smp = new []long
|
|
render3d_st.gvk_tex_samples = new []int; push(render3d_st.gvk_tex_samples, 0)
|
|
push(render3d_st.gvk_tex_dims_w, 0); push(render3d_st.gvk_tex_dims_h, 0)
|
|
push(render3d_st.gvk_tex_glfmt, 0); push(render3d_st.gvk_tex_array, 0); push(render3d_st.gvk_tex_gen, 0)
|
|
push(render3d_st.gvk_tex_smp_sig, 0); push(render3d_st.gvk_tex_smp, zero)
|
|
# handle 0 is "no texture", as it is on OpenGL
|
|
push(render3d_st.gvk_tex_image, zero); push(render3d_st.gvk_tex_view, zero); push(render3d_st.gvk_tex_mem, zero)
|
|
push(render3d_st.gvk_tex_levels, 0); push(render3d_st.gvk_tex_layers, 0); push(render3d_st.gvk_tex_vkfmt, 0)
|
|
}
|
|
# an id a freed texture gave back first: its generation goes on counting, so every cache keyed by
|
|
# (id, generation) - views, sets, samplers - tells the new texture from the old
|
|
if render3d_st.gvk_tex_spare != null and len(render3d_st.gvk_tex_spare) > 0 {
|
|
let sp = render3d_st.gvk_tex_spare
|
|
let t = sp[len(sp) - 1]
|
|
List.pop(sp)
|
|
render3d_st.gvk_tex_image[t] = zero; render3d_st.gvk_tex_view[t] = zero; render3d_st.gvk_tex_mem[t] = zero
|
|
render3d_st.gvk_tex_levels[t] = 0; render3d_st.gvk_tex_layers[t] = 0; render3d_st.gvk_tex_vkfmt[t] = 0
|
|
render3d_st.gvk_tex_dims_w[t] = 0; render3d_st.gvk_tex_dims_h[t] = 0; render3d_st.gvk_tex_glfmt[t] = 0; render3d_st.gvk_tex_array[t] = 0
|
|
render3d_st.gvk_tex_smp_sig[t] = 0; render3d_st.gvk_tex_smp[t] = zero; render3d_st.gvk_tex_samples[t] = 0
|
|
return t
|
|
}
|
|
push(render3d_st.gvk_tex_image, zero); push(render3d_st.gvk_tex_view, zero); push(render3d_st.gvk_tex_mem, zero)
|
|
push(render3d_st.gvk_tex_levels, 0); push(render3d_st.gvk_tex_layers, 0); push(render3d_st.gvk_tex_vkfmt, 0)
|
|
push(render3d_st.gvk_tex_dims_w, 0); push(render3d_st.gvk_tex_dims_h, 0)
|
|
push(render3d_st.gvk_tex_glfmt, 0); push(render3d_st.gvk_tex_array, 0); push(render3d_st.gvk_tex_gen, 0)
|
|
push(render3d_st.gvk_tex_smp_sig, 0); push(render3d_st.gvk_tex_smp, zero)
|
|
push(render3d_st.gvk_tex_samples, 0)
|
|
return len(render3d_st.gvk_tex_image) - 1
|
|
}
|
|
|
|
function gvk_mip_levels(w: int, h: int) -> int {
|
|
var n = 1
|
|
var s = w
|
|
if h > s { s = h }
|
|
while s > 1 { s = s / 2; n += 1 }
|
|
return n
|
|
}
|
|
|
|
function gvk_layout_access(layout: int) -> int {
|
|
if layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL { return VK_ACCESS_TRANSFER_WRITE_BIT }
|
|
if layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL { return VK_ACCESS_TRANSFER_READ_BIT }
|
|
if layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL { return VK_ACCESS_SHADER_READ_BIT }
|
|
if layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL { return VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT }
|
|
if layout == VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL { return VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT }
|
|
return 0
|
|
}
|
|
# levels [base, base + n) of every layer of an image, from one layout to another
|
|
function gvk_barrier(render3d_st: mut Render3dState, cb: pointer, image: long, depth: bool, base: int, n: int, layers: int, old_layout: int, new_layout: int) -> void {
|
|
# an image that was never made (its memory could not be had) has nothing to transition
|
|
if image == 0 { return }
|
|
let b = gvk_tmp(render3d_st, VkImageMemoryBarrier_sizeof)
|
|
Vk.zero(b, VkImageMemoryBarrier_sizeof)
|
|
Vk.put_i32(b, VkImageMemoryBarrier_sType, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER)
|
|
Vk.put_i32(b, VkImageMemoryBarrier_srcAccessMask, gvk_layout_access(old_layout))
|
|
Vk.put_i32(b, VkImageMemoryBarrier_dstAccessMask, gvk_layout_access(new_layout))
|
|
Vk.put_i32(b, VkImageMemoryBarrier_oldLayout, old_layout)
|
|
Vk.put_i32(b, VkImageMemoryBarrier_newLayout, new_layout)
|
|
Vk.put_i32(b, VkImageMemoryBarrier_srcQueueFamilyIndex, VK_QUEUE_FAMILY_IGNORED)
|
|
Vk.put_i32(b, VkImageMemoryBarrier_dstQueueFamilyIndex, VK_QUEUE_FAMILY_IGNORED)
|
|
Vk.put_i64(b, VkImageMemoryBarrier_image, image)
|
|
let r = VkImageMemoryBarrier_subresourceRange
|
|
var aspect = VK_IMAGE_ASPECT_COLOR_BIT
|
|
if depth { aspect = VK_IMAGE_ASPECT_DEPTH_BIT }
|
|
Vk.put_i32(b, r + VkImageSubresourceRange_aspectMask, aspect)
|
|
Vk.put_i32(b, r + VkImageSubresourceRange_baseMipLevel, base)
|
|
Vk.put_i32(b, r + VkImageSubresourceRange_levelCount, n)
|
|
Vk.put_i32(b, r + VkImageSubresourceRange_baseArrayLayer, 0)
|
|
Vk.put_i32(b, r + VkImageSubresourceRange_layerCount, layers)
|
|
Vk.cmd_pipeline_barrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, null, 0, null, 1, b)
|
|
}
|
|
|
|
# a host-visible buffer of n bytes, mapped; gvk_staging_free unmaps and frees it
|
|
function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> pointer {
|
|
let size: long = n
|
|
let bci = gvk_tmp(render3d_st, 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, size)
|
|
Vk.put_i32(bci, VkBufferCreateInfo_usage, usage)
|
|
Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
|
|
let out = gvk_tmp(render3d_st, 8)
|
|
render3d_st.gvk_mk_buf += 1
|
|
if Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) != VK_SUCCESS { return null }
|
|
render3d_st.gvk_st_buf = gvk_handle(out)
|
|
let req = gvk_tmp(render3d_st, VkMemoryRequirements_sizeof)
|
|
Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, render3d_st.gvk_st_buf, req)
|
|
render3d_st.gvk_st_mem = gvk_alloc(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)
|
|
let zero: long = 0
|
|
Vk.bind_buffer_memory(render3d_st.gvk_dev, render3d_st.gvk_st_buf, render3d_st.gvk_st_mem, zero)
|
|
let pp = gvk_tmp(render3d_st, 8)
|
|
if Vk.map_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, zero, size, 0, pp) != VK_SUCCESS { return null }
|
|
return Vk.get_ptr(pp, 0)
|
|
}
|
|
function gvk_staging_free(render3d_st: mut Render3dState) -> void {
|
|
gvk_frame_wait(render3d_st) # the frame in flight may still read it
|
|
Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem)
|
|
render3d_st.gvk_mk_x_buf += 1
|
|
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_st_buf, null)
|
|
render3d_st.gvk_mk_x_mem += 1
|
|
Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, null)
|
|
render3d_st.gvk_n_allocs -= 1
|
|
}
|
|
|
|
# (Re)make the image behind a handle: glTexImage2D on a texture that already has one replaces it.
|
|
function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool, ifmt: int, w: int, h: int, layers: int, with_mips: bool) -> bool {
|
|
gvk_frame_wait(render3d_st) # the frame in flight may still read it
|
|
if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) { return false }
|
|
let vkfmt = gvk_format(ifmt)
|
|
if vkfmt == VK_FORMAT_UNDEFINED { print(`r3d: vulkan: no image format for GL format {ifmt}`); return false }
|
|
gvk_tex_release(render3d_st, tex)
|
|
let depth = gvk_is_depth(ifmt)
|
|
var levels = 1
|
|
if with_mips and not depth { levels = gvk_mip_levels(w, h) }
|
|
var samples = render3d_st.gvk_storage_samples
|
|
if samples < 1 { samples = 1 }
|
|
if samples > 1 { levels = 1 }
|
|
var usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT
|
|
# a block-compressed image is only ever sampled and copied into: it cannot be drawn to
|
|
if depth { usage = usage | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT } else if not gvk_is_compressed(ifmt) { usage = usage | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT }
|
|
# DLSS reads and writes the frame from compute: a float colour target is storage too while
|
|
# Streamline is running (8-bit sRGB formats cannot be, so only the float ones)
|
|
if render3d_st.gsl_on and samples == 1 and (vkfmt == VK_FORMAT_R16G16B16A16_SFLOAT or vkfmt == VK_FORMAT_R32_SFLOAT) { usage = usage | VK_IMAGE_USAGE_STORAGE_BIT }
|
|
let ici = bytes(VkImageCreateInfo_sizeof)
|
|
Vk.zero(ici, VkImageCreateInfo_sizeof)
|
|
Vk.put_i32(ici, VkImageCreateInfo_sType, VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO)
|
|
Vk.put_i32(ici, VkImageCreateInfo_imageType, VK_IMAGE_TYPE_2D)
|
|
Vk.put_i32(ici, VkImageCreateInfo_format, vkfmt)
|
|
Vk.put_i32(ici, VkImageCreateInfo_extent + VkExtent3D_width, w)
|
|
Vk.put_i32(ici, VkImageCreateInfo_extent + VkExtent3D_height, h)
|
|
Vk.put_i32(ici, VkImageCreateInfo_extent + VkExtent3D_depth, 1)
|
|
Vk.put_i32(ici, VkImageCreateInfo_mipLevels, levels)
|
|
Vk.put_i32(ici, VkImageCreateInfo_arrayLayers, layers)
|
|
Vk.put_i32(ici, VkImageCreateInfo_samples, samples)
|
|
Vk.put_i32(ici, VkImageCreateInfo_tiling, VK_IMAGE_TILING_OPTIMAL)
|
|
Vk.put_i32(ici, VkImageCreateInfo_usage, usage)
|
|
Vk.put_i32(ici, VkImageCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
|
|
Vk.put_i32(ici, VkImageCreateInfo_initialLayout, VK_IMAGE_LAYOUT_UNDEFINED)
|
|
let out = bytes(8)
|
|
render3d_st.gvk_mk_img += 1
|
|
var r = Vk.create_image(render3d_st.gvk_dev, ici, null, out)
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateImage {w}x{h}x{layers} format {vkfmt}`, r) }
|
|
let image = gvk_handle(out)
|
|
let req = bytes(VkMemoryRequirements_sizeof)
|
|
Vk.get_image_memory_requirements(render3d_st.gvk_dev, image, req)
|
|
let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, true)
|
|
let mem: long = ma # the allocation id (gvk_mem_new), kept where the memory was
|
|
let zero: long = 0
|
|
# no memory for it (one allocation per resource meets the driver's allocation limit long before
|
|
# the card is full): say so instead of binding a null allocation, which the driver may accept
|
|
if mem == 0 {
|
|
render3d_st.gvk_mk_x_img += 1
|
|
Vk.destroy_image(render3d_st.gvk_dev, image, null)
|
|
return gvk_fail(render3d_st, `no device memory for a {w}x{h}x{layers} image ({render3d_st.gvk_n_allocs} allocations live)`, VK_ERROR_OUT_OF_DEVICE_MEMORY)
|
|
}
|
|
r = Vk.bind_image_memory(render3d_st.gvk_dev, image, gvk_mem_handle(render3d_st, ma), gvk_mem_offset(render3d_st, ma))
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkBindImageMemory", r) }
|
|
let vci = bytes(VkImageViewCreateInfo_sizeof)
|
|
Vk.zero(vci, VkImageViewCreateInfo_sizeof)
|
|
Vk.put_i32(vci, VkImageViewCreateInfo_sType, VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO)
|
|
Vk.put_i64(vci, VkImageViewCreateInfo_image, image)
|
|
if array { Vk.put_i32(vci, VkImageViewCreateInfo_viewType, VK_IMAGE_VIEW_TYPE_2D_ARRAY) } else { Vk.put_i32(vci, VkImageViewCreateInfo_viewType, VK_IMAGE_VIEW_TYPE_2D) }
|
|
Vk.put_i32(vci, VkImageViewCreateInfo_format, vkfmt)
|
|
let sr = VkImageViewCreateInfo_subresourceRange
|
|
if depth { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } else { Vk.put_i32(vci, sr + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
|
|
Vk.put_i32(vci, sr + VkImageSubresourceRange_levelCount, levels)
|
|
Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, layers)
|
|
render3d_st.gvk_mk_view += 1
|
|
r = Vk.create_image_view(render3d_st.gvk_dev, vci, null, out)
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateImageView", r) }
|
|
render3d_st.gvk_tex_image[tex] = image; render3d_st.gvk_tex_view[tex] = gvk_handle(out); render3d_st.gvk_tex_mem[tex] = mem
|
|
render3d_st.gvk_tex_levels[tex] = levels; render3d_st.gvk_tex_layers[tex] = layers; render3d_st.gvk_tex_vkfmt[tex] = vkfmt
|
|
render3d_st.gvk_tex_dims_w[tex] = w; render3d_st.gvk_tex_dims_h[tex] = h
|
|
render3d_st.gvk_tex_glfmt[tex] = ifmt; render3d_st.gvk_tex_gen[tex] = render3d_st.gvk_tex_gen[tex] + 1
|
|
render3d_st.gvk_tex_samples[tex] = samples
|
|
if array { render3d_st.gvk_tex_array[tex] = 1 } else { render3d_st.gvk_tex_array[tex] = 0 }
|
|
# a target starts cleared-to-nothing but readable: every level in the layout samplers expect
|
|
let cb = gvk_once_begin(render3d_st)
|
|
gvk_barrier(render3d_st, cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
return gvk_once_end(render3d_st, cb)
|
|
}
|
|
|
|
# IEEE single (as its bits) to IEEE half: out-of-range values saturate to infinity, tiny ones to zero
|
|
function gvk_half(f: int) -> int {
|
|
let s = (f >> 16) & 0x8000
|
|
let e = ((f >> 23) & 255) - 112
|
|
let m = f & 0x7FFFFF
|
|
if e <= 0 { return s }
|
|
if e >= 31 { return s | 0x7C00 }
|
|
return s | (e << 10) | (m >> 13)
|
|
}
|
|
function gvk_gl_channels(fmt: int) -> int {
|
|
if fmt == GL_RED or fmt == GL_DEPTH_COMPONENT { return 1 }
|
|
if fmt == GL_RG { return 2 }
|
|
if fmt == GL_RGB { return 3 }
|
|
return 4
|
|
}
|
|
function gvk_gl_type_bytes(ty: int) -> int {
|
|
if ty == GL_UNSIGNED_SHORT or ty == GL_HALF_FLOAT { return 2 }
|
|
if ty == GL_FLOAT or ty == GL_UNSIGNED_INT { return 4 }
|
|
return 1
|
|
}
|
|
|
|
# Level 0 of every layer from pixels laid out the OpenGL way (fmt channels of type ty), converted
|
|
# to what the image stores: a missing alpha filled opaque, 16-bit samples byte-swapped when the
|
|
# unpack state says so, 32-bit floats into a half-float image halved.
|
|
function gvk_tex_upload(render3d_st: mut Render3dState, tex: int, ifmt: int, w: int, h: int, layers: int, fmt: int, ty: int, data: pointer) -> bool {
|
|
let cin = gvk_gl_channels(fmt)
|
|
let bin = gvk_gl_type_bytes(ty)
|
|
let cout = gvk_channels(ifmt)
|
|
let bout = gvk_channel_bytes(ifmt)
|
|
let texels = w * h * layers
|
|
let n = texels * cout * bout
|
|
let dst = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_SRC_BIT)
|
|
if dst == null { print("r3d: vulkan: no staging buffer for an upload"); return false }
|
|
# written straight into the mapped staging memory: a copy of its own was never freed
|
|
let buf: pointer = dst
|
|
if cin == cout and bin == bout and not (bin == 2 and render3d_st.gvk_unpack_swap) {
|
|
mem_copy(buf, data, n)
|
|
} else {
|
|
let src: pointer = data
|
|
for t in 0 .. texels {
|
|
for c in 0 .. cout {
|
|
let o = (t * cout + c) * bout
|
|
if c >= cin {
|
|
# the channel the source does not have: alpha, opaque
|
|
if bout == 1 { buf[o] = 255 }
|
|
if bout == 2 { if ty == GL_FLOAT or ty == GL_HALF_FLOAT { buf[o] = 0; buf[o + 1] = 0x3C } else { buf[o] = 255; buf[o + 1] = 255 } }
|
|
if bout == 4 { Vk.put_i32(buf, o, 0x3F800000) }
|
|
} else {
|
|
let i = (t * cin + c) * bin
|
|
if bin == bout {
|
|
if bin == 1 { buf[o] = src[i] }
|
|
if bin == 2 {
|
|
if render3d_st.gvk_unpack_swap { buf[o] = src[i + 1]; buf[o + 1] = src[i] } else { buf[o] = src[i]; buf[o + 1] = src[i + 1] }
|
|
}
|
|
if bin == 4 { Vk.put_i32(buf, o, Vk.get_i32(src, i)) }
|
|
} else if bin == 4 and bout == 2 {
|
|
let hv = gvk_half(Vk.get_i32(src, i))
|
|
buf[o] = hv & 255; buf[o + 1] = (hv >> 8) & 255
|
|
} else {
|
|
print(`r3d: vulkan: no conversion from {bin}-byte to {bout}-byte samples`)
|
|
gvk_staging_free(render3d_st)
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let image = render3d_st.gvk_tex_image[tex]
|
|
let depth = gvk_is_depth(ifmt)
|
|
let levels = render3d_st.gvk_tex_levels[tex]
|
|
let cb = gvk_once_begin(render3d_st)
|
|
gvk_barrier(render3d_st, cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
|
|
let bic = gvk_tmp(render3d_st, VkBufferImageCopy_sizeof)
|
|
Vk.zero(bic, VkBufferImageCopy_sizeof)
|
|
if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) }
|
|
else { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
|
|
Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_layerCount, layers)
|
|
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_width, w)
|
|
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h)
|
|
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1)
|
|
Vk.cmd_copy_buffer_to_image(cb, render3d_st.gvk_st_buf, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, bic)
|
|
gvk_barrier(render3d_st, cb, image, depth, 0, levels, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
let ok = gvk_once_end(render3d_st, cb)
|
|
gvk_staging_free(render3d_st)
|
|
return ok
|
|
}
|
|
|
|
# The mip chain from level 0, each level blitted down from the one above it
|
|
# A target made without pixels has one level; the renderer asking it for mipmaps (the exposure
|
|
# measure reads the HDR scene's smallest level every frame) grows it a full chain, level 0 kept.
|
|
function gvk_tex_grow_mips(render3d_st: mut Render3dState, tex: int, w: int, h: int) -> bool {
|
|
gvk_frame_wait(render3d_st) # the frame in flight may still read it
|
|
let old_image = render3d_st.gvk_tex_image[tex]
|
|
let old_view = render3d_st.gvk_tex_view[tex]
|
|
let old_mem = render3d_st.gvk_tex_mem[tex]
|
|
let layers = render3d_st.gvk_tex_layers[tex]
|
|
let zero: long = 0
|
|
render3d_st.gvk_tex_image[tex] = zero
|
|
if not gvk_tex_storage(render3d_st, tex, render3d_st.gvk_tex_array[tex] == 1, render3d_st.gvk_tex_glfmt[tex], w, h, layers, true) { return false }
|
|
let cb = gvk_once_begin(render3d_st)
|
|
gvk_barrier(render3d_st, cb, old_image, false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
|
|
gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
|
|
let ic = gvk_tmp(render3d_st, VkImageCopy_sizeof)
|
|
Vk.zero(ic, VkImageCopy_sizeof)
|
|
Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
|
|
Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_layerCount, layers)
|
|
Vk.put_i32(ic, VkImageCopy_dstSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
|
|
Vk.put_i32(ic, VkImageCopy_dstSubresource + VkImageSubresourceLayers_layerCount, layers)
|
|
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_width, w)
|
|
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_height, h)
|
|
Vk.put_i32(ic, VkImageCopy_extent + VkExtent3D_depth, 1)
|
|
Vk.cmd_copy_image(cb, old_image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_tex_image[tex], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
|
|
gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
let ok = gvk_once_end(render3d_st, cb)
|
|
render3d_st.gvk_mk_x_view += 1
|
|
Vk.destroy_image_view(render3d_st.gvk_dev, old_view, null)
|
|
render3d_st.gvk_mk_x_img += 1
|
|
Vk.destroy_image(render3d_st.gvk_dev, old_image, null)
|
|
gvk_mem_free(render3d_st, gvk_mem_id(old_mem))
|
|
return ok
|
|
}
|
|
|
|
function gvk_tex_mips(render3d_st: mut Render3dState, tex: int, w: int, h: int) -> bool {
|
|
# a block-compressed texture brought every level with it, and cannot be blitted into
|
|
if gvk_is_compressed(render3d_st.gvk_tex_glfmt[tex]) { return true }
|
|
if render3d_st.gvk_tex_levels[tex] <= 1 and (w > 1 or h > 1) and not gvk_is_depth(render3d_st.gvk_tex_glfmt[tex]) {
|
|
if not gvk_tex_grow_mips(render3d_st, tex, w, h) { return false }
|
|
}
|
|
let levels = render3d_st.gvk_tex_levels[tex]
|
|
if levels <= 1 { return true }
|
|
let cb = gvk_once_begin(render3d_st)
|
|
gvk_tex_mips_into(render3d_st, cb, tex, w, h)
|
|
return gvk_once_end(render3d_st, cb)
|
|
}
|
|
# the chain's blits and barriers recorded into cb (the frame's own, when one is open)
|
|
function gvk_tex_mips_into(render3d_st: mut Render3dState, cb: pointer, tex: int, w: int, h: int) -> void {
|
|
let levels = render3d_st.gvk_tex_levels[tex]
|
|
if levels <= 1 { return }
|
|
let image = render3d_st.gvk_tex_image[tex]
|
|
let layers = render3d_st.gvk_tex_layers[tex]
|
|
var sw = w
|
|
var sh = h
|
|
for lv in 1 .. levels {
|
|
var dw = sw / 2
|
|
if dw < 1 { dw = 1 }
|
|
var dh = sh / 2
|
|
if dh < 1 { dh = 1 }
|
|
gvk_barrier(render3d_st, cb, image, false, lv - 1, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
|
|
gvk_barrier(render3d_st, cb, image, false, lv, 1, layers, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
|
|
let blit = gvk_tmp(render3d_st, VkImageBlit_sizeof)
|
|
Vk.zero(blit, VkImageBlit_sizeof)
|
|
Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
|
|
Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_mipLevel, lv - 1)
|
|
Vk.put_i32(blit, VkImageBlit_srcSubresource + VkImageSubresourceLayers_layerCount, layers)
|
|
Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_x, sw)
|
|
Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_y, sh)
|
|
Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1)
|
|
Vk.put_i32(blit, VkImageBlit_dstSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
|
|
Vk.put_i32(blit, VkImageBlit_dstSubresource + VkImageSubresourceLayers_mipLevel, lv)
|
|
Vk.put_i32(blit, VkImageBlit_dstSubresource + VkImageSubresourceLayers_layerCount, layers)
|
|
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_x, dw)
|
|
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, dh)
|
|
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1)
|
|
Vk.cmd_blit_image(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR)
|
|
gvk_barrier(render3d_st, cb, image, false, lv - 1, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
gvk_barrier(render3d_st, cb, image, false, lv, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
sw = dw
|
|
sh = dh
|
|
}
|
|
}
|
|
|
|
# Level 0 of layer 0 back to the CPU, laid out the OpenGL way when the layouts agree (the terrain
|
|
# reads its height field back as R32F into GL_RED / GL_FLOAT)
|
|
function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: int, h: int, fmt: int, ty: int, out: pointer) -> bool {
|
|
let cout = gvk_channels(ifmt)
|
|
let bout = gvk_channel_bytes(ifmt)
|
|
let cwant = gvk_gl_channels(fmt)
|
|
# a read may ask for fewer channels than the image has (the height field's R of an RGBA32F
|
|
# bake), never more, and only in the sample size the image stores
|
|
if cwant > cout or gvk_gl_type_bytes(ty) != bout {
|
|
print(`r3d: vulkan: no read-back conversion for GL format {ifmt} as {fmt}/{ty}`)
|
|
return false
|
|
}
|
|
let n = w * h * cout * bout
|
|
let src = gvk_staging(render3d_st, n, VK_BUFFER_USAGE_TRANSFER_DST_BIT)
|
|
if src == null { return false }
|
|
let image = render3d_st.gvk_tex_image[tex]
|
|
let depth = gvk_is_depth(ifmt)
|
|
let cb = gvk_once_begin(render3d_st)
|
|
gvk_barrier(render3d_st, cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
|
|
let bic = gvk_tmp(render3d_st, VkBufferImageCopy_sizeof)
|
|
Vk.zero(bic, VkBufferImageCopy_sizeof)
|
|
if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) }
|
|
else { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
|
|
Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_layerCount, 1)
|
|
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_width, w)
|
|
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_height, h)
|
|
Vk.put_i32(bic, VkBufferImageCopy_imageExtent + VkExtent3D_depth, 1)
|
|
Vk.cmd_copy_image_to_buffer(cb, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_st_buf, 1, bic)
|
|
gvk_barrier(render3d_st, cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
let ok = gvk_once_end(render3d_st, cb)
|
|
if ok and cwant == cout { mem_copy(out, src, n) }
|
|
if ok and cwant < cout {
|
|
let texel = cout * bout
|
|
let keep = cwant * bout
|
|
for t in 0 .. w * h { mem_copy(mem_off(out, t * keep), mem_off(src, t * texel), keep) }
|
|
}
|
|
gvk_staging_free(render3d_st)
|
|
return ok
|
|
}
|
|
|
|
function gvk_tex_release(render3d_st: mut Render3dState, tex: int) -> void {
|
|
gvk_frame_wait(render3d_st) # the frame in flight may still read it
|
|
if render3d_st.gvk_tex_image[tex] == 0 { return }
|
|
let zero: long = 0
|
|
render3d_st.gvk_mk_x_view += 1
|
|
Vk.destroy_image_view(render3d_st.gvk_dev, render3d_st.gvk_tex_view[tex], null)
|
|
render3d_st.gvk_mk_x_img += 1
|
|
Vk.destroy_image(render3d_st.gvk_dev, render3d_st.gvk_tex_image[tex], null)
|
|
gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_tex_mem[tex]))
|
|
gvk_layer_views_drop(render3d_st, tex)
|
|
render3d_st.gvk_tex_image[tex] = zero; render3d_st.gvk_tex_view[tex] = zero; render3d_st.gvk_tex_mem[tex] = zero
|
|
render3d_st.gvk_tex_levels[tex] = 0; render3d_st.gvk_tex_layers[tex] = 0; render3d_st.gvk_tex_vkfmt[tex] = 0
|
|
render3d_st.gvk_tex_gen[tex] = render3d_st.gvk_tex_gen[tex] + 1
|
|
}
|
|
|
|
# the per-level and per-layer views gvk_view_of made of a texture go with it: kept, each held its
|
|
# old Metal texture alive, so every rebuild of the screen targets left the last set's memory behind
|
|
function gvk_layer_views_drop(render3d_st: mut Render3dState, tex: int) -> void {
|
|
if render3d_st.gvk_layer_views == null { return }
|
|
let keys = render3d_st.gvk_layer_views
|
|
let views = render3d_st.gvk_layer_view
|
|
let texs = render3d_st.gvk_layer_view_tex
|
|
var w = 0
|
|
for i in 0 .. len(keys) {
|
|
if texs[i] == tex {
|
|
render3d_st.gvk_mk_x_view += 1
|
|
Vk.destroy_image_view(render3d_st.gvk_dev, views[i], null)
|
|
} else {
|
|
keys[w] = keys[i]; views[w] = views[i]; texs[w] = texs[i]
|
|
w += 1
|
|
}
|
|
}
|
|
while len(keys) > w { List.pop(keys) }
|
|
while len(views) > w { List.pop(views) }
|
|
while len(texs) > w { List.pop(texs) }
|
|
}
|
|
|
|
# ---- samplers -----------------------------------------------------------------------------
|
|
# One VkSampler per distinct way of reading a texture, made the first time it is asked for.
|
|
# The arguments are what the renderer set through gpu_tex_param (0 where it set nothing, which
|
|
# means GL's own default).
|
|
|
|
function gvk_filter(f: int) -> int {
|
|
if f == GL_NEAREST or f == GL_NEAREST_MIPMAP_NEAREST or f == GL_NEAREST_MIPMAP_LINEAR { return VK_FILTER_NEAREST }
|
|
return VK_FILTER_LINEAR
|
|
}
|
|
function gvk_address(wrap: int) -> int {
|
|
if wrap == GL_CLAMP_TO_EDGE { return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE }
|
|
if wrap == GL_CLAMP_TO_BORDER { return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER }
|
|
return VK_SAMPLER_ADDRESS_MODE_REPEAT
|
|
}
|
|
function gvk_compare_op(f: int) -> int {
|
|
if f == GL_LESS { return VK_COMPARE_OP_LESS }
|
|
if f == GL_EQUAL { return VK_COMPARE_OP_EQUAL }
|
|
if f == GL_ALWAYS { return VK_COMPARE_OP_ALWAYS }
|
|
return VK_COMPARE_OP_LESS_OR_EQUAL
|
|
}
|
|
# The texture mip bias DLSS needs. DLSS draws the scene at a fraction of the output resolution, so
|
|
# every texture picks its mip for THAT resolution - and then the upscaler has no detail left to
|
|
# reconstruct, which is what "blurry and muddy" is. NVIDIA's requirement is to bias the mip
|
|
# selection back toward the output resolution:
|
|
#
|
|
# bias = log2(renderWidth / displayWidth) - 1
|
|
#
|
|
# which is -2.0 at Performance and about -1.6 at Quality. It is applied ONLY while DLSS is live: a
|
|
# plain spatial upscale has no temporal accumulation to hide the aliasing a negative bias brings,
|
|
# so biasing there would trade blur for shimmer.
|
|
function gvk_mip_bias(render3d_st: mut Render3dState) -> float {
|
|
# R3D_NO_MIPBIAS=1 puts it back the way it was, so one build can be compared against itself
|
|
if r3d_env_has(render3d_st, "R3D_NO_MIPBIAS") { return 0.0 }
|
|
if not r3d_dlss_live(render3d_st) { return 0.0 }
|
|
let rw = r3d_dlss_render_w(render3d_st)
|
|
if rw <= 0 or gl_width() <= 0 or rw >= gl_width() { return 0.0 }
|
|
# log2 from the natural log the runtime has: log2(x) = ln(x) * 1/ln(2)
|
|
return Math.log(float(rw) / float(gl_width())) * 1.4426950408889634 - 1.0
|
|
}
|
|
# the sampler for texture tex's parameters, from the texture's own one-entry cache when they have
|
|
# not changed since it last asked - a draw asks for every texture it binds
|
|
function gvk_tex_sampler(render3d_st: mut Render3dState, tex: int, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
|
|
let bias = float_bits(gvk_mip_bias(render3d_st))
|
|
var sig = min_f * 31 + mag_f
|
|
sig = sig * 31 + wrap_s
|
|
sig = sig * 31 + wrap_t
|
|
sig = sig * 31 + compare
|
|
sig = sig * 31 + aniso
|
|
# the bias is part of what the sampler IS, so it has to invalidate this cache too - otherwise
|
|
# turning DLSS on mid-session keeps every sampler already made at the old bias
|
|
sig = (sig * 31 + bias) | 1
|
|
if tex > 0 and tex < len(render3d_st.gvk_tex_smp_sig) and render3d_st.gvk_tex_smp_sig[tex] == sig { return render3d_st.gvk_tex_smp[tex] }
|
|
let s = gvk_sampler(render3d_st, min_f, mag_f, wrap_s, wrap_t, compare, aniso)
|
|
if tex > 0 and tex < len(render3d_st.gvk_tex_smp_sig) { render3d_st.gvk_tex_smp_sig[tex] = sig; render3d_st.gvk_tex_smp[tex] = s }
|
|
return s
|
|
}
|
|
function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wrap_s: int, wrap_t: int, compare: int, aniso: int) -> long {
|
|
let bias = float_bits(gvk_mip_bias(render3d_st))
|
|
# looked up by the numbers themselves: a key string built on every call (and a texture read two
|
|
# ways in turn misses its own cache every time) was never given back
|
|
if render3d_st.gvk_smp_keys == null { render3d_st.gvk_smp_keys = new []int; render3d_st.gvk_smp = new []long }
|
|
let ks = render3d_st.gvk_smp_keys
|
|
var i = 0
|
|
while i < len(render3d_st.gvk_smp) {
|
|
let o = i * 7
|
|
if ks[o] == min_f and ks[o + 1] == mag_f and ks[o + 2] == wrap_s and ks[o + 3] == wrap_t and ks[o + 4] == compare and ks[o + 5] == aniso and ks[o + 6] == bias { return render3d_st.gvk_smp[i] }
|
|
i += 1
|
|
}
|
|
var mn = min_f
|
|
if mn == 0 { mn = GL_NEAREST_MIPMAP_LINEAR }
|
|
var mg = mag_f
|
|
if mg == 0 { mg = GL_LINEAR }
|
|
let mipmapped = mn == GL_LINEAR_MIPMAP_LINEAR or mn == GL_NEAREST_MIPMAP_LINEAR or mn == GL_LINEAR_MIPMAP_NEAREST or mn == GL_NEAREST_MIPMAP_NEAREST
|
|
let sci = bytes(VkSamplerCreateInfo_sizeof)
|
|
Vk.zero(sci, VkSamplerCreateInfo_sizeof)
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_sType, VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO)
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_magFilter, gvk_filter(mg))
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_minFilter, gvk_filter(mn))
|
|
if mn == GL_LINEAR_MIPMAP_LINEAR or mn == GL_NEAREST_MIPMAP_LINEAR { Vk.put_i32(sci, VkSamplerCreateInfo_mipmapMode, VK_SAMPLER_MIPMAP_MODE_LINEAR) }
|
|
else { Vk.put_i32(sci, VkSamplerCreateInfo_mipmapMode, VK_SAMPLER_MIPMAP_MODE_NEAREST) }
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_addressModeU, gvk_address(wrap_s))
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_addressModeV, gvk_address(wrap_t))
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_addressModeW, gvk_address(wrap_t))
|
|
# without mipmaps, a max LOD of 0.25 samples level 0 only (the spec's own recipe for GL_LINEAR)
|
|
if mipmapped { Vk.put_i32(sci, VkSamplerCreateInfo_maxLod, 0x447A0000) } else { Vk.put_i32(sci, VkSamplerCreateInfo_maxLod, 0x3E800000) }
|
|
if mipmapped { Vk.put_i32(sci, VkSamplerCreateInfo_mipLodBias, bias) }
|
|
if aniso > 0x3F800000 and mipmapped {
|
|
var a = aniso
|
|
if a > render3d_st.gvk_max_aniso { a = render3d_st.gvk_max_aniso }
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_anisotropyEnable, 1)
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_maxAnisotropy, a)
|
|
}
|
|
if compare != 0 {
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_compareEnable, 1)
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_compareOp, gvk_compare_op(compare))
|
|
}
|
|
Vk.put_i32(sci, VkSamplerCreateInfo_borderColor, VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE)
|
|
let out = bytes(8)
|
|
let zero: long = 0
|
|
render3d_st.gvk_mk_smp += 1
|
|
let r = Vk.create_sampler(render3d_st.gvk_dev, sci, null, out)
|
|
if r != VK_SUCCESS { gvk_fail(render3d_st, "vkCreateSampler", r); return zero }
|
|
let s = gvk_handle(out)
|
|
push(ks, min_f); push(ks, mag_f); push(ks, wrap_s); push(ks, wrap_t); push(ks, compare); push(ks, aniso); push(ks, bias)
|
|
push(render3d_st.gvk_smp, s)
|
|
return s
|
|
}
|
|
|
|
# ---- buffers ------------------------------------------------------------------------------
|
|
# A buffer handle (a mesh's vertices or indices, an instance buffer, the overlay's stream)
|
|
# indexes these lists. Every buffer can be read as vertices or as indices, so one handle serves
|
|
# whichever the renderer binds it as. While the backend comes up every buffer is host-visible and
|
|
# an upload maps and copies; staged device-local buffers arrive with the block allocator.
|
|
# Storage a buffer was moved off, or freed, while the frame that used it has not been submitted:
|
|
# destroyed at gvk_retire_flush, after the frame's work is done.
|
|
|
|
function gvk_buf_new(render3d_st: mut Render3dState) -> int {
|
|
let zero: long = 0
|
|
if render3d_st.gvk_buf == null {
|
|
render3d_st.gvk_buf = new []long; render3d_st.gvk_buf_mem = new []long; render3d_st.gvk_buf_size = new []int; render3d_st.gvk_buf_map = new []pointer
|
|
render3d_st.gvk_buf_used = new []int; render3d_st.gvk_retired_buf = new []long; render3d_st.gvk_retired_mem = new []long
|
|
# handle 0 is "no buffer", as it is on OpenGL
|
|
push(render3d_st.gvk_buf, zero); push(render3d_st.gvk_buf_mem, zero); push(render3d_st.gvk_buf_size, 0); push(render3d_st.gvk_buf_map, null); push(render3d_st.gvk_buf_used, 0)
|
|
}
|
|
# a handle a deleted buffer gave back first (gvk_buf_delete)
|
|
if render3d_st.gvk_buf_spare != null and len(render3d_st.gvk_buf_spare) > 0 {
|
|
let sp = render3d_st.gvk_buf_spare
|
|
let b = sp[len(sp) - 1]
|
|
List.pop(sp)
|
|
return b
|
|
}
|
|
push(render3d_st.gvk_buf, zero); push(render3d_st.gvk_buf_mem, zero); push(render3d_st.gvk_buf_size, 0); push(render3d_st.gvk_buf_map, null); push(render3d_st.gvk_buf_used, 0)
|
|
return len(render3d_st.gvk_buf) - 1
|
|
}
|
|
|
|
# a buffer done with for good (a mesh freed): its storage goes, and its handle is handed out again
|
|
# by gvk_buf_new. gvk_buf_release alone keeps the handle, for a caller that fills it again.
|
|
function gvk_buf_delete(render3d_st: mut Render3dState, b: int) -> void {
|
|
if b <= 0 or b >= len(render3d_st.gvk_buf) { return }
|
|
gvk_buf_release(render3d_st, b)
|
|
if render3d_st.gvk_buf_gpu != null and b < len(render3d_st.gvk_buf_gpu) { render3d_st.gvk_buf_gpu[b] = 0 }
|
|
if render3d_st.gvk_buf_spare == null { render3d_st.gvk_buf_spare = new []int }
|
|
let sp = render3d_st.gvk_buf_spare
|
|
for i in 0 .. len(sp) { if sp[i] == b { return } }
|
|
push(sp, b)
|
|
}
|
|
|
|
function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void {
|
|
if render3d_st.gvk_buf[b] == 0 { return }
|
|
let zero: long = 0
|
|
if gvk_buf_busy(render3d_st, b) {
|
|
# a draw recorded this frame (or the frame in flight) still reads it: destroy it once the frame has been submitted
|
|
push(render3d_st.gvk_retired_buf, render3d_st.gvk_buf[b]); push(render3d_st.gvk_retired_mem, render3d_st.gvk_buf_mem[b])
|
|
if render3d_st.gvk_retired_frame == null { render3d_st.gvk_retired_frame = new []int }
|
|
while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) - 1 { push(render3d_st.gvk_retired_frame, 0) }
|
|
push(render3d_st.gvk_retired_frame, render3d_st.gvk_buf_used[b])
|
|
} else {
|
|
render3d_st.gvk_mk_x_buf += 1
|
|
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null)
|
|
gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_buf_mem[b]))
|
|
}
|
|
render3d_st.gvk_buf[b] = zero; render3d_st.gvk_buf_mem[b] = zero; render3d_st.gvk_buf_size[b] = 0; render3d_st.gvk_buf_map[b] = null; render3d_st.gvk_buf_used[b] = 0
|
|
}
|
|
# read by a draw recorded this frame, or by the presented frame still on the GPU
|
|
function gvk_buf_busy(render3d_st: Render3dState, b: int) -> bool {
|
|
let u = render3d_st.gvk_buf_used[b]
|
|
return u == render3d_st.gvk_frame_no or (render3d_st.gvk_frame_pending and u == render3d_st.gvk_frame_pending_no)
|
|
}
|
|
|
|
# everything submitted is done: every retired buffer can go
|
|
function gvk_retire_flush(render3d_st: mut Render3dState) -> void { gvk_retire_upto(render3d_st, render3d_st.gvk_frame_no) }
|
|
|
|
# frame `done` and every frame before it are finished: storage last read by them can go, and what
|
|
# the frame being recorded read stays
|
|
function gvk_retire_upto(render3d_st: mut Render3dState, done: int) -> void {
|
|
if render3d_st.gvk_retired_buf == null { return }
|
|
if render3d_st.gvk_retired_frame == null { render3d_st.gvk_retired_frame = new []int }
|
|
while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) { push(render3d_st.gvk_retired_frame, 0) }
|
|
# compacted in place: three fresh lists a frame were never given back
|
|
var w = 0
|
|
for i in 0 .. len(render3d_st.gvk_retired_buf) {
|
|
let f = render3d_st.gvk_retired_frame[i]
|
|
if f > done {
|
|
render3d_st.gvk_retired_buf[w] = render3d_st.gvk_retired_buf[i]; render3d_st.gvk_retired_mem[w] = render3d_st.gvk_retired_mem[i]; render3d_st.gvk_retired_frame[w] = f
|
|
w += 1
|
|
} else {
|
|
render3d_st.gvk_mk_x_buf += 1
|
|
Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null)
|
|
gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i]))
|
|
}
|
|
}
|
|
let rb = render3d_st.gvk_retired_buf
|
|
let rm = render3d_st.gvk_retired_mem
|
|
let rf = render3d_st.gvk_retired_frame
|
|
while len(rb) > w { List.pop(rb) }
|
|
while len(rm) > w { List.pop(rm) }
|
|
while len(rf) > w { List.pop(rf) }
|
|
}
|
|
|
|
# A buffer a compute pass writes: a draw later in the same frame reads what the GPU put there,
|
|
# so it is never swapped for fresh storage because it was used this frame (gvk_buf_reserve).
|
|
function gvk_buf_gpu_owned(render3d_st: mut Render3dState, b: int) -> void {
|
|
if render3d_st.gvk_buf_gpu == null { render3d_st.gvk_buf_gpu = new []int }
|
|
while len(render3d_st.gvk_buf_gpu) <= b { push(render3d_st.gvk_buf_gpu, 0) }
|
|
render3d_st.gvk_buf_gpu[b] = 1
|
|
}
|
|
function gvk_buf_is_gpu(render3d_st: Render3dState, b: int) -> bool { return render3d_st.gvk_buf_gpu != null and b < len(render3d_st.gvk_buf_gpu) and render3d_st.gvk_buf_gpu[b] == 1 }
|
|
|
|
# Room for at least n bytes behind handle b; a buffer that is already big enough is kept, so a
|
|
# stream re-filled every frame allocates once.
|
|
function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool {
|
|
if b <= 0 or b >= len(render3d_st.gvk_buf) { return false }
|
|
# already big enough, and no draw this frame reads what is there: fill it in place
|
|
if render3d_st.gvk_buf[b] != 0 and render3d_st.gvk_buf_size[b] >= n and (not gvk_buf_busy(render3d_st, b) or gvk_buf_is_gpu(render3d_st, b)) { return true }
|
|
gvk_buf_release(render3d_st, b)
|
|
var size = n
|
|
if size < 64 { size = 64 }
|
|
let size_l: long = size
|
|
let bci = gvk_tmp(render3d_st, 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, size_l)
|
|
Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT)
|
|
Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE)
|
|
let out = gvk_tmp(render3d_st, 8)
|
|
render3d_st.gvk_mk_buf += 1
|
|
var r = Vk.create_buffer(render3d_st.gvk_dev, bci, null, out)
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateBuffer ({size} bytes)`, r) }
|
|
let buf = gvk_handle(out)
|
|
let req = gvk_tmp(render3d_st, VkMemoryRequirements_sizeof)
|
|
Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, buf, req)
|
|
let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, false)
|
|
let zero: long = 0
|
|
if ma == 0 { Vk.destroy_buffer(render3d_st.gvk_dev, buf, null); return false }
|
|
let mem: long = ma
|
|
r = Vk.bind_buffer_memory(render3d_st.gvk_dev, buf, gvk_mem_handle(render3d_st, ma), gvk_mem_offset(render3d_st, ma))
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkBindBufferMemory", r) }
|
|
render3d_st.gvk_buf[b] = buf; render3d_st.gvk_buf_mem[b] = mem; render3d_st.gvk_buf_size[b] = size; render3d_st.gvk_buf_map[b] = gvk_mem_ptr(render3d_st, ma)
|
|
return true
|
|
}
|
|
|
|
# glBufferData: the whole buffer, from data (or storage only when data is null)
|
|
function gvk_buf_upload(render3d_st: mut Render3dState, b: int, n: int, data: pointer) -> bool {
|
|
if not gvk_buf_reserve(render3d_st, b, n) { return false }
|
|
if data != null and n > 0 { mem_copy(render3d_st.gvk_buf_map[b], data, n) }
|
|
return true
|
|
}
|