gvk_swap_make freed none of what it made - the surface caps, the count, the format and present-mode lists, the create info, the handle out - and replaced gvk_swap_images without freeing it: a few hundred bytes every time the window changed size, went to or from Retina, or the chain came back suboptimal. Each is now freed on every way out (the minimised return and the failed create included), and the old image list before the new one is made. A windowed memory walk built against it (main 5ee600d2, 300 s, the autopilot opening screens every 6 s): footprint 2816 MB at 120 s, 2818 MB at 300 s; the fence judged 0 frames kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2078 lines
122 KiB
Text
2078 lines
122 KiB
Text
# gpu_vk_draw.ludic — the Vulkan backend's drawing: programs as manifest variants, pipelines
|
|
# built from what OpenGL decides at the draw, uniform blocks and descriptor sets per draw, and the
|
|
# frame's passes with dynamic rendering, through to the present and the screenshot.
|
|
#
|
|
# It reads render3d's own records - the SPIR-V manifest (gpu_manifest.ludic), a Mesh's recorded
|
|
# vertex layout, gpu.ludic's texture and framebuffer records - so it is compiled only inside
|
|
# render3d, after gpu_vk.ludic and gpu_vk_res.ludic.
|
|
|
|
# ---- programs -----------------------------------------------------------------------------
|
|
# A program handle is a manifest variant on Vulkan: its two SPIR-V modules, a descriptor set
|
|
# layout (binding 0 the vertex stage's uniform block, 1 the fragment stage's, the samplers at
|
|
# the manifest's bindings from 2) and the pipeline layout over it. Programs are never compiled
|
|
# here; one whose variant is not in the manifest cannot draw, and says so once.
|
|
|
|
function gvk_read_spv(render3d_st: mut Render3dState, path: string) -> bytes {
|
|
let f = file_open(path, "rb")
|
|
if f == null { return null }
|
|
file_seek(f, 0, 2)
|
|
let n = file_tell(f)
|
|
file_seek(f, 0, 0)
|
|
let b = bytes(n + 4)
|
|
file_read(f, b, n)
|
|
file_close(f)
|
|
render3d_st.gvk_spv_len = n
|
|
return b
|
|
}
|
|
|
|
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
|
|
function gvk_module(render3d_st: mut Render3dState, path: string) -> long {
|
|
let zero: long = 0
|
|
let spv = gvk_read_spv(render3d_st, path)
|
|
if spv == null { print(`r3d: vulkan: no SPIR-V at {path}`); return zero }
|
|
let smci = bytes(VkShaderModuleCreateInfo_sizeof)
|
|
Vk.zero(smci, VkShaderModuleCreateInfo_sizeof)
|
|
Vk.put_i32(smci, VkShaderModuleCreateInfo_sType, VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO)
|
|
let code_size: long = render3d_st.gvk_spv_len
|
|
Vk.put_i64(smci, VkShaderModuleCreateInfo_codeSize, code_size)
|
|
Vk.put_ptr(smci, VkShaderModuleCreateInfo_pCode, spv)
|
|
let out = bytes(8)
|
|
let r = Vk.create_shader_module(render3d_st.gvk_dev, smci, render3d_st.gvk_ac, out)
|
|
let module = gvk_handle(out)
|
|
# the driver has its own copy of the code: what was read and made for the call goes
|
|
free(spv); free(smci); free(out)
|
|
if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateShaderModule {path}`, r); return zero }
|
|
return module
|
|
}
|
|
|
|
# The Vulkan side of a program handle the renderer already made (gpu_program): the handle's
|
|
# manifest key finds the variant. Returns false when there is no such variant.
|
|
# a program whose first stage is a mesh shader (its first file is *.mesh): its pipeline has no vertex
|
|
# input, and its first stage's bindings are the mesh stage's
|
|
function gvk_stage_first(render3d_st: Render3dState, p: int) -> int {
|
|
if render3d_st.gvk_prog_mesh != null and p < len(render3d_st.gvk_prog_mesh) and render3d_st.gvk_prog_mesh[p] == 1 { return VK_SHADER_STAGE_MESH_BIT_EXT }
|
|
return VK_SHADER_STAGE_VERTEX_BIT
|
|
}
|
|
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
|
|
function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_dir: string) -> bool {
|
|
let zero: long = 0
|
|
if render3d_st.gvk_prog_var == null {
|
|
render3d_st.gvk_prog_var = new []GpuVariant; render3d_st.gvk_prog_vs = new []long; render3d_st.gvk_prog_fs = new []long
|
|
render3d_st.gvk_prog_dsl = new []long; render3d_st.gvk_prog_layout = new []long; render3d_st.gvk_prog_mesh = new []int
|
|
}
|
|
while len(render3d_st.gvk_prog_var) <= p {
|
|
push(render3d_st.gvk_prog_var, null); push(render3d_st.gvk_prog_vs, zero); push(render3d_st.gvk_prog_fs, zero); push(render3d_st.gvk_prog_dsl, zero); push(render3d_st.gvk_prog_layout, zero); push(render3d_st.gvk_prog_mesh, 0)
|
|
}
|
|
let parts = Text.split(key, "|")
|
|
var defs = ""
|
|
if len(parts) > 2 { defs = parts[2] }
|
|
let v = gpu_variant_find_key(render3d_st, parts[0], parts[1], defs)
|
|
if v == null { print(`r3d: vulkan: no SPIR-V variant for {key}`); return false }
|
|
for q in 0 .. len(parts) { free(parts[q]) }
|
|
free(parts)
|
|
let vpath = `{spv_dir}/{v.id}.vert.spv`
|
|
let vs = gvk_module(render3d_st, vpath)
|
|
free(vpath)
|
|
if Text.ends_with(v.vs, ".mesh") { render3d_st.gvk_prog_mesh[p] = 1 } else { render3d_st.gvk_prog_mesh[p] = 0 }
|
|
let fpath = `{spv_dir}/{v.id}.frag.spv`
|
|
let fs = gvk_module(render3d_st, fpath)
|
|
free(fpath)
|
|
if vs == 0 or fs == 0 { return false }
|
|
|
|
let nt = len(v.t_name)
|
|
var nb = nt
|
|
if v.vblock >= 0 { nb += 1 }
|
|
if v.fblock >= 0 { nb += 1 }
|
|
let bw = VkDescriptorSetLayoutBinding_sizeof
|
|
let binds = bytes(bw * (nb + 1))
|
|
Vk.zero(binds, bw * (nb + 1))
|
|
var k = 0
|
|
if v.vblock >= 0 {
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, 0)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(render3d_st, p))
|
|
k += 1
|
|
}
|
|
if v.fblock >= 0 {
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, 1)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, VK_SHADER_STAGE_FRAGMENT_BIT)
|
|
k += 1
|
|
}
|
|
for t in 0 .. nt {
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, v.t_bind[t])
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, gvk_stage_first(render3d_st, p) | VK_SHADER_STAGE_FRAGMENT_BIT)
|
|
k += 1
|
|
}
|
|
let dslci = bytes(VkDescriptorSetLayoutCreateInfo_sizeof)
|
|
Vk.zero(dslci, VkDescriptorSetLayoutCreateInfo_sizeof)
|
|
Vk.put_i32(dslci, VkDescriptorSetLayoutCreateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)
|
|
Vk.put_i32(dslci, VkDescriptorSetLayoutCreateInfo_bindingCount, nb)
|
|
Vk.put_ptr(dslci, VkDescriptorSetLayoutCreateInfo_pBindings, binds)
|
|
let dsl = bytes(8)
|
|
var r = Vk.create_descriptor_set_layout(render3d_st.gvk_dev, dslci, render3d_st.gvk_ac, dsl)
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateDescriptorSetLayout for {key}`, r) }
|
|
let plci = bytes(VkPipelineLayoutCreateInfo_sizeof)
|
|
Vk.zero(plci, VkPipelineLayoutCreateInfo_sizeof)
|
|
Vk.put_i32(plci, VkPipelineLayoutCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO)
|
|
Vk.put_i32(plci, VkPipelineLayoutCreateInfo_setLayoutCount, 1)
|
|
Vk.put_ptr(plci, VkPipelineLayoutCreateInfo_pSetLayouts, dsl)
|
|
let out = bytes(8)
|
|
r = Vk.create_pipeline_layout(render3d_st.gvk_dev, plci, render3d_st.gvk_ac, out)
|
|
let dsl_h = Vk.get_i64(dsl, 0)
|
|
let lay_h = gvk_handle(out)
|
|
free(binds); free(dslci); free(dsl); free(plci); free(out)
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreatePipelineLayout for {key}`, r) }
|
|
render3d_st.gvk_prog_var[p] = v; render3d_st.gvk_prog_vs[p] = vs; render3d_st.gvk_prog_fs[p] = fs
|
|
render3d_st.gvk_prog_dsl[p] = dsl_h; render3d_st.gvk_prog_layout[p] = lay_h
|
|
return true
|
|
}
|
|
|
|
# ---- compute ------------------------------------------------------------------------------
|
|
# A compute program is <name>.comp.spv beside the manifest, with bindings fixed by convention:
|
|
# 0 the parameter block (a uniform buffer, copied into the frame's ring at the dispatch),
|
|
# 1 .. n storage buffers, then n+1 .. n+m sampled textures (linear, clamped). Handles start at 1.
|
|
|
|
function gvk_compute_new(render3d_st: mut Render3dState, name: string, n_bufs: int) -> int { return gvk_compute_new_tex(render3d_st, name, n_bufs, 0) }
|
|
|
|
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
|
|
function gvk_compute_new_tex(render3d_st: mut Render3dState, name: string, n_bufs: int, n_tex: int) -> int {
|
|
let zero: long = 0
|
|
if render3d_st.gvk_cp_pipe == null {
|
|
render3d_st.gvk_cp_pipe = new []long; render3d_st.gvk_cp_layout = new []long; render3d_st.gvk_cp_dsl = new []long; render3d_st.gvk_cp_nbuf = new []int
|
|
render3d_st.gvk_cp_ntex = new []int
|
|
push(render3d_st.gvk_cp_pipe, zero); push(render3d_st.gvk_cp_layout, zero); push(render3d_st.gvk_cp_dsl, zero); push(render3d_st.gvk_cp_nbuf, 0); push(render3d_st.gvk_cp_ntex, 0)
|
|
}
|
|
let cpath = `{render3d_st.gvk_spv_dir}/{name}.comp.spv`
|
|
let module = gvk_module(render3d_st, cpath)
|
|
free(cpath)
|
|
if module == 0 { return 0 }
|
|
let nb = n_bufs + 1 + n_tex
|
|
let bw = VkDescriptorSetLayoutBinding_sizeof
|
|
let binds = bytes(bw * nb)
|
|
Vk.zero(binds, bw * nb)
|
|
for k in 0 .. nb {
|
|
var kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
|
|
if k == 0 { kind = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER }
|
|
if k > n_bufs { kind = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER }
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_binding, k)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorType, kind)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_descriptorCount, 1)
|
|
Vk.put_i32(binds, k * bw + VkDescriptorSetLayoutBinding_stageFlags, VK_SHADER_STAGE_COMPUTE_BIT)
|
|
}
|
|
let dslci = bytes(VkDescriptorSetLayoutCreateInfo_sizeof)
|
|
Vk.zero(dslci, VkDescriptorSetLayoutCreateInfo_sizeof)
|
|
Vk.put_i32(dslci, VkDescriptorSetLayoutCreateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)
|
|
Vk.put_i32(dslci, VkDescriptorSetLayoutCreateInfo_bindingCount, nb)
|
|
Vk.put_ptr(dslci, VkDescriptorSetLayoutCreateInfo_pBindings, binds)
|
|
let dsl = bytes(8)
|
|
var r = Vk.create_descriptor_set_layout(render3d_st.gvk_dev, dslci, render3d_st.gvk_ac, dsl)
|
|
if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateDescriptorSetLayout for compute {name}`, r); return 0 }
|
|
let plci = bytes(VkPipelineLayoutCreateInfo_sizeof)
|
|
Vk.zero(plci, VkPipelineLayoutCreateInfo_sizeof)
|
|
Vk.put_i32(plci, VkPipelineLayoutCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO)
|
|
Vk.put_i32(plci, VkPipelineLayoutCreateInfo_setLayoutCount, 1)
|
|
Vk.put_ptr(plci, VkPipelineLayoutCreateInfo_pSetLayouts, dsl)
|
|
let out = bytes(8)
|
|
r = Vk.create_pipeline_layout(render3d_st.gvk_dev, plci, render3d_st.gvk_ac, out)
|
|
if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreatePipelineLayout for compute {name}`, r); return 0 }
|
|
let layout = gvk_handle(out)
|
|
let cpci = bytes(VkComputePipelineCreateInfo_sizeof)
|
|
Vk.zero(cpci, VkComputePipelineCreateInfo_sizeof)
|
|
Vk.put_i32(cpci, VkComputePipelineCreateInfo_sType, VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO)
|
|
let so = VkComputePipelineCreateInfo_stage
|
|
Vk.put_i32(cpci, so + VkPipelineShaderStageCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
|
|
Vk.put_i32(cpci, so + VkPipelineShaderStageCreateInfo_stage, VK_SHADER_STAGE_COMPUTE_BIT)
|
|
Vk.put_i64(cpci, so + VkPipelineShaderStageCreateInfo_module, module)
|
|
Vk.put_ptr(cpci, so + VkPipelineShaderStageCreateInfo_pName, "main")
|
|
Vk.put_i64(cpci, VkComputePipelineCreateInfo_layout, layout)
|
|
r = Vk.create_compute_pipelines(render3d_st.gvk_dev, zero, 1, cpci, render3d_st.gvk_ac, out)
|
|
let pipe_h = gvk_handle(out)
|
|
let dsl_h = Vk.get_i64(dsl, 0)
|
|
free(binds); free(dslci); free(dsl); free(plci); free(cpci); free(out)
|
|
if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateComputePipelines {name}`, r); return 0 }
|
|
push(render3d_st.gvk_cp_pipe, pipe_h); push(render3d_st.gvk_cp_layout, layout); push(render3d_st.gvk_cp_dsl, dsl_h); push(render3d_st.gvk_cp_nbuf, n_bufs)
|
|
push(render3d_st.gvk_cp_ntex, n_tex)
|
|
return len(render3d_st.gvk_cp_pipe) - 1
|
|
}
|
|
|
|
# Record a dispatch of compute program c into the frame, between passes: params (n_params
|
|
# bytes, std140) as binding 0 and bufs[0 .. n) as bindings 1 .. n.
|
|
function gvk_dispatch(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, gx: int, gy: int, gz: int) -> void { gvk_dispatch_tex(render3d_st, c, params, n_params, bufs, null, gx, gy, gz) }
|
|
|
|
# ... and texs[0 .. m) as the sampled textures after the buffers. What it writes is made visible to
|
|
# the indirect draws, vertex attributes and shaders that follow (Metal's own hazard tracking was all
|
|
# that ordered the tree culling's output before this)
|
|
function gvk_dispatch_tex(render3d_st: mut Render3dState, c: int, params: pointer, n_params: int, bufs: words, texs: words, gx: int, gy: int, gz: int) -> void {
|
|
let zero: long = 0
|
|
if render3d_st.gvk_cp_pipe == null or c <= 0 or c >= len(render3d_st.gvk_cp_pipe) { return }
|
|
gvk_pass_end(render3d_st)
|
|
let cb = gvk_frame_cb(render3d_st)
|
|
let nb = render3d_st.gvk_cp_nbuf[c]
|
|
var nt = 0
|
|
if render3d_st.gvk_cp_ntex != null and c < len(render3d_st.gvk_cp_ntex) { nt = render3d_st.gvk_cp_ntex[c] }
|
|
let dsai = gvk_tmp(render3d_st, VkDescriptorSetAllocateInfo_sizeof)
|
|
Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
|
|
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO)
|
|
Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, render3d_st.gvk_dpool)
|
|
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1)
|
|
let layouts = gvk_tmp(render3d_st, 8)
|
|
Vk.put_i64(layouts, 0, render3d_st.gvk_cp_dsl[c])
|
|
Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts)
|
|
let sets = gvk_tmp(render3d_st, 8)
|
|
render3d_st.gvk_mk_set += 1
|
|
let r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets)
|
|
if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (compute)", r); return }
|
|
let set = Vk.get_i64(sets, 0)
|
|
let at = gvk_ring_put(render3d_st, params, n_params)
|
|
if at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return }
|
|
let ww = VkWriteDescriptorSet_sizeof
|
|
let bw = VkDescriptorBufferInfo_sizeof
|
|
let iw = VkDescriptorImageInfo_sizeof
|
|
let writes = gvk_tmp(render3d_st, ww * (nb + 1 + nt))
|
|
Vk.zero(writes, ww * (nb + 1 + nt))
|
|
let infos = gvk_tmp(render3d_st, bw * (nb + 1))
|
|
Vk.zero(infos, bw * (nb + 1))
|
|
let iis = gvk_tmp(render3d_st, iw * (nt + 1))
|
|
Vk.zero(iis, iw * (nt + 1))
|
|
for k in 0 .. nb + 1 {
|
|
var buf = render3d_st.gvk_ring_buf
|
|
var off: long = 0
|
|
var range: long = 0
|
|
var kind = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER
|
|
if k == 0 { off = at; range = n_params; kind = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER }
|
|
else { buf = bufs[k - 1]; range = render3d_st.gvk_buf_size[buf]; render3d_st.gvk_buf_used[buf] = render3d_st.gvk_frame_no }
|
|
Vk.put_i64(infos, k * bw + VkDescriptorBufferInfo_buffer, render3d_st.gvk_buf[buf])
|
|
Vk.put_i64(infos, k * bw + VkDescriptorBufferInfo_offset, off)
|
|
Vk.put_i64(infos, k * bw + VkDescriptorBufferInfo_range, range)
|
|
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
|
|
Vk.put_i64(writes, k * ww + VkWriteDescriptorSet_dstSet, set)
|
|
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_dstBinding, k)
|
|
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_descriptorCount, 1)
|
|
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_descriptorType, kind)
|
|
Vk.put_ptr(writes, k * ww + VkWriteDescriptorSet_pBufferInfo, mem_off(infos, k * bw))
|
|
}
|
|
let smp = gvk_sampler(render3d_st, GL_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, 0)
|
|
for t in 0 .. nt {
|
|
var tex = render3d_st.gvk_white
|
|
if texs != null and t < len(texs) and texs[t] > 0 { tex = texs[t] }
|
|
Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_sampler, smp)
|
|
Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_imageView, render3d_st.gvk_tex_view[tex])
|
|
Vk.put_i32(iis, t * iw + VkDescriptorImageInfo_imageLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
let k = nb + 1 + t
|
|
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
|
|
Vk.put_i64(writes, k * ww + VkWriteDescriptorSet_dstSet, set)
|
|
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_dstBinding, k)
|
|
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_descriptorCount, 1)
|
|
Vk.put_i32(writes, k * ww + VkWriteDescriptorSet_descriptorType, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER)
|
|
Vk.put_ptr(writes, k * ww + VkWriteDescriptorSet_pImageInfo, mem_off(iis, t * iw))
|
|
}
|
|
Vk.update_descriptor_sets(render3d_st.gvk_dev, nb + 1 + nt, writes, 0, null)
|
|
Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_pipe[c])
|
|
Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_COMPUTE, render3d_st.gvk_cp_layout[c], 0, 1, sets, 0, null)
|
|
Vk.cmd_dispatch(cb, gx, gy, gz)
|
|
let mb = gvk_tmp(render3d_st, VkMemoryBarrier_sizeof)
|
|
Vk.zero(mb, VkMemoryBarrier_sizeof)
|
|
Vk.put_i32(mb, VkMemoryBarrier_sType, VK_STRUCTURE_TYPE_MEMORY_BARRIER)
|
|
Vk.put_i32(mb, VkMemoryBarrier_srcAccessMask, VK_ACCESS_SHADER_WRITE_BIT)
|
|
Vk.put_i32(mb, VkMemoryBarrier_dstAccessMask, VK_ACCESS_INDIRECT_COMMAND_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT)
|
|
Vk.cmd_pipeline_barrier(cb, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, mb, 0, null, 0, null)
|
|
}
|
|
|
|
# R3D_VK_PROBE=1: one dispatch over a GPU-owned buffer, read back - the compute path end to end
|
|
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
|
|
function gvk_compute_probe(render3d_st: mut Render3dState) -> void {
|
|
let c = gvk_compute_new(render3d_st, "probe", 1)
|
|
if c == 0 { print("r3d: vulkan compute probe: FAILED (no program)"); return }
|
|
let n = 1000
|
|
let b = gvk_buf_new(render3d_st)
|
|
let data = bytes(n * 4)
|
|
for i in 0 .. n { Vk.put_i32(data, i * 4, float_bits(float(i))) }
|
|
gvk_buf_upload(render3d_st, b, n * 4, data)
|
|
gvk_buf_gpu_owned(render3d_st, b)
|
|
let pr = bytes(8)
|
|
Vk.put_i32(pr, 0, n)
|
|
Vk.put_i32(pr, 4, float_bits(3.0))
|
|
let bufs = words(1)
|
|
bufs[0] = b
|
|
gvk_dispatch(render3d_st, c, pr, 8, bufs, (n + 63) / 64, 1, 1)
|
|
gvk_flush(render3d_st)
|
|
var bad = 0
|
|
let mp = render3d_st.gvk_buf_map[b]
|
|
for i in 0 .. n { if float_from_bits(Vk.get_i32(mp, i * 4)) != float(i * 3) { bad += 1 } }
|
|
if bad == 0 { print(`r3d: vulkan compute probe OK ({n} values)`) } else { print(`r3d: vulkan compute probe: FAILED ({bad} of {n} wrong)`) }
|
|
}
|
|
|
|
# ---- pipelines ----------------------------------------------------------------------------
|
|
# A pipeline is everything OpenGL decides at the draw: the program, the vertex layout the mesh
|
|
# recorded, the render state the renderer set, and the formats and sample count of the pass it
|
|
# draws into. Each distinct combination is built once, the first time it is drawn.
|
|
property GvkState {
|
|
depth_test: int = 0,
|
|
depth_write: int = 1,
|
|
depth_func: int = 0x0201, # GL_LESS
|
|
blend: int = 0,
|
|
blend_src: int = 1, # GL_ONE
|
|
blend_dst: int = 0, # GL_ZERO
|
|
cull: int = 0,
|
|
cull_face: int = 0x0405, # GL_BACK
|
|
color_write: int = 1,
|
|
a2c: int = 0,
|
|
bias: int = 0,
|
|
bias_factor: int = 0, # float bits
|
|
bias_units: int = 0, # float bits
|
|
wireframe: int = 0
|
|
}
|
|
|
|
# does the mesh leave shader input `loc` unfed (no attribute recorded there)?
|
|
function gvk_input_unfed(m: Mesh, loc: int) -> bool {
|
|
if m == null or m.attrs == null or loc < 0 or loc >= m.n_attrs { return true }
|
|
return m.attrs[loc * GPU_ATTR_W + 1] == 0
|
|
}
|
|
# the float format a zero-fed shader input reads, from its manifest type
|
|
function gvk_input_format(t: string) -> int {
|
|
if t == "float" { return VK_FORMAT_R32_SFLOAT }
|
|
if t == "vec2" { return VK_FORMAT_R32G32_SFLOAT }
|
|
if t == "vec3" { return VK_FORMAT_R32G32B32_SFLOAT }
|
|
return VK_FORMAT_R32G32B32A32_SFLOAT
|
|
}
|
|
# 64 KB of zeros, the buffer every unfed input reads (one vec4 per instance, up to 4096)
|
|
@alloc_ok("the one buffer of zeros every unfed input reads: made once")
|
|
function gvk_zero_vbuf_get(render3d_st: mut Render3dState) -> int {
|
|
if render3d_st.gvk_zero_vbuf == 0 {
|
|
render3d_st.gvk_zero_vbuf = gvk_buf_new(render3d_st)
|
|
let z = bytes(65536)
|
|
Vk.zero(z, 65536)
|
|
gvk_buf_upload(render3d_st, render3d_st.gvk_zero_vbuf, 65536, z)
|
|
free(z)
|
|
}
|
|
return render3d_st.gvk_zero_vbuf
|
|
}
|
|
|
|
function gvk_attr_format(comps: int, type: int, normalized: int) -> int {
|
|
if type == GPU_U8 {
|
|
if normalized == 1 { if comps == 4 { return VK_FORMAT_R8G8B8A8_UNORM }; if comps == 3 { return VK_FORMAT_R8G8B8_UNORM }; if comps == 2 { return VK_FORMAT_R8G8_UNORM }; return VK_FORMAT_R8_UNORM }
|
|
# not normalised, read by a float input (a_joints is a vec4): OpenGL converts the integer to a
|
|
# float, and Vulkan's form of that is USCALED - a UINT format against a float input is invalid
|
|
if comps == 4 { return VK_FORMAT_R8G8B8A8_USCALED }; if comps == 2 { return VK_FORMAT_R8G8_USCALED }; return VK_FORMAT_R8_USCALED
|
|
}
|
|
if type == GPU_U16 {
|
|
if normalized == 1 { if comps == 4 { return VK_FORMAT_R16G16B16A16_UNORM }; if comps == 2 { return VK_FORMAT_R16G16_UNORM }; return VK_FORMAT_R16_UNORM }
|
|
if comps == 4 { return VK_FORMAT_R16G16B16A16_USCALED }; if comps == 2 { return VK_FORMAT_R16G16_USCALED }; return VK_FORMAT_R16_USCALED
|
|
}
|
|
if comps == 4 { return VK_FORMAT_R32G32B32A32_SFLOAT }
|
|
if comps == 3 { return VK_FORMAT_R32G32B32_SFLOAT }
|
|
if comps == 2 { return VK_FORMAT_R32G32_SFLOAT }
|
|
return VK_FORMAT_R32_SFLOAT
|
|
}
|
|
function gvk_blend_factor(f: int) -> int {
|
|
if f == GL_ONE { return VK_BLEND_FACTOR_ONE }
|
|
if f == GL_SRC_ALPHA { return VK_BLEND_FACTOR_SRC_ALPHA }
|
|
if f == GL_ONE_MINUS_SRC_ALPHA { return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA }
|
|
if f == GL_DST_ALPHA { return VK_BLEND_FACTOR_DST_ALPHA }
|
|
if f == GL_ONE_MINUS_DST_ALPHA { return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA }
|
|
if f == GL_SRC_COLOR { return VK_BLEND_FACTOR_SRC_COLOR }
|
|
if f == GL_ONE_MINUS_SRC_COLOR { return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR }
|
|
return VK_BLEND_FACTOR_ZERO
|
|
}
|
|
function gvk_depth_op(f: int) -> int {
|
|
if f == GL_LEQUAL { return VK_COMPARE_OP_LESS_OR_EQUAL }
|
|
if f == GL_EQUAL { return VK_COMPARE_OP_EQUAL }
|
|
if f == GL_ALWAYS { return VK_COMPARE_OP_ALWAYS }
|
|
if f == GL_GREATER { return VK_COMPARE_OP_GREATER }
|
|
if f == GL_GEQUAL { return VK_COMPARE_OP_GREATER_OR_EQUAL }
|
|
if f == GL_NEVER { return VK_COMPARE_OP_NEVER }
|
|
if f == GL_NOTEQUAL { return VK_COMPARE_OP_NOT_EQUAL }
|
|
return VK_COMPARE_OP_LESS
|
|
}
|
|
# the vertex layout a mesh recorded (gpu.ludic's attrs), as part of a pipeline key
|
|
# Buffers are named by the order they are first read in, not by handle: a scatter mesh re-pointed
|
|
# at another instance buffer keeps its layout, and so its pipeline.
|
|
function gvk_layout_key(m: Mesh) -> string {
|
|
if m == null or m.attrs == null { return "none"[0 .. 4] } # a copy: the caller frees what it gets
|
|
var k: string = null # each step frees the key it grew from
|
|
let seen = words(GPU_MAX_ATTRS)
|
|
var ns = 0
|
|
for i in 0 .. m.n_attrs {
|
|
let o = i * GPU_ATTR_W
|
|
if m.attrs[o + 1] == 0 { continue }
|
|
var bi = -1
|
|
for q in 0 .. ns { if seen[q] == m.attrs[o] and bi < 0 { bi = q } }
|
|
if bi < 0 { bi = ns; seen[ns] = m.attrs[o]; ns += 1 }
|
|
let part = `{i}:{bi}:{m.attrs[o + 1]}:{m.attrs[o + 2]}:{m.attrs[o + 3]}:{m.attrs[o + 4]}:{m.attrs[o + 5]}:{m.attrs[o + 6]};`
|
|
if k == null { k = part } else {
|
|
let nk = k + part
|
|
free(k)
|
|
free(part)
|
|
k = nk
|
|
}
|
|
}
|
|
free(seen)
|
|
if k == null { return ""[0 .. 0] }
|
|
return k
|
|
}
|
|
|
|
# The pipeline for program p drawing mesh m (null for a draw without vertex input, like the
|
|
# full-screen triangle) with state st into a pass of n_color colour attachments of format
|
|
# color_fmt, a depth attachment of depth_fmt (VK_FORMAT_UNDEFINED for none) at `samples`.
|
|
@creates(Pipeline)
|
|
function gvk_pipeline(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int) -> long {
|
|
let zero: long = 0
|
|
let v = render3d_st.gvk_prog_var[p]
|
|
if v == null { return zero }
|
|
let lkey = gvk_layout_key(m)
|
|
let key = `{p}|{lkey}|{st.depth_test},{st.depth_write},{st.depth_func},{st.blend},{st.blend_src},{st.blend_dst},{st.cull},{st.cull_face},{st.color_write},{st.a2c},{st.bias},{st.bias_factor},{st.bias_units},{st.wireframe}|{n_color},{color_fmt},{depth_fmt},{samples}`
|
|
free(lkey)
|
|
if render3d_st.gvk_pipe_keys == null { render3d_st.gvk_pipe_keys = new []string; render3d_st.gvk_pipe = new []long; render3d_st.gvk_pipe_bufs = words(GPU_MAX_VBUFS) }
|
|
for i in 0 .. len(render3d_st.gvk_pipe_keys) { if render3d_st.gvk_pipe_keys[i] == key { return render3d_st.gvk_pipe[i] } }
|
|
|
|
let ss = VkPipelineShaderStageCreateInfo_sizeof
|
|
let stages = bytes(ss * 2)
|
|
Vk.zero(stages, ss * 2)
|
|
Vk.put_i32(stages, VkPipelineShaderStageCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
|
|
Vk.put_i32(stages, VkPipelineShaderStageCreateInfo_stage, gvk_stage_first(render3d_st, p))
|
|
Vk.put_i64(stages, VkPipelineShaderStageCreateInfo_module, render3d_st.gvk_prog_vs[p])
|
|
Vk.put_ptr(stages, VkPipelineShaderStageCreateInfo_pName, "main")
|
|
Vk.put_i32(stages, ss + VkPipelineShaderStageCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO)
|
|
Vk.put_i32(stages, ss + VkPipelineShaderStageCreateInfo_stage, VK_SHADER_STAGE_FRAGMENT_BIT)
|
|
Vk.put_i64(stages, ss + VkPipelineShaderStageCreateInfo_module, render3d_st.gvk_prog_fs[p])
|
|
Vk.put_ptr(stages, ss + VkPipelineShaderStageCreateInfo_pName, "main")
|
|
|
|
# vertex input: one binding per distinct buffer the mesh's attributes read, in the order met;
|
|
# an attribute the shader does not read is left out
|
|
let aw = VkVertexInputAttributeDescription_sizeof
|
|
let bdw = VkVertexInputBindingDescription_sizeof
|
|
let attrs = bytes(aw * (GPU_MAX_ATTRS + 1))
|
|
let bnds = bytes(bdw * (GPU_MAX_VBUFS + 1))
|
|
Vk.zero(attrs, aw * (GPU_MAX_ATTRS + 1))
|
|
Vk.zero(bnds, bdw * (GPU_MAX_VBUFS + 1))
|
|
var na = 0
|
|
var nbd = 0
|
|
if m != null and m.attrs != null {
|
|
let bufs = render3d_st.gvk_pipe_bufs
|
|
for i in 0 .. m.n_attrs {
|
|
let o = i * GPU_ATTR_W
|
|
if m.attrs[o + 1] == 0 { continue }
|
|
var wanted = false
|
|
for q in 0 .. len(v.i_loc) { if v.i_loc[q] == i { wanted = true } }
|
|
if not wanted { continue }
|
|
var bi = -1
|
|
for q in 0 .. nbd { if bufs[q] == m.attrs[o] and bi < 0 { bi = q } }
|
|
if bi < 0 and nbd < GPU_MAX_VBUFS {
|
|
bi = nbd
|
|
bufs[nbd] = m.attrs[o]
|
|
Vk.put_i32(bnds, nbd * bdw + VkVertexInputBindingDescription_binding, nbd)
|
|
Vk.put_i32(bnds, nbd * bdw + VkVertexInputBindingDescription_stride, m.attrs[o + 3])
|
|
if m.attrs[o + 6] == 1 { Vk.put_i32(bnds, nbd * bdw + VkVertexInputBindingDescription_inputRate, VK_VERTEX_INPUT_RATE_INSTANCE) }
|
|
nbd += 1
|
|
}
|
|
Vk.put_i32(attrs, na * aw + VkVertexInputAttributeDescription_location, i)
|
|
Vk.put_i32(attrs, na * aw + VkVertexInputAttributeDescription_binding, bi)
|
|
Vk.put_i32(attrs, na * aw + VkVertexInputAttributeDescription_format, gvk_attr_format(m.attrs[o + 1], m.attrs[o + 2], m.attrs[o + 5]))
|
|
Vk.put_i32(attrs, na * aw + VkVertexInputAttributeDescription_offset, m.attrs[o + 4])
|
|
na += 1
|
|
}
|
|
}
|
|
# A shader input the mesh does not provide (a_joints on a mesh without a skin) reads zeros from a
|
|
# shared buffer, as OpenGL's disabled attribute reads its default. Vulkan requires every input the
|
|
# vertex stage declares to be fed (VUID-VkGraphicsPipelineCreateInfo-Input-07904).
|
|
var need_zero = false
|
|
for q in 0 .. len(v.i_loc) {
|
|
if not gvk_input_unfed(m, v.i_loc[q]) or na >= GPU_MAX_ATTRS { continue }
|
|
if not need_zero {
|
|
need_zero = true
|
|
Vk.put_i32(bnds, nbd * bdw + VkVertexInputBindingDescription_binding, nbd)
|
|
Vk.put_i32(bnds, nbd * bdw + VkVertexInputBindingDescription_stride, 16)
|
|
Vk.put_i32(bnds, nbd * bdw + VkVertexInputBindingDescription_inputRate, VK_VERTEX_INPUT_RATE_INSTANCE)
|
|
}
|
|
Vk.put_i32(attrs, na * aw + VkVertexInputAttributeDescription_location, v.i_loc[q])
|
|
Vk.put_i32(attrs, na * aw + VkVertexInputAttributeDescription_binding, nbd)
|
|
Vk.put_i32(attrs, na * aw + VkVertexInputAttributeDescription_format, gvk_input_format(v.i_type[q]))
|
|
Vk.put_i32(attrs, na * aw + VkVertexInputAttributeDescription_offset, 0)
|
|
na += 1
|
|
}
|
|
if need_zero { nbd += 1 }
|
|
let vin = bytes(VkPipelineVertexInputStateCreateInfo_sizeof)
|
|
Vk.zero(vin, VkPipelineVertexInputStateCreateInfo_sizeof)
|
|
Vk.put_i32(vin, VkPipelineVertexInputStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO)
|
|
Vk.put_i32(vin, VkPipelineVertexInputStateCreateInfo_vertexBindingDescriptionCount, nbd)
|
|
Vk.put_ptr(vin, VkPipelineVertexInputStateCreateInfo_pVertexBindingDescriptions, bnds)
|
|
Vk.put_i32(vin, VkPipelineVertexInputStateCreateInfo_vertexAttributeDescriptionCount, na)
|
|
Vk.put_ptr(vin, VkPipelineVertexInputStateCreateInfo_pVertexAttributeDescriptions, attrs)
|
|
|
|
let ias = bytes(VkPipelineInputAssemblyStateCreateInfo_sizeof)
|
|
Vk.zero(ias, VkPipelineInputAssemblyStateCreateInfo_sizeof)
|
|
Vk.put_i32(ias, VkPipelineInputAssemblyStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO)
|
|
Vk.put_i32(ias, VkPipelineInputAssemblyStateCreateInfo_topology, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
|
|
let vps = bytes(VkPipelineViewportStateCreateInfo_sizeof)
|
|
Vk.zero(vps, VkPipelineViewportStateCreateInfo_sizeof)
|
|
Vk.put_i32(vps, VkPipelineViewportStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO)
|
|
Vk.put_i32(vps, VkPipelineViewportStateCreateInfo_viewportCount, 1)
|
|
Vk.put_i32(vps, VkPipelineViewportStateCreateInfo_scissorCount, 1)
|
|
|
|
let rs = bytes(VkPipelineRasterizationStateCreateInfo_sizeof)
|
|
Vk.zero(rs, VkPipelineRasterizationStateCreateInfo_sizeof)
|
|
Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO)
|
|
if st.wireframe == 1 { Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_polygonMode, VK_POLYGON_MODE_LINE) } else { Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_polygonMode, VK_POLYGON_MODE_FILL) }
|
|
if st.cull == 1 {
|
|
if st.cull_face == GL_FRONT { Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_cullMode, VK_CULL_MODE_FRONT_BIT) } else { Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_cullMode, VK_CULL_MODE_BACK_BIT) }
|
|
}
|
|
# no y flip between the APIs' clip spaces and their framebuffers' rows, so a triangle that is
|
|
# counter-clockwise on OpenGL's bottom-up window is clockwise in Vulkan's top-down one
|
|
Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_frontFace, VK_FRONT_FACE_CLOCKWISE)
|
|
Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_lineWidth, 0x3F800000)
|
|
if st.bias == 1 {
|
|
Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_depthBiasEnable, 1)
|
|
Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_depthBiasSlopeFactor, st.bias_factor)
|
|
Vk.put_i32(rs, VkPipelineRasterizationStateCreateInfo_depthBiasConstantFactor, st.bias_units)
|
|
}
|
|
let ms = bytes(VkPipelineMultisampleStateCreateInfo_sizeof)
|
|
Vk.zero(ms, VkPipelineMultisampleStateCreateInfo_sizeof)
|
|
Vk.put_i32(ms, VkPipelineMultisampleStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO)
|
|
Vk.put_i32(ms, VkPipelineMultisampleStateCreateInfo_rasterizationSamples, samples)
|
|
# OpenGL ignores alpha to coverage without a multisampled target; Vulkan with one sample would
|
|
# instead drop every fragment under half alpha, which erased the meadow's flowers
|
|
if samples > 1 { Vk.put_i32(ms, VkPipelineMultisampleStateCreateInfo_alphaToCoverageEnable, st.a2c) }
|
|
let ds = bytes(VkPipelineDepthStencilStateCreateInfo_sizeof)
|
|
Vk.zero(ds, VkPipelineDepthStencilStateCreateInfo_sizeof)
|
|
Vk.put_i32(ds, VkPipelineDepthStencilStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO)
|
|
if depth_fmt != VK_FORMAT_UNDEFINED {
|
|
Vk.put_i32(ds, VkPipelineDepthStencilStateCreateInfo_depthTestEnable, st.depth_test)
|
|
# OpenGL writes no depth with the test off, whatever the mask says
|
|
if st.depth_test == 1 { Vk.put_i32(ds, VkPipelineDepthStencilStateCreateInfo_depthWriteEnable, st.depth_write) }
|
|
Vk.put_i32(ds, VkPipelineDepthStencilStateCreateInfo_depthCompareOp, gvk_depth_op(st.depth_func))
|
|
}
|
|
let cbw = VkPipelineColorBlendAttachmentState_sizeof
|
|
let cba = bytes(cbw * (n_color + 1))
|
|
Vk.zero(cba, cbw * (n_color + 1))
|
|
for c in 0 .. n_color {
|
|
if st.color_write == 1 { Vk.put_i32(cba, c * cbw + VkPipelineColorBlendAttachmentState_colorWriteMask, 15) }
|
|
if st.blend == 1 {
|
|
Vk.put_i32(cba, c * cbw + VkPipelineColorBlendAttachmentState_blendEnable, 1)
|
|
Vk.put_i32(cba, c * cbw + VkPipelineColorBlendAttachmentState_srcColorBlendFactor, gvk_blend_factor(st.blend_src))
|
|
Vk.put_i32(cba, c * cbw + VkPipelineColorBlendAttachmentState_dstColorBlendFactor, gvk_blend_factor(st.blend_dst))
|
|
Vk.put_i32(cba, c * cbw + VkPipelineColorBlendAttachmentState_srcAlphaBlendFactor, gvk_blend_factor(st.blend_src))
|
|
Vk.put_i32(cba, c * cbw + VkPipelineColorBlendAttachmentState_dstAlphaBlendFactor, gvk_blend_factor(st.blend_dst))
|
|
}
|
|
}
|
|
let cbs = bytes(VkPipelineColorBlendStateCreateInfo_sizeof)
|
|
Vk.zero(cbs, VkPipelineColorBlendStateCreateInfo_sizeof)
|
|
Vk.put_i32(cbs, VkPipelineColorBlendStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO)
|
|
Vk.put_i32(cbs, VkPipelineColorBlendStateCreateInfo_attachmentCount, n_color)
|
|
Vk.put_ptr(cbs, VkPipelineColorBlendStateCreateInfo_pAttachments, cba)
|
|
let dyn_states = bytes(8)
|
|
Vk.put_i32(dyn_states, 0, VK_DYNAMIC_STATE_VIEWPORT)
|
|
Vk.put_i32(dyn_states, 4, VK_DYNAMIC_STATE_SCISSOR)
|
|
let dys = bytes(VkPipelineDynamicStateCreateInfo_sizeof)
|
|
Vk.zero(dys, VkPipelineDynamicStateCreateInfo_sizeof)
|
|
Vk.put_i32(dys, VkPipelineDynamicStateCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO)
|
|
Vk.put_i32(dys, VkPipelineDynamicStateCreateInfo_dynamicStateCount, 2)
|
|
Vk.put_ptr(dys, VkPipelineDynamicStateCreateInfo_pDynamicStates, dyn_states)
|
|
let formats = bytes(4 * (n_color + 1))
|
|
for c in 0 .. n_color { Vk.put_i32(formats, c * 4, color_fmt) }
|
|
let prci = bytes(VkPipelineRenderingCreateInfo_sizeof)
|
|
Vk.zero(prci, VkPipelineRenderingCreateInfo_sizeof)
|
|
Vk.put_i32(prci, VkPipelineRenderingCreateInfo_sType, VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO)
|
|
Vk.put_i32(prci, VkPipelineRenderingCreateInfo_colorAttachmentCount, n_color)
|
|
Vk.put_ptr(prci, VkPipelineRenderingCreateInfo_pColorAttachmentFormats, formats)
|
|
Vk.put_i32(prci, VkPipelineRenderingCreateInfo_depthAttachmentFormat, depth_fmt)
|
|
|
|
let gpci = bytes(VkGraphicsPipelineCreateInfo_sizeof)
|
|
Vk.zero(gpci, VkGraphicsPipelineCreateInfo_sizeof)
|
|
Vk.put_i32(gpci, VkGraphicsPipelineCreateInfo_sType, VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO)
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pNext, prci)
|
|
Vk.put_i32(gpci, VkGraphicsPipelineCreateInfo_stageCount, 2)
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pStages, stages)
|
|
# a mesh-shader pipeline has neither: the mesh stage makes its own vertices
|
|
if gvk_stage_first(render3d_st, p) == VK_SHADER_STAGE_VERTEX_BIT {
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pVertexInputState, vin)
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pInputAssemblyState, ias)
|
|
}
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pViewportState, vps)
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pRasterizationState, rs)
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pMultisampleState, ms)
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pDepthStencilState, ds)
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pColorBlendState, cbs)
|
|
Vk.put_ptr(gpci, VkGraphicsPipelineCreateInfo_pDynamicState, dys)
|
|
Vk.put_i64(gpci, VkGraphicsPipelineCreateInfo_layout, render3d_st.gvk_prog_layout[p])
|
|
let out = bytes(8)
|
|
let r = Vk.create_graphics_pipelines(render3d_st.gvk_dev, zero, 1, gpci, render3d_st.gvk_ac, out)
|
|
let pipe = gvk_handle(out)
|
|
# the create infos are read by the create and go with it: a pipeline first met in play kept all of them
|
|
free(stages); free(attrs); free(bnds); free(vin); free(ias); free(vps); free(rs); free(ms); free(ds)
|
|
free(cba); free(cbs); free(dyn_states); free(dys); free(formats); free(prci); free(gpci); free(out)
|
|
if r != VK_SUCCESS {
|
|
let what = `vkCreateGraphicsPipelines for {v.vs} + {v.fs}`
|
|
gvk_fail(render3d_st, what, r)
|
|
free(what)
|
|
return zero
|
|
}
|
|
# R3D_VK_PROF names each pipeline as it is made: one made during play is a stall a warm-up missed
|
|
if gvk_prof(render3d_st) { render3d_st.gvk_n_pipe_new += 1; print(`r3d: vulkan pipeline {len(render3d_st.gvk_pipe) + 1}: {v.vs} + {v.fs}, {n_color} colour format {color_fmt}, depth {depth_fmt}, {samples}x`) }
|
|
push(render3d_st.gvk_pipe_keys, key)
|
|
push(render3d_st.gvk_pipe, pipe)
|
|
return pipe
|
|
}
|
|
|
|
# ---- uniforms -----------------------------------------------------------------------------
|
|
# On Vulkan a loose uniform is a place in its program's uniform block: glslang's relaxed mode
|
|
# gathered each stage's loose uniforms into one block and the manifest says where each one sits.
|
|
# A uniform "location" is program * 4096 + the index of its first manifest entry, so -1 still
|
|
# means "not in this program". Writing one writes every stage that declares it; the blocks go to
|
|
# the GPU at the draw.
|
|
|
|
function gvk_same(a: pointer, b: pointer, n: int) -> bool {
|
|
var i = 0
|
|
while i < n { if Vk.get_i32(a, i) != Vk.get_i32(b, i) { return false }; i += 4 }
|
|
return true
|
|
}
|
|
|
|
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
|
|
function gvk_uniform_blocks(render3d_st: mut Render3dState, p: int) -> void {
|
|
if render3d_st.gvk_ublk_v == null {
|
|
render3d_st.gvk_ublk_v = new []bytes; render3d_st.gvk_ublk_f = new []bytes; render3d_st.gvk_prog_tex = new []words; render3d_st.gvk_prog_unit = new []words
|
|
render3d_st.gvk_prog_dirty = new []int; render3d_st.gvk_set_last = new []long; render3d_st.gvk_set_frame = new []int; render3d_st.gvk_set_tex = new []words
|
|
}
|
|
let zero: long = 0
|
|
while len(render3d_st.gvk_ublk_v) <= p {
|
|
push(render3d_st.gvk_ublk_v, null); push(render3d_st.gvk_ublk_f, null); push(render3d_st.gvk_prog_tex, null); push(render3d_st.gvk_prog_unit, null)
|
|
push(render3d_st.gvk_prog_dirty, 1); push(render3d_st.gvk_set_last, zero); push(render3d_st.gvk_set_frame, 0); push(render3d_st.gvk_set_tex, null)
|
|
}
|
|
let v = render3d_st.gvk_prog_var[p]
|
|
if v == null or render3d_st.gvk_ublk_v[p] != null or render3d_st.gvk_prog_tex[p] != null { return }
|
|
if v.vblock > 0 { let b = bytes(v.vblock); Vk.zero(b, v.vblock); render3d_st.gvk_ublk_v[p] = b }
|
|
if v.fblock > 0 { let b = bytes(v.fblock); Vk.zero(b, v.fblock); render3d_st.gvk_ublk_f[p] = b }
|
|
let nt = len(v.t_name) + 1
|
|
let t = words(nt)
|
|
let u = words(nt)
|
|
for i in 0 .. nt { t[i] = 0; u[i] = 0 }
|
|
render3d_st.gvk_prog_tex[p] = t
|
|
render3d_st.gvk_prog_unit[p] = u
|
|
let st = words(nt)
|
|
for i in 0 .. nt { st[i] = -1 }
|
|
render3d_st.gvk_set_tex[p] = st
|
|
}
|
|
|
|
function gvk_uniform(render3d_st: Render3dState, p: int, name: string) -> int {
|
|
if render3d_st.gvk_prog_var == null or p <= 0 or p >= len(render3d_st.gvk_prog_var) { return -1 }
|
|
let v = render3d_st.gvk_prog_var[p]
|
|
if v == null { return -1 }
|
|
for i in 0 .. len(v.u_name) { if v.u_name[i] == name { return p * 4096 + i } }
|
|
# a sampler: OpenGL code points it at a texture unit once with u_i and then only binds units
|
|
# (the actors do), so its location is kept apart and u_i on it records the unit
|
|
for i in 0 .. len(v.t_name) { if v.t_name[i] == name { return p * 4096 + 2048 + i } }
|
|
return -1
|
|
}
|
|
|
|
# n elements of `size` bytes each from src into every block that declares the uniform at loc
|
|
function gvk_u_set(render3d_st: mut Render3dState, loc: int, src: pointer, size: int, n: int) -> void {
|
|
if loc < 0 { return }
|
|
let p = loc / 4096
|
|
let v = render3d_st.gvk_prog_var[p]
|
|
if v == null { return }
|
|
gvk_uniform_blocks(render3d_st, p)
|
|
if loc % 4096 >= 2048 {
|
|
let si = loc % 4096 - 2048
|
|
if si < len(v.t_name) and render3d_st.gvk_prog_unit[p][si] != Vk.get_i32(src, 0) + 1 { render3d_st.gvk_prog_unit[p][si] = Vk.get_i32(src, 0) + 1; render3d_st.gvk_prog_dirty[p] = 1 }
|
|
return
|
|
}
|
|
let name = v.u_name[loc % 4096]
|
|
for j in 0 .. len(v.u_name) {
|
|
if v.u_name[j] != name { continue }
|
|
var blk = render3d_st.gvk_ublk_v[p]
|
|
var cap = v.vblock
|
|
if v.u_stage[j] == 1 { blk = render3d_st.gvk_ublk_f[p]; cap = v.fblock }
|
|
if blk == null { continue }
|
|
var stride = v.u_stride[j]
|
|
if stride == 0 { stride = size }
|
|
var count = n
|
|
if count > v.u_count[j] { count = v.u_count[j] }
|
|
for k in 0 .. count {
|
|
let at = v.u_off[j] + k * stride
|
|
# the same value again (most per-draw uniforms repeat) leaves the program's set reusable
|
|
if at + size <= cap and not gvk_same(mem_off(blk, at), mem_off(src, k * size), size) {
|
|
mem_copy(mem_off(blk, at), mem_off(src, k * size), size)
|
|
render3d_st.gvk_prog_dirty[p] = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# a sampler uniform: the texture for the manifest binding with that name
|
|
function gvk_bind_texture(render3d_st: mut Render3dState, p: int, name: string, tex: int) -> void {
|
|
if render3d_st.gvk_prog_var == null or p <= 0 or p >= len(render3d_st.gvk_prog_var) { return }
|
|
let v = render3d_st.gvk_prog_var[p]
|
|
if v == null { return }
|
|
gvk_uniform_blocks(render3d_st, p)
|
|
for i in 0 .. len(v.t_name) { if v.t_name[i] == name and render3d_st.gvk_prog_tex[p][i] != tex { render3d_st.gvk_prog_tex[p][i] = tex; render3d_st.gvk_prog_dirty[p] = 1 } }
|
|
}
|
|
|
|
# ---- per-frame uniform ring and descriptor sets -------------------------------------------
|
|
# Each draw's blocks are copied into one host-visible ring buffer at the device's alignment and
|
|
# its descriptor set comes from a pool that is reset with the frame. Both are rewound at
|
|
# gvk_frame_reset.
|
|
const GVK_RING_BYTES: int = 64 * 1024 * 1024
|
|
|
|
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
|
|
function gvk_frame_init(render3d_st: mut Render3dState) -> bool {
|
|
let props = bytes(VkPhysicalDeviceProperties_sizeof)
|
|
Vk.get_physical_device_properties(render3d_st.gvk_pd, props)
|
|
let al = Vk.get_i64(props, VkPhysicalDeviceProperties_limits + VkPhysicalDeviceLimits_minUniformBufferOffsetAlignment)
|
|
render3d_st.gvk_ring_align = int(al)
|
|
if render3d_st.gvk_ring_align < 16 { render3d_st.gvk_ring_align = 16 }
|
|
# MSAA: the most samples (up to the 4 the scene asks for) both a colour and a depth target can take
|
|
let lim = VkPhysicalDeviceProperties_limits
|
|
let counts = Vk.get_i32(props, lim + VkPhysicalDeviceLimits_framebufferColorSampleCounts) & Vk.get_i32(props, lim + VkPhysicalDeviceLimits_framebufferDepthSampleCounts)
|
|
render3d_st.gvk_msaa_max = 1
|
|
if (counts & VK_SAMPLE_COUNT_2_BIT) != 0 { render3d_st.gvk_msaa_max = 2 }
|
|
if (counts & VK_SAMPLE_COUNT_4_BIT) != 0 { render3d_st.gvk_msaa_max = 4 }
|
|
render3d_st.gvk_ring_buf = gvk_buf_new(render3d_st)
|
|
# one half per frame slot: the cached sets bind this one buffer, and each draw's offset says the half
|
|
if not gvk_buf_reserve(render3d_st, render3d_st.gvk_ring_buf, GVK_RING_BYTES * 2) { return false }
|
|
let sizes = bytes(VkDescriptorPoolSize_sizeof * 3)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_descriptorCount, 32768)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_sizeof + VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_sizeof + VkDescriptorPoolSize_descriptorCount, 131072)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_sizeof * 2 + VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_sizeof * 2 + VkDescriptorPoolSize_descriptorCount, 16384)
|
|
let dpci = bytes(VkDescriptorPoolCreateInfo_sizeof)
|
|
Vk.zero(dpci, VkDescriptorPoolCreateInfo_sizeof)
|
|
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO)
|
|
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_maxSets, 16384)
|
|
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 3)
|
|
Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes)
|
|
let out = bytes(8)
|
|
render3d_st.gvk_dpools = new []long
|
|
for k in 0 .. 2 {
|
|
render3d_st.gvk_mk_dpool += 1
|
|
let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, render3d_st.gvk_ac, out)
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool", r) }
|
|
push(render3d_st.gvk_dpools, gvk_handle(out))
|
|
}
|
|
render3d_st.gvk_dpool = render3d_st.gvk_dpools[0]
|
|
if not gvk_kpool_make(render3d_st) { return false }
|
|
render3d_st.gvk_white = gvk_tex_new(render3d_st)
|
|
let px = bytes(4)
|
|
px[0] = 255; px[1] = 255; px[2] = 255; px[3] = 255
|
|
return gvk_tex_storage(render3d_st, render3d_st.gvk_white, false, GL_RGBA8, 1, 1, 1, false) and gvk_tex_upload(render3d_st, render3d_st.gvk_white, GL_RGBA8, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px)
|
|
}
|
|
|
|
function gvk_frame_reset(render3d_st: mut Render3dState) -> void {
|
|
# what MoltenVK autoreleased last frame goes back here (a no-op off macOS); R3D_VK_NOPOOL=1 for an A/B
|
|
if not render3d_st.gvk_nopool { Vk.frame_pool() }
|
|
# a freed texture's kept sets never match again (its generation moved on) and were held until the
|
|
# pool filled - 8192 sets, hours of play: past GVK_SC_DEAD_MAX releases the pool starts over, and a
|
|
# draw still in use makes its set again the first time it is drawn
|
|
if render3d_st.gvk_sc_dead >= GVK_SC_DEAD_MAX { gvk_kpool_reset(render3d_st) }
|
|
let slot = render3d_st.gvk_frame_no & 1
|
|
render3d_st.gvk_ring_off = slot * GVK_RING_BYTES
|
|
render3d_st.gvk_ring_end = (slot + 1) * GVK_RING_BYTES
|
|
render3d_st.gvk_dpool = render3d_st.gvk_dpools[slot]
|
|
Vk.reset_descriptor_pool(render3d_st.gvk_dev, render3d_st.gvk_dpool, 0)
|
|
}
|
|
|
|
# a block into the ring; its offset, or -1 when the frame has used the whole ring
|
|
function gvk_ring_put(render3d_st: mut Render3dState, blk: pointer, n: int) -> int {
|
|
let at = (render3d_st.gvk_ring_off + render3d_st.gvk_ring_align - 1) / render3d_st.gvk_ring_align * render3d_st.gvk_ring_align
|
|
if at + n > render3d_st.gvk_ring_end { return -1 }
|
|
mem_copy(mem_off(render3d_st.gvk_buf_map[render3d_st.gvk_ring_buf], at), blk, n)
|
|
render3d_st.gvk_ring_off = at + n
|
|
return at
|
|
}
|
|
|
|
# ---- descriptor sets that survive the frame -------------------------------------------------
|
|
# A draw's set carries its textures; its uniform blocks are dynamic uniform buffers into the
|
|
# frame's ring, bound with this draw's offsets. So one set serves every draw of a program with the
|
|
# same textures, this frame and the frames after it, and a draw that changes only uniforms allocates
|
|
# and writes no set at all - which was about half of a Vulkan frame's CPU time (8.5 ms at the camp).
|
|
# The sets live in a pool of their own that is only reset when it fills. The key holds each
|
|
# texture's handle and generation (bumped when the image behind a handle is replaced) and its
|
|
# sampler, so a set is never matched against a texture it no longer describes.
|
|
const GVK_KPOOL_SETS: int = 8192
|
|
const GVK_SC_DEAD_MAX: int = 256 # textures released before the kept sets start over
|
|
|
|
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
|
|
function gvk_kpool_make(render3d_st: mut Render3dState) -> bool {
|
|
let sizes = bytes(VkDescriptorPoolSize_sizeof * 2)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_descriptorCount, GVK_KPOOL_SETS * 2)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_sizeof + VkDescriptorPoolSize_type, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER)
|
|
Vk.put_i32(sizes, VkDescriptorPoolSize_sizeof + VkDescriptorPoolSize_descriptorCount, GVK_KPOOL_SETS * 8)
|
|
let dpci = bytes(VkDescriptorPoolCreateInfo_sizeof)
|
|
Vk.zero(dpci, VkDescriptorPoolCreateInfo_sizeof)
|
|
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO)
|
|
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_maxSets, GVK_KPOOL_SETS)
|
|
Vk.put_i32(dpci, VkDescriptorPoolCreateInfo_poolSizeCount, 2)
|
|
Vk.put_ptr(dpci, VkDescriptorPoolCreateInfo_pPoolSizes, sizes)
|
|
let out = bytes(8)
|
|
render3d_st.gvk_mk_dpool += 1
|
|
let r = Vk.create_descriptor_pool(render3d_st.gvk_dev, dpci, render3d_st.gvk_ac, out)
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDescriptorPool (kept sets)", r) }
|
|
render3d_st.gvk_kpool = gvk_handle(out)
|
|
gvk_sc_clear(render3d_st)
|
|
return true
|
|
}
|
|
|
|
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
|
|
function gvk_sc_clear(render3d_st: mut Render3dState) -> void {
|
|
# emptied, not replaced: five fresh lists each time the pool filled were never given back
|
|
if render3d_st.gvk_sc_prog == null {
|
|
render3d_st.gvk_sc_prog = new []int; render3d_st.gvk_sc_koff = new []int; render3d_st.gvk_sc_set = new []long; render3d_st.gvk_sc_next = new []int
|
|
render3d_st.gvk_sc_keys = new []long
|
|
}
|
|
List.clear(render3d_st.gvk_sc_prog); List.clear(render3d_st.gvk_sc_koff); List.clear(render3d_st.gvk_sc_set); List.clear(render3d_st.gvk_sc_next)
|
|
List.clear(render3d_st.gvk_sc_keys)
|
|
render3d_st.gvk_sc_dead = 0
|
|
if render3d_st.gvk_sc_head == null { render3d_st.gvk_sc_head = words(4096) }
|
|
for i in 0 .. 4096 { render3d_st.gvk_sc_head[i] = -1 }
|
|
}
|
|
|
|
# the pool is full: finish the frame recorded so far (it may use sets from it), then start again
|
|
function gvk_kpool_reset(render3d_st: mut Render3dState) -> void {
|
|
gvk_flush(render3d_st)
|
|
gvk_frame_wait(render3d_st)
|
|
Vk.reset_descriptor_pool(render3d_st.gvk_dev, render3d_st.gvk_kpool, 0)
|
|
gvk_sc_clear(render3d_st)
|
|
}
|
|
|
|
function gvk_draw_set(render3d_st: mut Render3dState, p: int, tx: words, tx_w: int, tx_cap: int) -> long {
|
|
let zero: long = 0
|
|
let v = render3d_st.gvk_prog_var[p]
|
|
gvk_uniform_blocks(render3d_st, p)
|
|
@alloc_ok("filled to 4096 programs at start-up (@max 4096)")
|
|
while len(render3d_st.gvk_ub_frame) <= p { push(render3d_st.gvk_ub_frame, 0); push(render3d_st.gvk_ub_offv, 0); push(render3d_st.gvk_ub_offf, 0) }
|
|
let nt = len(v.t_name)
|
|
# the key: every texture as this draw resolves it, and its sampler
|
|
@alloc_ok("filled to 130 at start-up (@max 130: 64 textures a draw)")
|
|
while len(render3d_st.gvk_sc_tmp) < nt * 2 + 2 { push(render3d_st.gvk_sc_tmp, zero) }
|
|
for t in 0 .. nt {
|
|
var tex = render3d_st.gvk_prog_tex[p][t]
|
|
# nothing bound by name: the texture on the unit the sampler was pointed at, as OpenGL reads it
|
|
let unit1 = render3d_st.gvk_prog_unit[p][t]
|
|
if tex <= 0 and unit1 > 0 and unit1 <= 32 and render3d_st.gpu_unit_2d != null { tex = render3d_st.gpu_unit_2d[unit1 - 1] }
|
|
if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 0 { tex = render3d_st.gvk_white }
|
|
var smp: long = 0
|
|
let o = tex * tx_w
|
|
if tex != render3d_st.gvk_white and tex < tx_cap {
|
|
smp = gvk_tex_sampler(render3d_st, tex, tx[o + 5], tx[o + 6], tx[o + 7], tx[o + 8], tx[o + 9], tx[o + 11])
|
|
} else {
|
|
# the sampler for a slot nothing was bound to, made once: looking it up by its string key on
|
|
# every draw was the costly part of the key for the shadow and depth variants
|
|
if render3d_st.gvk_default_smp == 0 { render3d_st.gvk_default_smp = gvk_sampler(render3d_st, GL_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, 0, 0) }
|
|
smp = render3d_st.gvk_default_smp
|
|
}
|
|
let tg: long = tex * 65536 + render3d_st.gvk_tex_gen[tex]
|
|
render3d_st.gvk_sc_tmp[t * 2] = tg
|
|
render3d_st.gvk_sc_tmp[t * 2 + 1] = smp
|
|
}
|
|
var set: long = 0
|
|
var e = -1
|
|
if p < 4096 { e = render3d_st.gvk_sc_head[p] }
|
|
while e >= 0 and set == 0 {
|
|
let ko = render3d_st.gvk_sc_koff[e]
|
|
var same = true
|
|
var t = 0
|
|
while same and t < nt * 2 { if render3d_st.gvk_sc_keys[ko + t] != render3d_st.gvk_sc_tmp[t] { same = false }; t += 1 }
|
|
if same { set = render3d_st.gvk_sc_set[e] } else { e = render3d_st.gvk_sc_next[e] }
|
|
}
|
|
if set == 0 {
|
|
set = gvk_sc_make(render3d_st, p, nt)
|
|
if set == 0 { return zero }
|
|
}
|
|
# the uniform blocks: copied into the ring once per frame, and again only when a value changed
|
|
if render3d_st.gvk_prog_dirty[p] == 1 or render3d_st.gvk_ub_frame[p] != render3d_st.gvk_frame_no {
|
|
if render3d_st.gvk_ublk_v[p] != null and v.vblock > 0 {
|
|
let at = gvk_ring_put(render3d_st, render3d_st.gvk_ublk_v[p], v.vblock)
|
|
if at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return zero }
|
|
render3d_st.gvk_ub_offv[p] = at
|
|
}
|
|
if render3d_st.gvk_ublk_f[p] != null and v.fblock > 0 {
|
|
let at = gvk_ring_put(render3d_st, render3d_st.gvk_ublk_f[p], v.fblock)
|
|
if at < 0 { print("r3d: vulkan: the frame's uniform ring is full"); return zero }
|
|
render3d_st.gvk_ub_offf[p] = at
|
|
}
|
|
render3d_st.gvk_ub_frame[p] = render3d_st.gvk_frame_no
|
|
render3d_st.gvk_prog_dirty[p] = 0
|
|
}
|
|
render3d_st.gvk_set_ndyn = 0
|
|
if v.vblock >= 0 { Vk.put_i32(render3d_st.gvk_set_offs, render3d_st.gvk_set_ndyn * 4, render3d_st.gvk_ub_offv[p]); render3d_st.gvk_set_ndyn += 1 }
|
|
if v.fblock >= 0 { Vk.put_i32(render3d_st.gvk_set_offs, render3d_st.gvk_set_ndyn * 4, render3d_st.gvk_ub_offf[p]); render3d_st.gvk_set_ndyn += 1 }
|
|
render3d_st.gvk_buf_used[render3d_st.gvk_ring_buf] = render3d_st.gvk_frame_no
|
|
return set
|
|
}
|
|
|
|
# a new kept set for program p with the textures in gvk_sc_tmp, written and cached
|
|
@alloc_ok("the kept-set cache: at most GVK_KPOOL_SETS (8192) sets and their keys, then gvk_kpool_reset empties the pool and the cache and it fills again")
|
|
function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
|
|
let zero: long = 0
|
|
let v = render3d_st.gvk_prog_var[p]
|
|
let dsai = gvk_tmp(render3d_st, VkDescriptorSetAllocateInfo_sizeof)
|
|
let layouts = gvk_tmp(render3d_st, 8)
|
|
let sets = gvk_tmp(render3d_st, 8)
|
|
var r = 0
|
|
var tries = 0
|
|
while tries < 2 {
|
|
Vk.zero(dsai, VkDescriptorSetAllocateInfo_sizeof)
|
|
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_sType, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO)
|
|
Vk.put_i64(dsai, VkDescriptorSetAllocateInfo_descriptorPool, render3d_st.gvk_kpool)
|
|
Vk.put_i32(dsai, VkDescriptorSetAllocateInfo_descriptorSetCount, 1)
|
|
Vk.put_i64(layouts, 0, render3d_st.gvk_prog_dsl[p])
|
|
Vk.put_ptr(dsai, VkDescriptorSetAllocateInfo_pSetLayouts, layouts)
|
|
render3d_st.gvk_mk_set += 1
|
|
r = Vk.allocate_descriptor_sets(render3d_st.gvk_dev, dsai, sets)
|
|
if r == VK_SUCCESS { tries = 2 } else { gvk_kpool_reset(render3d_st); tries += 1 }
|
|
}
|
|
if r != VK_SUCCESS { gvk_fail(render3d_st, "vkAllocateDescriptorSets (kept sets)", r); return zero }
|
|
let set = Vk.get_i64(sets, 0)
|
|
let ww = VkWriteDescriptorSet_sizeof
|
|
let writes = gvk_tmp(render3d_st, ww * (nt + 3))
|
|
Vk.zero(writes, ww * (nt + 3))
|
|
let bis = gvk_tmp(render3d_st, VkDescriptorBufferInfo_sizeof * 2)
|
|
let iis = gvk_tmp(render3d_st, VkDescriptorImageInfo_sizeof * (nt + 1))
|
|
var nw = 0
|
|
var bi = 0
|
|
for stage in 0 .. 2 {
|
|
var size = v.vblock
|
|
if stage == 1 { size = v.fblock }
|
|
if size < 0 { continue }
|
|
let at = bi * VkDescriptorBufferInfo_sizeof
|
|
let off0: long = 0
|
|
var range: long = size
|
|
if size == 0 { range = 16 }
|
|
Vk.put_i64(bis, at + VkDescriptorBufferInfo_buffer, render3d_st.gvk_buf[render3d_st.gvk_ring_buf])
|
|
Vk.put_i64(bis, at + VkDescriptorBufferInfo_offset, off0)
|
|
Vk.put_i64(bis, at + VkDescriptorBufferInfo_range, range)
|
|
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
|
|
Vk.put_i64(writes, nw * ww + VkWriteDescriptorSet_dstSet, set)
|
|
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_dstBinding, stage)
|
|
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_descriptorCount, 1)
|
|
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_descriptorType, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC)
|
|
Vk.put_ptr(writes, nw * ww + VkWriteDescriptorSet_pBufferInfo, mem_off(bis, at))
|
|
nw += 1
|
|
bi += 1
|
|
}
|
|
let iw = VkDescriptorImageInfo_sizeof
|
|
for t in 0 .. nt {
|
|
let tex = int(render3d_st.gvk_sc_tmp[t * 2] / 65536)
|
|
Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_sampler, render3d_st.gvk_sc_tmp[t * 2 + 1])
|
|
Vk.put_i64(iis, t * iw + VkDescriptorImageInfo_imageView, render3d_st.gvk_tex_view[tex])
|
|
Vk.put_i32(iis, t * iw + VkDescriptorImageInfo_imageLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_sType, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET)
|
|
Vk.put_i64(writes, nw * ww + VkWriteDescriptorSet_dstSet, set)
|
|
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_dstBinding, v.t_bind[t])
|
|
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_descriptorCount, 1)
|
|
Vk.put_i32(writes, nw * ww + VkWriteDescriptorSet_descriptorType, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER)
|
|
Vk.put_ptr(writes, nw * ww + VkWriteDescriptorSet_pImageInfo, mem_off(iis, t * iw))
|
|
nw += 1
|
|
}
|
|
if nw > 0 { Vk.update_descriptor_sets(render3d_st.gvk_dev, nw, writes, 0, null) }
|
|
let e = len(render3d_st.gvk_sc_set)
|
|
push(render3d_st.gvk_sc_prog, p); push(render3d_st.gvk_sc_koff, len(render3d_st.gvk_sc_keys)); push(render3d_st.gvk_sc_set, set)
|
|
for t in 0 .. nt * 2 { push(render3d_st.gvk_sc_keys, render3d_st.gvk_sc_tmp[t]) }
|
|
var head = -1
|
|
if p < 4096 { head = render3d_st.gvk_sc_head[p]; render3d_st.gvk_sc_head[p] = e }
|
|
push(render3d_st.gvk_sc_next, head)
|
|
render3d_st.gvk_sc_made += 1
|
|
return set
|
|
}
|
|
|
|
# ---- the frame and its passes -------------------------------------------------------------
|
|
# One command buffer records the whole frame. Binding a framebuffer ends the pass in progress;
|
|
# the next one begins at its first clear or draw, so a clear that comes first becomes the pass's
|
|
# load op. For the length of a pass its attachments sit in the attachment layouts, and between
|
|
# passes every image is back in SHADER_READ_ONLY where samplers expect it. The present submits
|
|
# and waits: the frame is finished before the next one starts, as it is on OpenGL.
|
|
#
|
|
# Framebuffer 0 is the screen: a colour and a depth image made at gvk_screen_make. Headless that
|
|
# is all the screen there is; with a window the present copies it to the swapchain.
|
|
|
|
# ---- HDR output ------------------------------------------------------------------------------
|
|
# HDR10 (ST 2084 over BT.2020) when the player asks for it and the surface offers it. The screen
|
|
# image and the tonemap's LDR image go 10-bit with it; the tonemap and the overlay switch to their
|
|
# HDR10 variants (gpu_hdr_active). Headless there is no surface, so never.
|
|
# R3D_HDR=1 / 0 overrides the setting, for a desktop test
|
|
function r3d_hdr(render3d_st: mut Render3dState, on: bool) -> void {
|
|
var want = on
|
|
if r3d_env_has(render3d_st, "R3D_HDR") { want = Text.to_int(r3d_env(render3d_st, "R3D_HDR")) != 0 }
|
|
if want == render3d_st.gvk_hdr_want { return }
|
|
render3d_st.gvk_hdr_want = want
|
|
if render3d_st.gvk_swap != 0 { render3d_st.gvk_swap_stale = true }
|
|
}
|
|
function gpu_hdr_active(render3d_st: Render3dState) -> bool { return render3d_st.gpu_kind == GPU_VK and render3d_st.gvk_hdr_on }
|
|
function gvk_screen_fmt(render3d_st: Render3dState) -> int { if render3d_st.gvk_hdr_on { return GL_RGB10_A2 }; return GL_RGBA8 }
|
|
|
|
# The player's calibration of their display, in nits (float bits): the brightest it shows, where the
|
|
# picture's and the interface's white sit, and how far the darkest shade is lifted. Displays differ by
|
|
# an order of magnitude - a 400-nit monitor and a 2000-nit television - and one fixed curve either
|
|
# clips the first's highlights flat or leaves the second dim.
|
|
function r3d_hdr_peak_nits(render3d_st: Render3dState) -> float { if render3d_st.r3d_hdr_peak == 0.0 { return 1000.0 }; return render3d_st.r3d_hdr_peak }
|
|
function r3d_hdr_paper_nits(render3d_st: Render3dState) -> float { if render3d_st.r3d_hdr_paper == 0.0 { return 200.0 }; return render3d_st.r3d_hdr_paper }
|
|
function r3d_hdr_black_nits(render3d_st: Render3dState) -> float { return render3d_st.r3d_hdr_black }
|
|
function r3d_hdr_calibrate(render3d_st: mut Render3dState, peak: float, paper: float, black: float) -> void {
|
|
var pk = Math.clamp(peak, 100.0, 10000.0)
|
|
let pp = Math.clamp(paper, 80.0, 1000.0)
|
|
if pk < pp { pk = pp }
|
|
let bl = Math.clamp(black, 0.0, 5.0)
|
|
if pk == render3d_st.r3d_hdr_peak and pp == render3d_st.r3d_hdr_paper and bl == render3d_st.r3d_hdr_black { return }
|
|
render3d_st.r3d_hdr_peak = pk; render3d_st.r3d_hdr_paper = pp; render3d_st.r3d_hdr_black = bl
|
|
# the display is told what the picture now reaches
|
|
if render3d_st.gvk_hdr_on and render3d_st.gvk_has_hdr_meta and render3d_st.gvk_swap != 0 { gvk_hdr_metadata(render3d_st) }
|
|
}
|
|
|
|
# what the picture is: graded in BT.709 around D65, highlights to the calibrated peak, paper white average
|
|
@alloc_ok("a settings change (HDR output): made once per change")
|
|
function gvk_hdr_metadata(render3d_st: Render3dState) -> void {
|
|
let md = bytes(VkHdrMetadataEXT_sizeof)
|
|
Vk.zero(md, VkHdrMetadataEXT_sizeof)
|
|
Vk.put_i32(md, VkHdrMetadataEXT_sType, VK_STRUCTURE_TYPE_HDR_METADATA_EXT)
|
|
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryRed + VkXYColorEXT_x, float_bits(0.64)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryRed + VkXYColorEXT_y, float_bits(0.33))
|
|
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_x, float_bits(0.30)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryGreen + VkXYColorEXT_y, float_bits(0.60))
|
|
Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_x, float_bits(0.15)); Vk.put_i32(md, VkHdrMetadataEXT_displayPrimaryBlue + VkXYColorEXT_y, float_bits(0.06))
|
|
Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_x, float_bits(0.3127)); Vk.put_i32(md, VkHdrMetadataEXT_whitePoint + VkXYColorEXT_y, float_bits(0.3290))
|
|
Vk.put_i32(md, VkHdrMetadataEXT_maxLuminance, float_bits(r3d_hdr_peak_nits(render3d_st)))
|
|
Vk.put_i32(md, VkHdrMetadataEXT_minLuminance, float_bits(0.001))
|
|
Vk.put_i32(md, VkHdrMetadataEXT_maxContentLightLevel, float_bits(r3d_hdr_peak_nits(render3d_st)))
|
|
Vk.put_i32(md, VkHdrMetadataEXT_maxFrameAverageLightLevel, float_bits(r3d_hdr_paper_nits(render3d_st)))
|
|
let chains = bytes(8)
|
|
Vk.put_i64(chains, 0, render3d_st.gvk_swap)
|
|
Vk.set_hdr_metadata_ext(render3d_st.gvk_dev, 1, chains, md)
|
|
}
|
|
|
|
@alloc_ok("the window changed size or a surface was lost: the screen, the swapchain and its images are made again")
|
|
function gvk_screen_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
|
|
if render3d_st.gvk_vp == null {
|
|
render3d_st.gvk_vp = words(4); render3d_st.gvk_sc = words(5); render3d_st.gvk_clear_rgba = floats(4)
|
|
render3d_st.gvk_pass_col = words(4); render3d_st.gvk_pass_dep = words(2)
|
|
render3d_st.gvk_fb_ncolor = words(4096)
|
|
for i in 0 .. 4096 { render3d_st.gvk_fb_ncolor[i] = 1 }
|
|
for i in 0 .. 5 { render3d_st.gvk_sc[i] = 0 }
|
|
}
|
|
render3d_st.gvk_screen_w = w
|
|
render3d_st.gvk_screen_h = h
|
|
if render3d_st.gvk_screen_color == 0 { render3d_st.gvk_screen_color = gvk_tex_new(render3d_st); render3d_st.gvk_screen_depth = gvk_tex_new(render3d_st) }
|
|
render3d_st.gvk_vp[0] = 0; render3d_st.gvk_vp[1] = 0; render3d_st.gvk_vp[2] = w; render3d_st.gvk_vp[3] = h
|
|
return gvk_tex_storage(render3d_st, render3d_st.gvk_screen_color, false, gvk_screen_fmt(render3d_st), w, h, 1, false) and gvk_tex_storage(render3d_st, render3d_st.gvk_screen_depth, false, GL_DEPTH_COMPONENT32F, w, h, 1, false)
|
|
}
|
|
|
|
function gvk_frame_cb(render3d_st: mut Render3dState) -> pointer {
|
|
if render3d_st.gvk_cb == null {
|
|
# the frame in flight uses the other half of the ring and the other pool, unless it has this slot
|
|
if render3d_st.gvk_frame_pending and (render3d_st.gvk_frame_pending_no & 1) == (render3d_st.gvk_frame_no & 1) { gvk_frame_wait(render3d_st) }
|
|
gvk_frame_reset(render3d_st)
|
|
gvk_prime_submit(render3d_st)
|
|
render3d_st.gvk_cb = gvk_once_begin(render3d_st)
|
|
}
|
|
return render3d_st.gvk_cb
|
|
}
|
|
|
|
# The view a pass draws into: level 0 only (an attachment view has exactly one level, and a target
|
|
# the exposure measure mipmaps has several), and one layer of an array image for a cascade drawn
|
|
# on its own. Keyed by the image's generation, so a replaced image never reuses a stale view.
|
|
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
|
|
function gvk_view_of(render3d_st: mut Render3dState, tex: int, layer1: int) -> long {
|
|
if layer1 == 0 and render3d_st.gvk_tex_levels[tex] <= 1 and render3d_st.gvk_tex_layers[tex] <= 1 { return render3d_st.gvk_tex_view[tex] }
|
|
# keyed by numbers, not a string built on every call: the texture, its generation and the layer
|
|
let key = render3d_st.gvk_tex_gen[tex] * 4096 + layer1
|
|
if render3d_st.gvk_layer_views == null { render3d_st.gvk_layer_views = new []int; render3d_st.gvk_layer_view = new []long; render3d_st.gvk_layer_view_tex = new []int }
|
|
for i in 0 .. len(render3d_st.gvk_layer_views) { if render3d_st.gvk_layer_view_tex[i] == tex and render3d_st.gvk_layer_views[i] == key { return render3d_st.gvk_layer_view[i] } }
|
|
let depth = render3d_st.gvk_tex_vkfmt[tex] == VK_FORMAT_D32_SFLOAT
|
|
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, render3d_st.gvk_tex_image[tex])
|
|
Vk.put_i32(vci, VkImageViewCreateInfo_viewType, VK_IMAGE_VIEW_TYPE_2D)
|
|
Vk.put_i32(vci, VkImageViewCreateInfo_format, render3d_st.gvk_tex_vkfmt[tex])
|
|
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, 1)
|
|
if layer1 > 0 { Vk.put_i32(vci, sr + VkImageSubresourceRange_baseArrayLayer, layer1 - 1) }
|
|
Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, 1)
|
|
let out = bytes(8)
|
|
let zero: long = 0
|
|
render3d_st.gvk_mk_view += 1
|
|
let made = Vk.create_image_view(render3d_st.gvk_dev, vci, render3d_st.gvk_ac, out)
|
|
let view = gvk_handle(out)
|
|
free(vci); free(out)
|
|
if made != VK_SUCCESS { return zero }
|
|
push(render3d_st.gvk_layer_views, key)
|
|
push(render3d_st.gvk_layer_view, view)
|
|
push(render3d_st.gvk_layer_view_tex, tex)
|
|
return view
|
|
}
|
|
# the layer range a barrier for an attachment covers: the whole image unless one layer is drawn
|
|
function gvk_att_barrier(render3d_st: mut Render3dState, cb: pointer, tex: int, layer1: int, depth: bool, old_layout: int, new_layout: int) -> void {
|
|
# an attachment whose image was never made (no memory for it) has nothing to transition
|
|
if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 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, render3d_st.gvk_tex_image[tex])
|
|
let r = VkImageMemoryBarrier_subresourceRange
|
|
if depth { Vk.put_i32(b, r + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) } else { Vk.put_i32(b, r + VkImageSubresourceRange_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
|
|
Vk.put_i32(b, r + VkImageSubresourceRange_levelCount, 1)
|
|
if layer1 == 0 { Vk.put_i32(b, r + VkImageSubresourceRange_layerCount, render3d_st.gvk_tex_layers[tex]) }
|
|
else { Vk.put_i32(b, r + VkImageSubresourceRange_baseArrayLayer, layer1 - 1); Vk.put_i32(b, r + VkImageSubresourceRange_layerCount, 1) }
|
|
Vk.cmd_pipeline_barrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, null, 0, null, 1, b)
|
|
}
|
|
|
|
# fb's attachments from gpu.ludic's record (colour 0, colour 1, depth texture, depth layer + 1,
|
|
# colour rb, depth rb, samples, colour layer + 1), framebuffer 0 being the screen
|
|
function gvk_pass_collect(render3d_st: mut Render3dState, fb: int, rec: words, rec_o: int) -> void {
|
|
for i in 0 .. 4 { render3d_st.gvk_pass_col[i] = 0 }
|
|
render3d_st.gvk_pass_dep[0] = 0; render3d_st.gvk_pass_dep[1] = 0
|
|
render3d_st.gvk_pass_ncolor = 0
|
|
if fb == 0 {
|
|
render3d_st.gvk_pass_col[0] = render3d_st.gvk_screen_color; render3d_st.gvk_pass_ncolor = 1
|
|
render3d_st.gvk_pass_dep[0] = render3d_st.gvk_screen_depth
|
|
return
|
|
}
|
|
if rec_o < 0 { return }
|
|
var want = 1
|
|
if fb < 4096 { want = render3d_st.gvk_fb_ncolor[fb] }
|
|
for slot in 0 .. 2 {
|
|
if slot < want and rec[rec_o + slot] > 0 {
|
|
render3d_st.gvk_pass_col[render3d_st.gvk_pass_ncolor * 2] = rec[rec_o + slot]
|
|
if slot == 0 { render3d_st.gvk_pass_col[render3d_st.gvk_pass_ncolor * 2 + 1] = rec[rec_o + 7] }
|
|
render3d_st.gvk_pass_ncolor += 1
|
|
}
|
|
}
|
|
render3d_st.gvk_pass_dep[0] = rec[rec_o + 2]
|
|
render3d_st.gvk_pass_dep[1] = rec[rec_o + 3]
|
|
}
|
|
|
|
# A block-compressed image cannot be drawn into (Metal aborts, "BC7 is not color renderable"):
|
|
# an attachment that is one is dropped from the pass, and said, with what it was
|
|
function gvk_pass_no_compressed(render3d_st: mut Render3dState) -> void {
|
|
var w = 0
|
|
for c in 0 .. render3d_st.gvk_pass_ncolor {
|
|
let tex = render3d_st.gvk_pass_col[c * 2]
|
|
if tex > 0 and tex < len(render3d_st.gvk_tex_glfmt) and gvk_is_compressed(render3d_st.gvk_tex_glfmt[tex]) {
|
|
if render3d_st.gvk_bc_said < 8 {
|
|
render3d_st.gvk_bc_said += 1
|
|
gvk_say_compressed(render3d_st, tex)
|
|
}
|
|
} else {
|
|
render3d_st.gvk_pass_col[w * 2] = tex; render3d_st.gvk_pass_col[w * 2 + 1] = render3d_st.gvk_pass_col[c * 2 + 1]
|
|
w += 1
|
|
}
|
|
}
|
|
render3d_st.gvk_pass_ncolor = w
|
|
}
|
|
function gvk_pass_begin(render3d_st: mut Render3dState, rec: words, rec_o: int) -> void {
|
|
if render3d_st.gvk_in_pass { return }
|
|
let cb = gvk_frame_cb(render3d_st)
|
|
gvk_pass_collect(render3d_st, render3d_st.gvk_fb_cur, rec, rec_o)
|
|
gvk_pass_no_compressed(render3d_st)
|
|
let aw = VkRenderingAttachmentInfo_sizeof
|
|
let catt = gvk_tmp(render3d_st, aw * 3)
|
|
Vk.zero(catt, aw * 3)
|
|
render3d_st.gvk_pass_w = 0
|
|
render3d_st.gvk_pass_h = 0
|
|
render3d_st.gvk_pass_cfmt = VK_FORMAT_UNDEFINED
|
|
render3d_st.gvk_pass_dfmt = VK_FORMAT_UNDEFINED
|
|
render3d_st.gvk_pass_samples = 1
|
|
for c in 0 .. render3d_st.gvk_pass_ncolor {
|
|
let tex = render3d_st.gvk_pass_col[c * 2]
|
|
let layer1 = render3d_st.gvk_pass_col[c * 2 + 1]
|
|
gvk_att_barrier(render3d_st, cb, tex, layer1, false, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)
|
|
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO)
|
|
Vk.put_i64(catt, c * aw + VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, tex, layer1))
|
|
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_imageLayout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL)
|
|
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE)
|
|
if (render3d_st.gvk_clear_bits & GL_COLOR_BUFFER_BIT) != 0 {
|
|
Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_CLEAR)
|
|
for k in 0 .. 4 { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_clearValue + k * 4, float_bits(render3d_st.gvk_clear_rgba[k])) }
|
|
} else { Vk.put_i32(catt, c * aw + VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD) }
|
|
render3d_st.gvk_pass_cfmt = render3d_st.gvk_tex_vkfmt[tex]
|
|
render3d_st.gvk_pass_samples = gvk_tex_samples_of(render3d_st, tex)
|
|
if render3d_st.gvk_pass_w == 0 { render3d_st.gvk_pass_w = gvk_tex_w(render3d_st, tex); render3d_st.gvk_pass_h = gvk_tex_h(render3d_st, tex) }
|
|
}
|
|
let datt = gvk_tmp(render3d_st, aw)
|
|
Vk.zero(datt, aw)
|
|
let dtex = render3d_st.gvk_pass_dep[0]
|
|
if dtex > 0 {
|
|
gvk_att_barrier(render3d_st, cb, dtex, render3d_st.gvk_pass_dep[1], true, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL)
|
|
Vk.put_i32(datt, VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO)
|
|
Vk.put_i64(datt, VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, dtex, render3d_st.gvk_pass_dep[1]))
|
|
Vk.put_i32(datt, VkRenderingAttachmentInfo_imageLayout, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL)
|
|
Vk.put_i32(datt, VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE)
|
|
if (render3d_st.gvk_clear_bits & GL_DEPTH_BUFFER_BIT) != 0 {
|
|
Vk.put_i32(datt, VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_CLEAR)
|
|
Vk.put_i32(datt, VkRenderingAttachmentInfo_clearValue, 0x3F800000)
|
|
} else { Vk.put_i32(datt, VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD) }
|
|
render3d_st.gvk_pass_dfmt = VK_FORMAT_D32_SFLOAT
|
|
render3d_st.gvk_pass_samples = gvk_tex_samples_of(render3d_st, dtex)
|
|
if render3d_st.gvk_pass_w == 0 { render3d_st.gvk_pass_w = gvk_tex_w(render3d_st, dtex); render3d_st.gvk_pass_h = gvk_tex_h(render3d_st, dtex) }
|
|
}
|
|
render3d_st.gvk_clear_bits = 0
|
|
let ri = gvk_tmp(render3d_st, VkRenderingInfo_sizeof)
|
|
Vk.zero(ri, VkRenderingInfo_sizeof)
|
|
Vk.put_i32(ri, VkRenderingInfo_sType, VK_STRUCTURE_TYPE_RENDERING_INFO)
|
|
Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w)
|
|
Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_pass_h)
|
|
Vk.put_i32(ri, VkRenderingInfo_layerCount, 1)
|
|
Vk.put_i32(ri, VkRenderingInfo_colorAttachmentCount, render3d_st.gvk_pass_ncolor)
|
|
Vk.put_ptr(ri, VkRenderingInfo_pColorAttachments, catt)
|
|
if dtex > 0 { Vk.put_ptr(ri, VkRenderingInfo_pDepthAttachment, datt) }
|
|
Vk.cmd_begin_rendering(cb, ri)
|
|
render3d_st.gvk_in_pass = true
|
|
}
|
|
|
|
function gvk_pass_end(render3d_st: mut Render3dState) -> void {
|
|
if not render3d_st.gvk_in_pass { return }
|
|
let cb = render3d_st.gvk_cb
|
|
Vk.cmd_end_rendering(cb)
|
|
for c in 0 .. render3d_st.gvk_pass_ncolor {
|
|
gvk_att_barrier(render3d_st, cb, render3d_st.gvk_pass_col[c * 2], render3d_st.gvk_pass_col[c * 2 + 1], false, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
}
|
|
if render3d_st.gvk_pass_dep[0] > 0 {
|
|
gvk_att_barrier(render3d_st, cb, render3d_st.gvk_pass_dep[0], render3d_st.gvk_pass_dep[1], true, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
}
|
|
render3d_st.gvk_in_pass = false
|
|
}
|
|
|
|
# a clear: the load op of a pass that has not begun, an explicit clear inside one that has
|
|
function gvk_clear(render3d_st: mut Render3dState, mask: int, rec: words, rec_o: int) -> void {
|
|
if not render3d_st.gvk_in_pass { render3d_st.gvk_clear_bits = render3d_st.gvk_clear_bits | mask; return }
|
|
let cb = render3d_st.gvk_cb
|
|
let caw = VkClearAttachment_sizeof
|
|
let atts = gvk_tmp(render3d_st, caw * 4)
|
|
Vk.zero(atts, caw * 4)
|
|
var n = 0
|
|
if (mask & GL_COLOR_BUFFER_BIT) != 0 {
|
|
for c in 0 .. render3d_st.gvk_pass_ncolor {
|
|
Vk.put_i32(atts, n * caw + VkClearAttachment_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT)
|
|
Vk.put_i32(atts, n * caw + VkClearAttachment_colorAttachment, c)
|
|
for k in 0 .. 4 { Vk.put_i32(atts, n * caw + VkClearAttachment_clearValue + k * 4, float_bits(render3d_st.gvk_clear_rgba[k])) }
|
|
n += 1
|
|
}
|
|
}
|
|
if (mask & GL_DEPTH_BUFFER_BIT) != 0 and render3d_st.gvk_pass_dep[0] > 0 {
|
|
Vk.put_i32(atts, n * caw + VkClearAttachment_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT)
|
|
Vk.put_i32(atts, n * caw + VkClearAttachment_clearValue, 0x3F800000)
|
|
n += 1
|
|
}
|
|
if n == 0 { return }
|
|
let rect = gvk_tmp(render3d_st, VkClearRect_sizeof)
|
|
Vk.zero(rect, VkClearRect_sizeof)
|
|
Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w)
|
|
Vk.put_i32(rect, VkClearRect_rect + VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_pass_h)
|
|
Vk.put_i32(rect, VkClearRect_layerCount, 1)
|
|
Vk.cmd_clear_attachments(cb, n, atts, 1, rect)
|
|
}
|
|
|
|
function gvk_tex_w(render3d_st: Render3dState, tex: int) -> int { return render3d_st.gvk_tex_dims_w[tex] }
|
|
function gvk_tex_h(render3d_st: Render3dState, tex: int) -> int { return render3d_st.gvk_tex_dims_h[tex] }
|
|
|
|
# viewport and scissor for the draw; OpenGL's rows count from the bottom and so do a Vulkan
|
|
# target's here (no y flip), so both pass straight through
|
|
function gvk_set_view(render3d_st: mut Render3dState, cb: pointer) -> void {
|
|
let vp = gvk_tmp(render3d_st, VkViewport_sizeof)
|
|
Vk.zero(vp, VkViewport_sizeof)
|
|
Vk.put_i32(vp, VkViewport_x, float_bits(float(render3d_st.gvk_vp[0])))
|
|
Vk.put_i32(vp, VkViewport_y, float_bits(float(render3d_st.gvk_vp[1])))
|
|
Vk.put_i32(vp, VkViewport_width, float_bits(float(render3d_st.gvk_vp[2])))
|
|
Vk.put_i32(vp, VkViewport_height, float_bits(float(render3d_st.gvk_vp[3])))
|
|
Vk.put_i32(vp, VkViewport_maxDepth, 0x3F800000)
|
|
Vk.cmd_set_viewport(cb, 0, 1, vp)
|
|
let sc = gvk_tmp(render3d_st, VkRect2D_sizeof)
|
|
Vk.zero(sc, VkRect2D_sizeof)
|
|
if render3d_st.gvk_sc[0] == 1 {
|
|
Vk.put_i32(sc, VkRect2D_offset + VkOffset2D_x, render3d_st.gvk_sc[1])
|
|
Vk.put_i32(sc, VkRect2D_offset + VkOffset2D_y, render3d_st.gvk_sc[2])
|
|
Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_sc[3])
|
|
Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_sc[4])
|
|
} else {
|
|
Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_width, render3d_st.gvk_pass_w)
|
|
Vk.put_i32(sc, VkRect2D_extent + VkExtent2D_height, render3d_st.gvk_pass_h)
|
|
}
|
|
Vk.cmd_set_scissor(cb, 0, 1, sc)
|
|
}
|
|
|
|
# A draw of mesh m with program p and state st: the pipeline, the view, the set, the buffers.
|
|
# first / count select vertices or indices; count 0 means the mesh's own count. instances >= 1.
|
|
function gvk_draw(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, first: int, count: int, instances: int, tx: words, tx_w: int, tx_cap: int, rec: words, rec_o: int) -> void {
|
|
if render3d_st.gvk_prog_var == null or p <= 0 or p >= len(render3d_st.gvk_prog_var) or render3d_st.gvk_prog_var[p] == null { return }
|
|
let prof = gvk_prof(render3d_st)
|
|
var t0: long = 0
|
|
if prof { t0 = gl_now_us() }
|
|
gvk_pass_begin(render3d_st, rec, rec_o)
|
|
let cb = render3d_st.gvk_cb
|
|
let samples = render3d_st.gvk_pass_samples
|
|
let pipe = gvk_pipeline_fast(render3d_st, p, m, st, render3d_st.gvk_pass_ncolor, render3d_st.gvk_pass_cfmt, render3d_st.gvk_pass_dfmt, samples)
|
|
if pipe == 0 {
|
|
# Say so, once per program: a driver that refuses a pipeline other drivers accept (MoltenVK
|
|
# rejected every skinned one) otherwise leaves a whole layer missing from the frame in silence.
|
|
if p < 4096 and render3d_st.gvk_skip_said[p] == 0 {
|
|
render3d_st.gvk_skip_said[p] = 1
|
|
gvk_say_no_pipeline(render3d_st, p)
|
|
}
|
|
return
|
|
}
|
|
var t1: long = 0
|
|
if prof { t1 = gl_now_us(); render3d_st.gvk_us_pipe = render3d_st.gvk_us_pipe + (t1 - t0) }
|
|
Vk.cmd_bind_pipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipe)
|
|
gvk_set_view(render3d_st, cb)
|
|
let set = gvk_draw_set(render3d_st, p, tx, tx_w, tx_cap)
|
|
if set == 0 { return }
|
|
if prof { render3d_st.gvk_us_set = render3d_st.gvk_us_set + (gl_now_us() - t1) }
|
|
let sets = gvk_tmp(render3d_st, 8)
|
|
Vk.put_i64(sets, 0, set)
|
|
Vk.cmd_bind_descriptor_sets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, render3d_st.gvk_prog_layout[p], 0, 1, sets, render3d_st.gvk_set_ndyn, render3d_st.gvk_set_offs)
|
|
let v = render3d_st.gvk_prog_var[p]
|
|
if m != null and m.attrs != null {
|
|
# the same bindings, in the same order, as gvk_pipeline gave the layout
|
|
let bufs = gvk_tmp(render3d_st, 8 * (GPU_MAX_VBUFS + 1))
|
|
let offs = gvk_tmp(render3d_st, 8 * (GPU_MAX_VBUFS + 1))
|
|
Vk.zero(offs, 8 * (GPU_MAX_VBUFS + 1))
|
|
let seen = render3d_st.gvk_seen
|
|
var nbd = 0
|
|
for i in 0 .. m.n_attrs {
|
|
let o = i * GPU_ATTR_W
|
|
if m.attrs[o + 1] == 0 { continue }
|
|
var wanted = false
|
|
for q in 0 .. len(v.i_loc) { if v.i_loc[q] == i { wanted = true } }
|
|
if not wanted { continue }
|
|
var known = false
|
|
for q in 0 .. nbd { if seen[q] == m.attrs[o] { known = true } }
|
|
if not known and nbd < GPU_MAX_VBUFS {
|
|
seen[nbd] = m.attrs[o]
|
|
Vk.put_i64(bufs, nbd * 8, render3d_st.gvk_buf[m.attrs[o]])
|
|
render3d_st.gvk_buf_used[m.attrs[o]] = render3d_st.gvk_frame_no
|
|
nbd += 1
|
|
}
|
|
}
|
|
var unfed = false
|
|
for q in 0 .. len(v.i_loc) { if gvk_input_unfed(m, v.i_loc[q]) { unfed = true } }
|
|
if unfed and nbd <= GPU_MAX_VBUFS {
|
|
let zb = gvk_zero_vbuf_get(render3d_st)
|
|
Vk.put_i64(bufs, nbd * 8, render3d_st.gvk_buf[zb])
|
|
render3d_st.gvk_buf_used[zb] = render3d_st.gvk_frame_no
|
|
nbd += 1
|
|
}
|
|
if nbd > 0 { Vk.cmd_bind_vertex_buffers(cb, 0, nbd, bufs, offs) }
|
|
}
|
|
var n = count
|
|
if n == 0 and m != null { n = m.count }
|
|
if render3d_st.gvk_mesh_x > 0 {
|
|
# a mesh-shader dispatch (gpu_draw_mesh_tasks): the device's command, through a pointer
|
|
Vk.sl_call_piii(render3d_st.gvk_mesh_fn, cb, render3d_st.gvk_mesh_x, render3d_st.gvk_mesh_y, render3d_st.gvk_mesh_z)
|
|
if prof { render3d_st.gvk_n_draws += 1; render3d_st.gvk_us_draw = render3d_st.gvk_us_draw + (gl_now_us() - t0) }
|
|
return
|
|
}
|
|
if m != null and m.ebo != 0 {
|
|
let zero: long = 0
|
|
var itype = VK_INDEX_TYPE_UINT32
|
|
if m.itype == GL_UNSIGNED_SHORT { itype = VK_INDEX_TYPE_UINT16 }
|
|
Vk.cmd_bind_index_buffer(cb, render3d_st.gvk_buf[m.ebo], zero, itype)
|
|
render3d_st.gvk_buf_used[m.ebo] = render3d_st.gvk_frame_no
|
|
if render3d_st.gvk_ind_buf > 0 {
|
|
# the draws are records in a buffer (gvk_draw_indirect_now); the GPU may have written them
|
|
let ioff: long = render3d_st.gvk_ind_off
|
|
render3d_st.gvk_buf_used[render3d_st.gvk_ind_buf] = render3d_st.gvk_frame_no
|
|
if render3d_st.gvk_ind_cbuf > 0 and render3d_st.gvk_has_dic {
|
|
let coff: long = render3d_st.gvk_ind_coff
|
|
render3d_st.gvk_buf_used[render3d_st.gvk_ind_cbuf] = render3d_st.gvk_frame_no
|
|
Vk.cmd_draw_indexed_indirect_count(cb, render3d_st.gvk_buf[render3d_st.gvk_ind_buf], ioff, render3d_st.gvk_buf[render3d_st.gvk_ind_cbuf], coff, render3d_st.gvk_ind_n, VkDrawIndexedIndirectCommand_sizeof)
|
|
} else {
|
|
Vk.cmd_draw_indexed_indirect(cb, render3d_st.gvk_buf[render3d_st.gvk_ind_buf], ioff, render3d_st.gvk_ind_n, VkDrawIndexedIndirectCommand_sizeof)
|
|
}
|
|
} else {
|
|
Vk.cmd_draw_indexed(cb, n, instances, first, 0, 0)
|
|
}
|
|
} else {
|
|
Vk.cmd_draw(cb, n, instances, first, 0)
|
|
}
|
|
if prof { render3d_st.gvk_n_draws += 1; render3d_st.gvk_us_draw = render3d_st.gvk_us_draw + (gl_now_us() - t0) }
|
|
}
|
|
|
|
# The frame so far, submitted and waited for, so work that submits on its own - an upload, a
|
|
# read-back, a new or freed image or buffer - happens after the draws recorded before it, in the
|
|
# order OpenGL would have done them. Costs a submit per such call while the backend comes up.
|
|
function gvk_flush(render3d_st: mut Render3dState) -> void {
|
|
if render3d_st.gvk_cb == null { return }
|
|
render3d_st.gvk_n_flush += 1
|
|
gvk_pass_end(render3d_st)
|
|
gvk_labels_close(render3d_st)
|
|
gvk_once_end(render3d_st, render3d_st.gvk_cb)
|
|
render3d_st.gvk_cb = null
|
|
render3d_st.gvk_frame_no += 1
|
|
gvk_retire_flush(render3d_st)
|
|
}
|
|
|
|
# The finished frame: submitted and waited for. With a window, the screen image is blitted into
|
|
# the swapchain's next image first - flipped, since the screen image keeps OpenGL's bottom-up rows
|
|
# - and that image is presented.
|
|
function gvk_present(render3d_st: mut Render3dState) -> void {
|
|
gvk_prof_frame(render3d_st)
|
|
if render3d_st.gvk_swap != 0 {
|
|
gvk_present_window(render3d_st)
|
|
render3d_st.gvk_frame_no += 1
|
|
# what the frame in flight retired goes when it is done (gvk_frame_wait)
|
|
if not render3d_st.gvk_frame_pending { gvk_retire_flush(render3d_st) }
|
|
return
|
|
}
|
|
if render3d_st.gvk_cb == null { return }
|
|
gvk_pass_end(render3d_st)
|
|
gvk_labels_close(render3d_st)
|
|
gvk_once_end(render3d_st, render3d_st.gvk_cb)
|
|
render3d_st.gvk_cb = null
|
|
render3d_st.gvk_frame_no += 1
|
|
gvk_retire_flush(render3d_st)
|
|
}
|
|
|
|
# The screen as a binary PPM, top row first. The frame so far is finished first, then read back;
|
|
# row 0 of the image is OpenGL's bottom row, so rows are written last to first, as gl_screenshot does.
|
|
@alloc_ok("asked for by the player or a tool, not by the frame")
|
|
function gvk_screenshot(render3d_st: mut Render3dState, path: string) -> bool {
|
|
# the screen image is PQ-encoded 10-bit while HDR is on, which an 8-bit PPM would misread
|
|
if render3d_st.gvk_hdr_on { print("r3d: vulkan: screenshots are SDR only - turn HDR output off to take one"); return false }
|
|
gvk_present(render3d_st)
|
|
let w = render3d_st.gvk_screen_w
|
|
let h = render3d_st.gvk_screen_h
|
|
# the read-back, the header and the row are this shot's own and go on every way out: a buffer the
|
|
# size of the screen was once kept per shot
|
|
let px = bytes(w * h * 4)
|
|
if not gvk_tex_read(render3d_st, render3d_st.gvk_screen_color, GL_RGBA8, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px) {
|
|
free(px)
|
|
return false
|
|
}
|
|
let f = file_open(path, "wb")
|
|
if f == null {
|
|
free(px)
|
|
return false
|
|
}
|
|
let hdr = `P6\n{w} {h}\n255\n`
|
|
file_write(f, hdr, len(hdr))
|
|
free(hdr)
|
|
let row = bytes(w * 3)
|
|
var y = h - 1
|
|
while y >= 0 {
|
|
for x in 0 .. w {
|
|
let o = (y * w + x) * 4
|
|
row[x * 3] = px[o]; row[x * 3 + 1] = px[o + 1]; row[x * 3 + 2] = px[o + 2]
|
|
}
|
|
file_write(f, row, w * 3)
|
|
y -= 1
|
|
}
|
|
file_close(f)
|
|
free(row)
|
|
free(px)
|
|
return true
|
|
}
|
|
|
|
# ---- what gpu.ludic's Vulkan branches call -----------------------------------------------------
|
|
|
|
# the manifest the programs are looked up in, from the renderer's own shader directory
|
|
@alloc_ok("start-up: the device, its tables, the programs, the passes and the world's first textures are made once, before play")
|
|
function gvk_manifest(render3d_st: mut Render3dState) -> bool {
|
|
r3d_find_root(render3d_st)
|
|
render3d_st.gvk_spv_dir = `{render3d_st.r3d_root}/shaders/spv`
|
|
return gpu_manifest_load(render3d_st, `{render3d_st.gvk_spv_dir}/manifest.txt`) > 0 and len(render3d_st.gpu_variants) > 0
|
|
}
|
|
|
|
# The screen: a colour and a depth image. Headless they are the asked-for size; with a window
|
|
# they are its client area in pixels, and the swapchain is made on it.
|
|
function gvk_open(render3d_st: mut Render3dState, w: int, h: int, title: string) -> bool {
|
|
gl_set_drawable(w, h)
|
|
if is_windowed() {
|
|
win_open(w, h, 1, title) # the window exists before main: this retitles it
|
|
win_gl_resize(w, h)
|
|
# macOS: the Metal layer sets the backing scale, so it exists before the drawable is measured
|
|
if Os.platform() == "macos" and win_metal_layer() == null { render3d_st.gvk_why = "no Metal layer on the window"; return false }
|
|
win_gl_drawable(render3d_st.gvk_size_buf)
|
|
if render3d_st.gvk_size_buf[0] > 0 and render3d_st.gvk_size_buf[1] > 0 { gl_set_drawable(render3d_st.gvk_size_buf[0], render3d_st.gvk_size_buf[1]) }
|
|
gl_set_pixel_scale(win_gl_scale())
|
|
}
|
|
if not gvk_frame_init(render3d_st) { return false }
|
|
if not gvk_screen_make(render3d_st, gl_width(), gl_height()) { return false }
|
|
if r3d_env_has(render3d_st, "R3D_VK_PROBE") { gvk_compute_probe(render3d_st) }
|
|
if is_windowed() { return gvk_swap_make(render3d_st, gl_width(), gl_height()) }
|
|
return true
|
|
}
|
|
|
|
# A variant is made once and shared: a program is immutable here, so every actor asking for the
|
|
# one it wants (actor.ludic, one per actor) gets the same handle, not its own modules and pipelines.
|
|
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
|
|
function gvk_program_new(render3d_st: mut Render3dState, vs: string, fs: string, defines: string) -> int {
|
|
let key = `{vs}|{fs}|{Text.replace(defines, "\n", ";")}`
|
|
if render3d_st.gpu_prog_ids == null { render3d_st.gpu_prog_ids = new []int; render3d_st.gpu_prog_keys = new []string }
|
|
for i in 0 .. len(render3d_st.gpu_prog_keys) {
|
|
if render3d_st.gpu_prog_keys[i] == key {
|
|
free(key)
|
|
return render3d_st.gpu_prog_ids[i]
|
|
}
|
|
}
|
|
render3d_st.gvk_prog_counter += 1
|
|
let p = render3d_st.gvk_prog_counter
|
|
push(render3d_st.gpu_prog_ids, p)
|
|
push(render3d_st.gpu_prog_keys, key)
|
|
# a variant that cannot be made stays 0 for everyone who asks for it later
|
|
if not gvk_program(render3d_st, p, key, render3d_st.gvk_spv_dir) {
|
|
render3d_st.gpu_prog_ids[len(render3d_st.gpu_prog_ids) - 1] = 0
|
|
return 0
|
|
}
|
|
return p
|
|
}
|
|
|
|
function gvk_mesh_free(render3d_st: mut Render3dState, m: Mesh) -> void {
|
|
if m == null { return }
|
|
if m.vbufs != null { for i in 0 .. m.n_vbufs { if m.vbufs[i] > 0 { gvk_buf_delete(render3d_st, m.vbufs[i]) } } }
|
|
m.n_vbufs = 0
|
|
m.vbo = 0
|
|
if m.ebo > 0 { gvk_buf_delete(render3d_st, m.ebo); m.ebo = 0 }
|
|
}
|
|
|
|
function gvk_scissor(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void {
|
|
if render3d_st.gvk_sc == null { return }
|
|
render3d_st.gvk_sc[0] = 1; render3d_st.gvk_sc[1] = x; render3d_st.gvk_sc[2] = y; render3d_st.gvk_sc[3] = w; render3d_st.gvk_sc[4] = h
|
|
}
|
|
function gvk_scissor_off(render3d_st: mut Render3dState) -> void { if render3d_st.gvk_sc != null { render3d_st.gvk_sc[0] = 0 } }
|
|
function gvk_viewport(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void {
|
|
if render3d_st.gvk_vp == null { return }
|
|
render3d_st.gvk_vp[0] = x; render3d_st.gvk_vp[1] = y; render3d_st.gvk_vp[2] = w; render3d_st.gvk_vp[3] = h
|
|
}
|
|
function gvk_clear_color(render3d_st: mut Render3dState, r: float, g: float, b: float, a: float) -> void {
|
|
if render3d_st.gvk_clear_rgba == null { return }
|
|
render3d_st.gvk_clear_rgba[0] = r; render3d_st.gvk_clear_rgba[1] = g; render3d_st.gvk_clear_rgba[2] = b; render3d_st.gvk_clear_rgba[3] = a
|
|
}
|
|
function gvk_fb_colors(render3d_st: mut Render3dState, fb: int, n: int) -> void { if render3d_st.gvk_fb_ncolor != null and fb >= 0 and fb < 4096 { render3d_st.gvk_fb_ncolor[fb] = n } }
|
|
# The framebuffer changes: a clear still waiting for this one's pass runs now, on this target,
|
|
# rather than becoming the load op of whichever pass begins next.
|
|
function gvk_rebind(render3d_st: mut Render3dState, fb: int) -> void {
|
|
if fb == render3d_st.gvk_fb_cur { return }
|
|
if not render3d_st.gvk_in_pass and render3d_st.gvk_clear_bits != 0 { gvk_pass_begin(render3d_st, render3d_st.gpu_fb, gpu_fb_at(render3d_st, render3d_st.gvk_fb_cur)) }
|
|
gvk_pass_end(render3d_st)
|
|
render3d_st.gvk_fb_cur = fb
|
|
}
|
|
function gvk_fb_forget(render3d_st: mut Render3dState, fb: int) -> void {
|
|
if fb == render3d_st.gvk_fb_cur { gvk_pass_end(render3d_st) }
|
|
gvk_fb_colors(render3d_st, fb, 1)
|
|
}
|
|
|
|
# the render state gpu.ludic has cached, with OpenGL's defaults where nothing was set yet
|
|
function gvk_state_now(render3d_st: mut Render3dState) -> GvkState {
|
|
let st = render3d_st.gvk_state
|
|
st.depth_test = 0; if render3d_st.gpu_s_depth_test == 1 { st.depth_test = 1 }
|
|
st.depth_write = 1; if render3d_st.gpu_s_depth_write == 0 { st.depth_write = 0 }
|
|
st.depth_func = GL_LESS; if render3d_st.gpu_s_depth_func > 0 { st.depth_func = render3d_st.gpu_s_depth_func }
|
|
st.blend = 0; if render3d_st.gpu_s_blend == 1 { st.blend = 1 }
|
|
st.blend_src = GL_ONE; if render3d_st.gpu_s_blend_src >= 0 { st.blend_src = render3d_st.gpu_s_blend_src }
|
|
st.blend_dst = GL_ZERO; if render3d_st.gpu_s_blend_dst >= 0 { st.blend_dst = render3d_st.gpu_s_blend_dst }
|
|
st.cull = 0; if render3d_st.gpu_s_cull == 1 { st.cull = 1 }
|
|
st.cull_face = GL_BACK; if render3d_st.gpu_s_cull_face > 0 { st.cull_face = render3d_st.gpu_s_cull_face }
|
|
st.color_write = 1; if render3d_st.gpu_s_color_write == 0 { st.color_write = 0 }
|
|
st.a2c = 0; if render3d_st.gpu_s_a2c == 1 { st.a2c = 1 }
|
|
st.bias = 0; if render3d_st.gpu_s_bias == 1 { st.bias = 1 }
|
|
st.bias_factor = float_bits(render3d_st.gpu_s_bias_f)
|
|
st.bias_units = float_bits(render3d_st.gpu_s_bias_u)
|
|
st.wireframe = render3d_st.gvk_wireframe
|
|
return st
|
|
}
|
|
# An indirect draw goes through gvk_draw like any other - the same pipeline, set and buffers -
|
|
# and only its last call differs, so the record is handed over in these for that one draw.
|
|
# x * y * z mesh-shader invocations with the current program (a *.mesh one) and state
|
|
function gvk_draw_mesh_tasks_now(render3d_st: mut Render3dState, x: int, y: int, z: int) -> void {
|
|
if not render3d_st.gvk_has_mesh or render3d_st.gvk_mesh_fn == null or x <= 0 or y <= 0 or z <= 0 { return }
|
|
render3d_st.gvk_mesh_x = x; render3d_st.gvk_mesh_y = y; render3d_st.gvk_mesh_z = z
|
|
gvk_draw_now(render3d_st, null, 0, 0, 1)
|
|
render3d_st.gvk_mesh_x = 0
|
|
}
|
|
function gvk_draw_indirect_now(render3d_st: mut Render3dState, m: Mesh, cmds: int, offset: int, n: int, count_buf: int, count_off: int) -> void {
|
|
if m == null or m.ebo == 0 or cmds <= 0 or n <= 0 { return }
|
|
render3d_st.gvk_ind_buf = cmds; render3d_st.gvk_ind_off = offset; render3d_st.gvk_ind_n = n; render3d_st.gvk_ind_cbuf = count_buf; render3d_st.gvk_ind_coff = count_off
|
|
gvk_draw_now(render3d_st, m, 0, 0, 1)
|
|
render3d_st.gvk_ind_buf = 0; render3d_st.gvk_ind_cbuf = 0
|
|
}
|
|
|
|
function gvk_draw_now(render3d_st: mut Render3dState, m: Mesh, first: int, count: int, instances: int) -> void {
|
|
if gvk_prof(render3d_st) { render3d_st.gvk_n_asked += 1 }
|
|
gvk_draw(render3d_st, render3d_st.gpu_prog_cur, m, gvk_state_now(render3d_st), first, count, instances, render3d_st.gpu_tx, GPU_TX_W, render3d_st.gpu_tx_cap, render3d_st.gpu_fb, gpu_fb_at(render3d_st, render3d_st.gvk_fb_cur))
|
|
}
|
|
|
|
# the colour (and / or depth) of the read framebuffer into the draw framebuffer, same size
|
|
function gvk_fb_att(render3d_st: mut Render3dState, fb: int, depth: bool) -> int {
|
|
if fb == 0 { if depth { return render3d_st.gvk_screen_depth }; return render3d_st.gvk_screen_color }
|
|
let o = gpu_fb_at(render3d_st, fb)
|
|
if o < 0 { return 0 }
|
|
if depth { return render3d_st.gpu_fb[o + 2] }
|
|
return render3d_st.gpu_fb[o]
|
|
}
|
|
function gvk_tex_samples_of(render3d_st: Render3dState, tex: int) -> int {
|
|
if tex <= 0 or render3d_st.gvk_tex_samples == null or tex >= len(render3d_st.gvk_tex_samples) or render3d_st.gvk_tex_samples[tex] < 1 { return 1 }
|
|
return render3d_st.gvk_tex_samples[tex]
|
|
}
|
|
# A multisampled image into a single-sampled one: a pass that draws nothing and resolves on its end -
|
|
# colour averaged, depth from sample zero. vkCmdResolveImage cannot resolve depth; a pass can.
|
|
function gvk_resolve(render3d_st: mut Render3dState, cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void {
|
|
var layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
|
|
var mode = VK_RESOLVE_MODE_AVERAGE_BIT
|
|
if depth { layout = VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL; mode = VK_RESOLVE_MODE_SAMPLE_ZERO_BIT }
|
|
gvk_att_barrier(render3d_st, cb, src, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout)
|
|
gvk_att_barrier(render3d_st, cb, dst, 0, depth, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, layout)
|
|
let aw = VkRenderingAttachmentInfo_sizeof
|
|
let att = gvk_tmp(render3d_st, aw)
|
|
Vk.zero(att, aw)
|
|
Vk.put_i32(att, VkRenderingAttachmentInfo_sType, VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO)
|
|
Vk.put_i64(att, VkRenderingAttachmentInfo_imageView, gvk_view_of(render3d_st, src, 0))
|
|
Vk.put_i32(att, VkRenderingAttachmentInfo_imageLayout, layout)
|
|
Vk.put_i32(att, VkRenderingAttachmentInfo_resolveMode, mode)
|
|
Vk.put_i64(att, VkRenderingAttachmentInfo_resolveImageView, gvk_view_of(render3d_st, dst, 0))
|
|
Vk.put_i32(att, VkRenderingAttachmentInfo_resolveImageLayout, layout)
|
|
Vk.put_i32(att, VkRenderingAttachmentInfo_loadOp, VK_ATTACHMENT_LOAD_OP_LOAD)
|
|
Vk.put_i32(att, VkRenderingAttachmentInfo_storeOp, VK_ATTACHMENT_STORE_OP_STORE)
|
|
let ri = gvk_tmp(render3d_st, VkRenderingInfo_sizeof)
|
|
Vk.zero(ri, VkRenderingInfo_sizeof)
|
|
Vk.put_i32(ri, VkRenderingInfo_sType, VK_STRUCTURE_TYPE_RENDERING_INFO)
|
|
Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_width, w)
|
|
Vk.put_i32(ri, VkRenderingInfo_renderArea + VkRect2D_extent + VkExtent2D_height, h)
|
|
Vk.put_i32(ri, VkRenderingInfo_layerCount, 1)
|
|
if depth { Vk.put_ptr(ri, VkRenderingInfo_pDepthAttachment, att) }
|
|
else { Vk.put_i32(ri, VkRenderingInfo_colorAttachmentCount, 1); Vk.put_ptr(ri, VkRenderingInfo_pColorAttachments, att) }
|
|
Vk.cmd_begin_rendering(cb, ri)
|
|
Vk.cmd_end_rendering(cb)
|
|
gvk_att_barrier(render3d_st, cb, src, 0, depth, layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
gvk_att_barrier(render3d_st, cb, dst, 0, depth, layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
}
|
|
function gvk_copy(render3d_st: mut Render3dState, cb: pointer, src: int, dst: int, depth: bool, w: int, h: int) -> void {
|
|
if src <= 0 or dst <= 0 or render3d_st.gvk_tex_image[src] == 0 or render3d_st.gvk_tex_image[dst] == 0 { return }
|
|
if gvk_tex_samples_of(render3d_st, src) > 1 and gvk_tex_samples_of(render3d_st, dst) == 1 { gvk_resolve(render3d_st, cb, src, dst, depth, w, h); return }
|
|
gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[src], depth, 0, 1, render3d_st.gvk_tex_layers[src], VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
|
|
gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[dst], depth, 0, 1, render3d_st.gvk_tex_layers[dst], 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)
|
|
var aspect = VK_IMAGE_ASPECT_COLOR_BIT
|
|
if depth { aspect = VK_IMAGE_ASPECT_DEPTH_BIT }
|
|
Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_aspectMask, aspect)
|
|
Vk.put_i32(ic, VkImageCopy_srcSubresource + VkImageSubresourceLayers_layerCount, 1)
|
|
Vk.put_i32(ic, VkImageCopy_dstSubresource + VkImageSubresourceLayers_aspectMask, aspect)
|
|
Vk.put_i32(ic, VkImageCopy_dstSubresource + VkImageSubresourceLayers_layerCount, 1)
|
|
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, render3d_st.gvk_tex_image[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, render3d_st.gvk_tex_image[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ic)
|
|
gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[src], depth, 0, 1, render3d_st.gvk_tex_layers[src], VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[dst], depth, 0, 1, render3d_st.gvk_tex_layers[dst], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
}
|
|
function gvk_blit(render3d_st: mut Render3dState, w: int, h: int, mask: int) -> void {
|
|
gvk_pass_end(render3d_st)
|
|
let cb = gvk_frame_cb(render3d_st)
|
|
if (mask & GL_COLOR_BUFFER_BIT) != 0 { gvk_copy(render3d_st, cb, gvk_fb_att(render3d_st, render3d_st.gvk_fb_read, false), gvk_fb_att(render3d_st, render3d_st.gvk_fb_draw, false), false, w, h) }
|
|
if (mask & GL_DEPTH_BUFFER_BIT) != 0 { gvk_copy(render3d_st, cb, gvk_fb_att(render3d_st, render3d_st.gvk_fb_read, true), gvk_fb_att(render3d_st, render3d_st.gvk_fb_draw, true), true, w, h) }
|
|
}
|
|
|
|
# the screen as RGB8, bottom row first, as glReadPixels hands it back (a photograph)
|
|
@alloc_ok("asked for by the player or a tool, not by the frame")
|
|
function gvk_read_screen(render3d_st: mut Render3dState, w: int, h: int, out: pointer) -> void {
|
|
gvk_present(render3d_st)
|
|
# one read-back buffer, kept and replaced only when the screen's size changes: a buffer the size of
|
|
# the screen was made on every read and dropped (23 MB a photograph at 3024x1898)
|
|
let need = render3d_st.gvk_screen_w * render3d_st.gvk_screen_h * 4
|
|
if render3d_st.gvk_read_px == null or render3d_st.gvk_read_cap != need {
|
|
if render3d_st.gvk_read_px != null { free(render3d_st.gvk_read_px) }
|
|
render3d_st.gvk_read_px = bytes(need)
|
|
render3d_st.gvk_read_cap = need
|
|
}
|
|
let px = render3d_st.gvk_read_px
|
|
if not gvk_tex_read(render3d_st, render3d_st.gvk_screen_color, GL_RGBA8, render3d_st.gvk_screen_w, render3d_st.gvk_screen_h, GL_RGBA, GL_UNSIGNED_BYTE, px) { return }
|
|
let dst: pointer = out
|
|
for y in 0 .. h {
|
|
for x in 0 .. w {
|
|
let o = (y * render3d_st.gvk_screen_w + x) * 4
|
|
let q = (y * w + x) * 3
|
|
dst[q] = px[o]; dst[q + 1] = px[o + 1]; dst[q + 2] = px[o + 2]
|
|
}
|
|
}
|
|
}
|
|
|
|
# ---- the window: surface and swapchain ----------------------------------------------------------
|
|
# The Win32 surface on Windows; on macOS a CAMetalLayer the runtime hangs off the view (MoltenVK).
|
|
extern function win_native_window() -> pointer = "win_native_window"
|
|
extern function win_native_instance() -> pointer = "win_native_instance"
|
|
extern function win_metal_layer() -> pointer = "win_metal_layer"
|
|
extern function win_visible() -> int = "win_visible"
|
|
|
|
function gvk_surface_os(render3d_st: mut Render3dState, out: pointer) -> bool {
|
|
if Os.platform() == "macos" {
|
|
let layer = win_metal_layer()
|
|
if layer == null { render3d_st.gvk_why = "no Metal layer on the window"; return false }
|
|
let mci = bytes(VkMetalSurfaceCreateInfoEXT_sizeof)
|
|
Vk.zero(mci, VkMetalSurfaceCreateInfoEXT_sizeof)
|
|
Vk.put_i32(mci, VkMetalSurfaceCreateInfoEXT_sType, VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT)
|
|
Vk.put_ptr(mci, VkMetalSurfaceCreateInfoEXT_pLayer, layer)
|
|
let rm = Vk.create_metal_surface_ext(render3d_st.gvk_inst, mci, render3d_st.gvk_ac, out)
|
|
if rm != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateMetalSurfaceEXT", rm) }
|
|
return true
|
|
}
|
|
let hwnd = win_native_window()
|
|
if hwnd == null { render3d_st.gvk_why = "no window to present to"; return false }
|
|
let sci = bytes(VkWin32SurfaceCreateInfoKHR_sizeof)
|
|
Vk.zero(sci, VkWin32SurfaceCreateInfoKHR_sizeof)
|
|
Vk.put_i32(sci, VkWin32SurfaceCreateInfoKHR_sType, VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR)
|
|
Vk.put_ptr(sci, VkWin32SurfaceCreateInfoKHR_hinstance, win_native_instance())
|
|
Vk.put_ptr(sci, VkWin32SurfaceCreateInfoKHR_hwnd, hwnd)
|
|
let r = Vk.create_win32_surface_khr(render3d_st.gvk_inst, sci, render3d_st.gvk_ac, out)
|
|
if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateWin32SurfaceKHR", r) }
|
|
return true
|
|
}
|
|
|
|
@alloc_ok("the window changed size or a surface was lost: the screen, the swapchain and its images are made again")
|
|
function gvk_surface_make(render3d_st: mut Render3dState) -> bool {
|
|
if render3d_st.gvk_surface != 0 { return true }
|
|
let out = bytes(8)
|
|
if not gvk_surface_os(render3d_st, out) { return false }
|
|
render3d_st.gvk_surface = gvk_handle(out)
|
|
let ok = bytes(4)
|
|
Vk.put_i32(ok, 0, 0)
|
|
Vk.get_physical_device_surface_support_khr(render3d_st.gvk_pd, render3d_st.gvk_family, render3d_st.gvk_surface, ok)
|
|
if Vk.get_i32(ok, 0) != 1 { render3d_st.gvk_why = "the graphics queue cannot present to the window"; return false }
|
|
let fci = bytes(VkFenceCreateInfo_sizeof)
|
|
Vk.zero(fci, VkFenceCreateInfo_sizeof)
|
|
Vk.put_i32(fci, VkFenceCreateInfo_sType, VK_STRUCTURE_TYPE_FENCE_CREATE_INFO)
|
|
render3d_st.gvk_acq_fence = bytes(8)
|
|
return Vk.create_fence(render3d_st.gvk_dev, fci, render3d_st.gvk_ac, render3d_st.gvk_acq_fence) == VK_SUCCESS
|
|
}
|
|
|
|
# (Re)make the swapchain for a w x h client area. The old one is handed over and then destroyed.
|
|
@alloc_ok("the window changed size or a surface was lost: the screen, the swapchain and its images are made again")
|
|
function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool {
|
|
if not gvk_surface_make(render3d_st) { return false }
|
|
gvk_frame_wait(render3d_st)
|
|
Vk.device_wait_idle(render3d_st.gvk_dev)
|
|
let caps = bytes(VkSurfaceCapabilitiesKHR_sizeof)
|
|
Vk.get_physical_device_surface_capabilities_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, caps)
|
|
var ew = Vk.get_i32(caps, VkSurfaceCapabilitiesKHR_currentExtent + VkExtent2D_width)
|
|
var eh = Vk.get_i32(caps, VkSurfaceCapabilitiesKHR_currentExtent + VkExtent2D_height)
|
|
if ew == -1 or ew <= 0 { ew = w; eh = h }
|
|
if ew <= 0 or eh <= 0 { # minimised: keep the old chain until it has a size
|
|
free(caps)
|
|
return false
|
|
}
|
|
let cnt = bytes(4)
|
|
Vk.put_i32(cnt, 0, 0)
|
|
Vk.get_physical_device_surface_formats_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, cnt, null)
|
|
let nf = Vk.get_i32(cnt, 0)
|
|
let fmts = bytes(nf * VkSurfaceFormatKHR_sizeof + 8)
|
|
Vk.get_physical_device_surface_formats_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, cnt, fmts)
|
|
# the screen image holds display-ready 8-bit colour, so the swapchain takes it unconverted
|
|
var fmt = Vk.get_i32(fmts, VkSurfaceFormatKHR_format)
|
|
var cs = Vk.get_i32(fmts, VkSurfaceFormatKHR_colorSpace)
|
|
for i in 0 .. nf {
|
|
let f = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_format)
|
|
let c = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_colorSpace)
|
|
if (f == VK_FORMAT_B8G8R8A8_UNORM or f == VK_FORMAT_R8G8B8A8_UNORM) and c == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR { fmt = f; cs = c }
|
|
}
|
|
var hdr = false
|
|
if render3d_st.gvk_hdr_want and render3d_st.gvk_has_colorspace {
|
|
for i in 0 .. nf {
|
|
let f = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_format)
|
|
let c = Vk.get_i32(fmts, i * VkSurfaceFormatKHR_sizeof + VkSurfaceFormatKHR_colorSpace)
|
|
if f == VK_FORMAT_A2B10G10R10_UNORM_PACK32 and c == VK_COLOR_SPACE_HDR10_ST2084_EXT { fmt = f; cs = c; hdr = true }
|
|
}
|
|
if not hdr { print("r3d: vulkan: HDR output asked for, but this display offers no HDR10 swapchain") }
|
|
}
|
|
# the screen image carries the swapchain's depth of colour: remade when HDR comes or goes
|
|
if hdr != render3d_st.gvk_hdr_on { render3d_st.gvk_hdr_on = hdr; gvk_screen_make(render3d_st, render3d_st.gvk_screen_w, render3d_st.gvk_screen_h) }
|
|
Vk.put_i32(cnt, 0, 0)
|
|
free(fmts)
|
|
Vk.get_physical_device_surface_present_modes_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, cnt, null)
|
|
let nm = Vk.get_i32(cnt, 0)
|
|
let modes = bytes(nm * 4 + 8)
|
|
Vk.get_physical_device_surface_present_modes_khr(render3d_st.gvk_pd, render3d_st.gvk_surface, cnt, modes)
|
|
# vsync: FIFO, which every device has. Off: mailbox where offered (no tearing), else immediate.
|
|
var mode = VK_PRESENT_MODE_FIFO_KHR
|
|
if not render3d_st.gvk_vsync {
|
|
for i in 0 .. nm { if Vk.get_i32(modes, i * 4) == VK_PRESENT_MODE_IMMEDIATE_KHR { mode = VK_PRESENT_MODE_IMMEDIATE_KHR } }
|
|
for i in 0 .. nm { if Vk.get_i32(modes, i * 4) == VK_PRESENT_MODE_MAILBOX_KHR { mode = VK_PRESENT_MODE_MAILBOX_KHR } }
|
|
}
|
|
free(modes)
|
|
var n = Vk.get_i32(caps, VkSurfaceCapabilitiesKHR_minImageCount) + 1
|
|
let mx = Vk.get_i32(caps, VkSurfaceCapabilitiesKHR_maxImageCount)
|
|
if mx > 0 and n > mx { n = mx }
|
|
let sci = bytes(VkSwapchainCreateInfoKHR_sizeof)
|
|
Vk.zero(sci, VkSwapchainCreateInfoKHR_sizeof)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_sType, VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR)
|
|
Vk.put_i64(sci, VkSwapchainCreateInfoKHR_surface, render3d_st.gvk_surface)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_minImageCount, n)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageFormat, fmt)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageColorSpace, cs)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageExtent + VkExtent2D_width, ew)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageExtent + VkExtent2D_height, eh)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageArrayLayers, 1)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageUsage, VK_IMAGE_USAGE_TRANSFER_DST_BIT)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_imageSharingMode, VK_SHARING_MODE_EXCLUSIVE)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_preTransform, Vk.get_i32(caps, VkSurfaceCapabilitiesKHR_currentTransform))
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_compositeAlpha, VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_presentMode, mode)
|
|
Vk.put_i32(sci, VkSwapchainCreateInfoKHR_clipped, 1)
|
|
Vk.put_i64(sci, VkSwapchainCreateInfoKHR_oldSwapchain, render3d_st.gvk_swap)
|
|
let out = bytes(8)
|
|
let r = Vk.create_swapchain_khr(render3d_st.gvk_dev, sci, render3d_st.gvk_ac, out)
|
|
# the create's scratch goes on every way out: a resize once left six buffers and the image list behind
|
|
free(sci)
|
|
free(caps)
|
|
if r != VK_SUCCESS {
|
|
free(out)
|
|
free(cnt)
|
|
return gvk_fail(render3d_st, "vkCreateSwapchainKHR", r)
|
|
}
|
|
if render3d_st.gvk_swap != 0 { Vk.destroy_swapchain_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, render3d_st.gvk_ac) }
|
|
render3d_st.gvk_swap = gvk_handle(out)
|
|
free(out)
|
|
if render3d_st.gvk_hdr_on and render3d_st.gvk_has_hdr_meta { gvk_hdr_metadata(render3d_st) }
|
|
Vk.put_i32(cnt, 0, 0)
|
|
Vk.get_swapchain_images_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, cnt, null)
|
|
render3d_st.gvk_swap_n = Vk.get_i32(cnt, 0)
|
|
if render3d_st.gvk_swap_images != null { free(render3d_st.gvk_swap_images) }
|
|
render3d_st.gvk_swap_images = bytes(render3d_st.gvk_swap_n * 8 + 8)
|
|
Vk.get_swapchain_images_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, cnt, render3d_st.gvk_swap_images)
|
|
free(cnt)
|
|
render3d_st.gvk_swap_fmt = fmt
|
|
render3d_st.gvk_swap_w = ew
|
|
render3d_st.gvk_swap_h = eh
|
|
render3d_st.gvk_swap_stale = false
|
|
var mname = "fifo"
|
|
if mode == VK_PRESENT_MODE_MAILBOX_KHR { mname = "mailbox" }
|
|
if mode == VK_PRESENT_MODE_IMMEDIATE_KHR { mname = "immediate" }
|
|
var cname = "SDR"
|
|
if render3d_st.gvk_hdr_on { cname = "HDR10" }
|
|
print(`r3d: vulkan swapchain {ew}x{eh}, {render3d_st.gvk_swap_n} images, {mname}, {cname}`)
|
|
return true
|
|
}
|
|
|
|
function gvk_present_window(render3d_st: mut Render3dState) -> void {
|
|
let cb = gvk_frame_cb(render3d_st)
|
|
gvk_pass_end(render3d_st)
|
|
gvk_labels_close(render3d_st)
|
|
# a covered window is not drawn to: its layer would hold the frame for a drawable the compositor
|
|
# hands back once a second. The frame still runs, paced at about 60 Hz, so the game keeps time.
|
|
if win_visible() == 0 {
|
|
if gvk_inflight_on(render3d_st) { gvk_frame_submit(render3d_st, cb) } else { gvk_once_end(render3d_st, cb) }
|
|
render3d_st.gvk_cb = null
|
|
Time.sleep_us(16000)
|
|
return
|
|
}
|
|
if render3d_st.gvk_swap_stale { gvk_once_end(render3d_st, cb); render3d_st.gvk_cb = null; print("r3d: vulkan: swapchain remade at acquire"); gvk_swap_make(render3d_st, render3d_st.gvk_swap_w, render3d_st.gvk_swap_h); return }
|
|
let idx = gvk_tmp(render3d_st, 4)
|
|
Vk.reset_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence)
|
|
let forever: long = -1
|
|
let zero: long = 0
|
|
var r = Vk.acquire_next_image_khr(render3d_st.gvk_dev, render3d_st.gvk_swap, forever, zero, Vk.get_i64(render3d_st.gvk_acq_fence, 0), idx)
|
|
if r == VK_ERROR_OUT_OF_DATE_KHR { gvk_once_end(render3d_st, cb); render3d_st.gvk_cb = null; print("r3d: vulkan: swapchain remade at acquire"); gvk_swap_make(render3d_st, render3d_st.gvk_swap_w, render3d_st.gvk_swap_h); return }
|
|
Vk.wait_for_fences(render3d_st.gvk_dev, 1, render3d_st.gvk_acq_fence, 1, forever)
|
|
let i = Vk.get_i32(idx, 0)
|
|
let dst = Vk.get_i64(render3d_st.gvk_swap_images, i * 8)
|
|
let src = render3d_st.gvk_tex_image[render3d_st.gvk_screen_color]
|
|
gvk_barrier(render3d_st, cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
|
|
gvk_barrier(render3d_st, cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_UNDEFINED, 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_layerCount, 1)
|
|
# the screen image's row 0 is the bottom of the picture: read it from the top edge down
|
|
Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_y, render3d_st.gvk_screen_h)
|
|
Vk.put_i32(blit, VkImageBlit_srcOffsets + VkOffset3D_sizeof + VkOffset3D_x, render3d_st.gvk_screen_w)
|
|
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_layerCount, 1)
|
|
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_x, render3d_st.gvk_swap_w)
|
|
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_y, render3d_st.gvk_swap_h)
|
|
Vk.put_i32(blit, VkImageBlit_dstOffsets + VkOffset3D_sizeof + VkOffset3D_z, 1)
|
|
Vk.cmd_blit_image(cb, src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, blit, VK_FILTER_LINEAR)
|
|
gvk_barrier(render3d_st, cb, src, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
|
|
gvk_barrier(render3d_st, cb, dst, false, 0, 1, 1, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR)
|
|
if gvk_inflight_on(render3d_st) { gvk_frame_submit(render3d_st, cb) } else { gvk_once_end(render3d_st, cb) }
|
|
render3d_st.gvk_cb = null
|
|
let pi = gvk_tmp(render3d_st, VkPresentInfoKHR_sizeof)
|
|
Vk.zero(pi, VkPresentInfoKHR_sizeof)
|
|
Vk.put_i32(pi, VkPresentInfoKHR_sType, VK_STRUCTURE_TYPE_PRESENT_INFO_KHR)
|
|
let chains = gvk_tmp(render3d_st, 8)
|
|
Vk.put_i64(chains, 0, render3d_st.gvk_swap)
|
|
Vk.put_i32(pi, VkPresentInfoKHR_swapchainCount, 1)
|
|
Vk.put_ptr(pi, VkPresentInfoKHR_pSwapchains, chains)
|
|
Vk.put_ptr(pi, VkPresentInfoKHR_pImageIndices, idx)
|
|
gsl_before_present(render3d_st)
|
|
r = Vk.queue_present_khr(render3d_st.gvk_queue, pi)
|
|
if r == VK_ERROR_OUT_OF_DATE_KHR or r == VK_SUBOPTIMAL_KHR { render3d_st.gvk_swap_stale = true }
|
|
gsl_after_present(render3d_st)
|
|
}
|
|
|
|
# a window's client area changed: the screen images and the swapchain follow it
|
|
function gvk_resize_check(render3d_st: mut Render3dState) -> bool {
|
|
if render3d_st.gvk_swap == 0 { return false }
|
|
win_gl_drawable(render3d_st.gvk_size_buf)
|
|
let w = render3d_st.gvk_size_buf[0]
|
|
let h = render3d_st.gvk_size_buf[1]
|
|
if w <= 0 or h <= 0 { return false }
|
|
if w == gl_width() and h == gl_height() and not render3d_st.gvk_swap_stale { return false }
|
|
# a remake is rare (a resize); said, so one on every frame shows in the log
|
|
gvk_say_swap_remade(render3d_st, w, h)
|
|
gvk_flush(render3d_st)
|
|
gl_set_drawable(w, h)
|
|
gvk_screen_make(render3d_st, w, h)
|
|
gvk_swap_make(render3d_st, w, h)
|
|
return true
|
|
}
|
|
|
|
# Mipmaps for a texture the renderer asks for mid-frame (the exposure measure reads the HDR scene's
|
|
# smallest level every frame): recorded into the open frame after its pass, so they cost no submit.
|
|
# A chain that has to grow first still goes through its one-shot path.
|
|
function gvk_mips_now(render3d_st: mut Render3dState, tex: int, w: int, h: int) -> void {
|
|
if gvk_is_compressed(render3d_st.gvk_tex_glfmt[tex]) { return } # its levels came with it
|
|
if render3d_st.gvk_cb == null or render3d_st.gvk_tex_levels[tex] <= 1 { gvk_flush(render3d_st); gvk_tex_mips(render3d_st, tex, w, h); return }
|
|
gvk_pass_end(render3d_st)
|
|
gvk_tex_mips_into(render3d_st, render3d_st.gvk_cb, tex, w, h)
|
|
}
|
|
|
|
# ---- R3D_VK_PROF: where a Vulkan frame's CPU time goes ---------------------------------------------
|
|
# Every 120 frames: draws and flushes per frame, and milliseconds per frame spent finding pipelines,
|
|
# filling descriptor sets, and inside draws altogether. Off, it costs one flag test a draw.
|
|
function gvk_prof(render3d_st: mut Render3dState) -> bool {
|
|
if render3d_st.gvk_prof_state < 0 { render3d_st.gvk_prof_state = 0; if r3d_env_has(render3d_st, "R3D_VK_PROF") { render3d_st.gvk_prof_state = 1 } }
|
|
return render3d_st.gvk_prof_state == 1
|
|
}
|
|
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
|
|
function gvk_prof_frame(render3d_st: mut Render3dState) -> void {
|
|
if not gvk_prof(render3d_st) { return }
|
|
render3d_st.gvk_prof_frames += 1
|
|
if render3d_st.gvk_prof_frames < 120 { return }
|
|
let f = render3d_st.gvk_prof_frames
|
|
let pipe = int(render3d_st.gvk_us_pipe) / f
|
|
let set = int(render3d_st.gvk_us_set) / f
|
|
let draw = int(render3d_st.gvk_us_draw) / f
|
|
print(`r3d: vulkan per frame: {render3d_st.gvk_n_draws / f} draws, {render3d_st.gvk_n_flush / f} flushes; pipelines {pipe / 1000}.{(pipe / 100) % 10} ms, sets {set / 1000}.{(set / 100) % 10} ms, inside draws {draw / 1000}.{(draw / 100) % 10} ms; {render3d_st.gvk_n_pipe_new} pipelines made`)
|
|
# every draw asked for should have been made: a gap is a layer missing from the frame, which is how
|
|
# MoltenVK's refused skinned pipelines went unseen (the drawstats session found it by counting)
|
|
if render3d_st.gvk_n_asked != render3d_st.gvk_n_draws {
|
|
print(`r3d: vulkan: {(render3d_st.gvk_n_asked - render3d_st.gvk_n_draws) / f} draws a frame were asked for and not made ({render3d_st.gvk_n_asked / f} asked, {render3d_st.gvk_n_draws / f} made)`)
|
|
}
|
|
gvk_prof_made(render3d_st)
|
|
let zero: long = 0
|
|
render3d_st.gvk_us_pipe = zero; render3d_st.gvk_us_set = zero; render3d_st.gvk_us_draw = zero
|
|
render3d_st.gvk_n_draws = 0; render3d_st.gvk_n_asked = 0; render3d_st.gvk_n_flush = 0; render3d_st.gvk_prof_frames = 0; render3d_st.gvk_n_pipe_new = 0
|
|
}
|
|
|
|
# what was made and destroyed over those frames, and how long the caches are: a kind made every
|
|
# frame and never destroyed, or a cache that only grows, is a leak
|
|
@alloc_ok("profiling and statistics, only under R3D_PROF / R3D_DRAWSTATS")
|
|
function gvk_prof_made(render3d_st: mut Render3dState) -> void {
|
|
let r = render3d_st
|
|
print(`r3d: vulkan made/destroyed: images {r.gvk_mk_img}/{r.gvk_mk_x_img}, views {r.gvk_mk_view}/{r.gvk_mk_x_view}, buffers {r.gvk_mk_buf}/{r.gvk_mk_x_buf}, memory {r.gvk_mk_mem}/{r.gvk_mk_x_mem}, samplers {r.gvk_mk_smp}, sets {r.gvk_mk_set}, pools {r.gvk_mk_dpool}, cmds {r.gvk_mk_cmd}`)
|
|
print(`r3d: vulkan caches: layer views {gvk_len_i(r.gvk_layer_views)}, pipelines {gvk_len_s(r.gvk_pipe_keys)}, layouts {gvk_len_i(r.gvk_lay_off)}, pipe cache {gvk_len_i(r.gvk_pc_prog)}, retired {gvk_len_l(r.gvk_retired_buf)}, buffers {gvk_len_l(r.gvk_buf)}, allocs {r.gvk_n_allocs}`)
|
|
render3d_st.gvk_mk_img = 0; render3d_st.gvk_mk_x_img = 0; render3d_st.gvk_mk_view = 0; render3d_st.gvk_mk_x_view = 0
|
|
render3d_st.gvk_mk_buf = 0; render3d_st.gvk_mk_x_buf = 0; render3d_st.gvk_mk_mem = 0; render3d_st.gvk_mk_x_mem = 0
|
|
render3d_st.gvk_mk_smp = 0; render3d_st.gvk_mk_set = 0; render3d_st.gvk_mk_dpool = 0; render3d_st.gvk_mk_cmd = 0
|
|
}
|
|
function gvk_len_s(xs: []string) -> int { if xs == null { return 0 }; return len(xs) }
|
|
function gvk_len_i(xs: []int) -> int { if xs == null { return 0 }; return len(xs) }
|
|
function gvk_len_l(xs: []long) -> int { if xs == null { return 0 }; return len(xs) }
|
|
|
|
# ---- the pipeline cache, by integers -----------------------------------------------------------
|
|
# gvk_pipeline builds and keys pipelines by a string; a draw only needs to find one it already has.
|
|
# A mesh carries an interned layout id (its recorded layout, buffers named by order), the render
|
|
# state packs into one int, and the pass's formats into another; the program's last hit is tried
|
|
# first, then the entries it owns.
|
|
|
|
@alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)")
|
|
function gvk_layout_id(render3d_st: mut Render3dState, m: Mesh) -> int {
|
|
if m == null or m.attrs == null { return 1 }
|
|
if m.vk_layout > 0 { return m.vk_layout }
|
|
# the layout as numbers in a buffer made once - per attribute its index, its buffer's order of
|
|
# first use and its six fields - matched against the layouts known: a new mesh costs nothing, and
|
|
# only a layout never seen before is kept (a key string per mesh was never given back)
|
|
if render3d_st.gvk_lay_sig == null {
|
|
render3d_st.gvk_lay_sig = words(GPU_MAX_ATTRS * 8); render3d_st.gvk_lay_seen = words(GPU_MAX_ATTRS)
|
|
render3d_st.gvk_lay_flat = new []int; render3d_st.gvk_lay_off = new []int; render3d_st.gvk_lay_len = new []int
|
|
}
|
|
let sig = render3d_st.gvk_lay_sig
|
|
let seen = render3d_st.gvk_lay_seen
|
|
var n = 0
|
|
var ns = 0
|
|
for i in 0 .. m.n_attrs {
|
|
let o = i * GPU_ATTR_W
|
|
if m.attrs[o + 1] == 0 { continue }
|
|
var bi = -1
|
|
for q in 0 .. ns { if seen[q] == m.attrs[o] and bi < 0 { bi = q } }
|
|
if bi < 0 { bi = ns; seen[ns] = m.attrs[o]; ns += 1 }
|
|
sig[n] = i; sig[n + 1] = bi
|
|
for k in 1 .. 7 { sig[n + 1 + k] = m.attrs[o + k] }
|
|
n += 8
|
|
}
|
|
let flat = render3d_st.gvk_lay_flat
|
|
for e in 0 .. len(render3d_st.gvk_lay_off) {
|
|
if render3d_st.gvk_lay_len[e] == n {
|
|
let at = render3d_st.gvk_lay_off[e]
|
|
var same = true
|
|
var k = 0
|
|
while same and k < n { if flat[at + k] != sig[k] { same = false }; k += 1 }
|
|
if same {
|
|
m.vk_layout = e + 2
|
|
return m.vk_layout
|
|
}
|
|
}
|
|
}
|
|
push(render3d_st.gvk_lay_off, len(flat)); push(render3d_st.gvk_lay_len, n)
|
|
for k in 0 .. n { push(flat, sig[k]) }
|
|
m.vk_layout = len(render3d_st.gvk_lay_off) + 1
|
|
return m.vk_layout
|
|
}
|
|
function gvk_blend_index(f: int) -> int {
|
|
if f == GL_ZERO { return 0 }
|
|
if f == GL_ONE { return 1 }
|
|
if f == GL_SRC_ALPHA { return 2 }
|
|
if f == GL_ONE_MINUS_SRC_ALPHA { return 3 }
|
|
if f == GL_DST_ALPHA { return 4 }
|
|
if f == GL_ONE_MINUS_DST_ALPHA { return 5 }
|
|
if f == GL_SRC_COLOR { return 6 }
|
|
if f == GL_ONE_MINUS_SRC_COLOR { return 7 }
|
|
return 8
|
|
}
|
|
function gvk_pipeline_fast(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int) -> long {
|
|
let layout = gvk_layout_id(render3d_st, m)
|
|
var state = st.depth_test | (st.depth_write << 1) | (st.blend << 2) | (st.cull << 3) | (st.color_write << 4) | (st.a2c << 5) | (st.bias << 6) | (st.wireframe << 7)
|
|
state = state | ((st.depth_func & 15) << 8) | ((st.cull_face & 15) << 12) | (gvk_blend_index(st.blend_src) << 16) | (gvk_blend_index(st.blend_dst) << 20)
|
|
let bias = st.bias_factor * 31 + st.bias_units
|
|
let pass = ((n_color * 1000 + color_fmt) * 1000 + depth_fmt) * 64 + samples
|
|
if p < 4096 {
|
|
let k = render3d_st.gvk_pc_last[p]
|
|
if k >= 0 and render3d_st.gvk_pc_layout[k] == layout and render3d_st.gvk_pc_state[k] == state and render3d_st.gvk_pc_pass[k] == pass and render3d_st.gvk_pc_bias[k] == bias { return render3d_st.gvk_pc_pipe[k] }
|
|
}
|
|
for k in 0 .. len(render3d_st.gvk_pc_prog) {
|
|
if render3d_st.gvk_pc_prog[k] == p and render3d_st.gvk_pc_layout[k] == layout and render3d_st.gvk_pc_state[k] == state and render3d_st.gvk_pc_pass[k] == pass and render3d_st.gvk_pc_bias[k] == bias {
|
|
if p < 4096 { render3d_st.gvk_pc_last[p] = k }
|
|
return render3d_st.gvk_pc_pipe[k]
|
|
}
|
|
}
|
|
# a pipeline the driver refused is remembered as 0 too: retried on every draw, each try built its
|
|
# key and its create structs again and gave none of them back
|
|
return gvk_pipe_build(render3d_st, p, m, st, n_color, color_fmt, depth_fmt, samples, layout, state, bias, pass)
|
|
}
|
|
# The one place the pipeline cache grows: once per variant the game draws (program, vertex layout,
|
|
# render state, formats), kept for the program's life - the fence's declared warm-up (plan 25.2)
|
|
@alloc_ok("pipeline cache: one per variant the game draws")
|
|
function gvk_pipe_build(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, n_color: int, color_fmt: int, depth_fmt: int, samples: int, layout: int, state: int, bias: int, pass: int) -> long {
|
|
let pipe = gvk_pipeline(render3d_st, p, m, st, n_color, color_fmt, depth_fmt, samples)
|
|
push(render3d_st.gvk_pc_prog, p); push(render3d_st.gvk_pc_layout, layout); push(render3d_st.gvk_pc_state, state)
|
|
push(render3d_st.gvk_pc_bias, bias); push(render3d_st.gvk_pc_pass, pass); push(render3d_st.gvk_pc_pipe, pipe)
|
|
if p < 4096 { render3d_st.gvk_pc_last[p] = len(render3d_st.gvk_pc_prog) - 1 }
|
|
return pipe
|
|
}
|
|
|
|
# messages, each built in a function of its own so the path that says it holds no allocation
|
|
@alloc_ok("a message, built only when it is said: a failure, a warning or a debug switch")
|
|
function gvk_say_compressed(render3d_st: Render3dState, tex: int) -> void { print(`r3d: vulkan: framebuffer {render3d_st.gvk_fb_cur} would draw into compressed texture {tex} ({gvk_tex_w(render3d_st, tex)}x{gvk_tex_h(render3d_st, tex)}); left out`) }
|
|
@alloc_ok("a message, built only when it is said: a failure, a warning or a debug switch")
|
|
function gvk_say_no_pipeline(render3d_st: Render3dState, p: int) -> void { print(`r3d: vulkan: no pipeline for {gpu_program_key(render3d_st, p)}; its draws are skipped`) }
|
|
@alloc_ok("a message, built only when it is said: a failure, a warning or a debug switch")
|
|
function gvk_say_swap_remade(render3d_st: Render3dState, w: int, h: int) -> void { print(`r3d: vulkan: swapchain remade {w}x{h} (was {gl_width()}x{gl_height()}, stale {render3d_st.gvk_swap_stale})`) }
|