# 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 .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) 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 { gvk_fail(render3d_st, `vkCreateGraphicsPipelines for {v.vs} + {v.fs}`, r); 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() } 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 @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) 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) 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 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 window changed size or a surface was lost: the screen, the swapchain and its images are made 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) let px = bytes(render3d_st.gvk_screen_w * render3d_st.gvk_screen_h * 4) 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 { return false } # minimised: keep the old chain until it has a size 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) 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 } } } 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) if r != VK_SUCCESS { 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) 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) 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) 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})`) }