# 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, render3d_st.gvk_ac, 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, render3d_st.gvk_ac) render3d_st.gvk_mk_x_mem += 1 Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, render3d_st.gvk_ac) 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, render3d_st.gvk_ac, out) # the create infos are read by the call they are handed to and go straight after (a texture # made in play once left four of them behind) free(ici) if r != VK_SUCCESS { free(out) 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) free(req) 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, render3d_st.gvk_ac) free(out) 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 { free(out) 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, render3d_st.gvk_ac, out) free(vci) let view = gvk_handle(out) free(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] = view; 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, render3d_st.gvk_ac) render3d_st.gvk_mk_x_img += 1 Vk.destroy_image(render3d_st.gvk_dev, old_image, render3d_st.gvk_ac) 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], render3d_st.gvk_ac) render3d_st.gvk_mk_x_img += 1 Vk.destroy_image(render3d_st.gvk_dev, render3d_st.gvk_tex_image[tex], render3d_st.gvk_ac) 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], render3d_st.gvk_ac) } 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, render3d_st.gvk_ac, 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], render3d_st.gvk_ac) 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], render3d_st.gvk_ac) 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, render3d_st.gvk_ac, 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, render3d_st.gvk_ac); 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) gvk_prime(render3d_st, b, buf) return true } # MoltenVK makes a buffer's Metal buffer the first time a command uses it, so one written through # its mapping and first drawn a thousand frames later allocated then, in play. Each new buffer is # read once (4 bytes, copied out) at the start of the next frame's commands instead. const GVK_PRIME_MAX: int = 16384 function gvk_prime(render3d_st: mut Render3dState, b: int, buf: long) -> void { if Os.platform() != "macos" { return } if render3d_st.gvk_prime_b == null { render3d_st.gvk_prime_b = words(GVK_PRIME_MAX) render3d_st.gvk_prime_h = new []long let hs = render3d_st.gvk_prime_h let none: long = 0 for i in 0 .. GVK_PRIME_MAX { push(hs, none) } } if render3d_st.gvk_prime_n >= GVK_PRIME_MAX { return } render3d_st.gvk_prime_b[render3d_st.gvk_prime_n] = b render3d_st.gvk_prime_h[render3d_st.gvk_prime_n] = buf render3d_st.gvk_prime_n += 1 } # In a command buffer of their own, submitted and waited for at once: recorded into the frame's, a # buffer released later in that frame (a guest loading its models) was destroyed before the copy ran. function gvk_prime_submit(render3d_st: mut Render3dState) -> void { if render3d_st.gvk_prime_n == 0 { return } let cb = gvk_once_begin(render3d_st) gvk_prime_flush(render3d_st, cb) gvk_once_end(render3d_st, cb) } function gvk_prime_flush(render3d_st: mut Render3dState, cb: pointer) -> void { if render3d_st.gvk_prime_n == 0 { return } if render3d_st.gvk_prime_dst == 0 and not gvk_prime_target(render3d_st) { render3d_st.gvk_prime_n = 0 return } let region = gvk_tmp(render3d_st, VkBufferCopy_sizeof) Vk.zero(region, VkBufferCopy_sizeof) let four: long = 4 Vk.put_i64(region, VkBufferCopy_size, four) for i in 0 .. render3d_st.gvk_prime_n { let b = render3d_st.gvk_prime_b[i] let h = render3d_st.gvk_prime_h[i] # released and made again, or gone, since: that one is not this buffer any more if b > 0 and b < len(render3d_st.gvk_buf) and render3d_st.gvk_buf[b] == h { Vk.cmd_copy_buffer(cb, h, render3d_st.gvk_prime_dst, 1, region) } } render3d_st.gvk_prime_n = 0 } function gvk_prime_target(render3d_st: mut Render3dState) -> bool { let size: long = 64 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, VK_BUFFER_USAGE_TRANSFER_DST_BIT) Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE) let out = gvk_tmp(render3d_st, 8) if Vk.create_buffer(render3d_st.gvk_dev, bci, render3d_st.gvk_ac, out) != VK_SUCCESS { return false } 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_DEVICE_LOCAL_BIT, false) if ma == 0 { Vk.destroy_buffer(render3d_st.gvk_dev, buf, render3d_st.gvk_ac) return false } Vk.bind_buffer_memory(render3d_st.gvk_dev, buf, gvk_mem_handle(render3d_st, ma), gvk_mem_offset(render3d_st, ma)) render3d_st.gvk_prime_dst = buf let m: long = ma render3d_st.gvk_prime_mem = m 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 }