diff --git a/changes/render3d-plan25-vulkan.md b/changes/render3d-plan25-vulkan.md new file mode 100644 index 00000000..12970e72 --- /dev/null +++ b/changes/render3d-plan25-vulkan.md @@ -0,0 +1,9 @@ +bump: patch +type: fix +**Vulkan's host allocations can be counted, a pipeline built in play keeps nothing, and actors come +from a pool made at start-up.** With `R3D_ALLOC_VK=1` render3d hands one set of +`VkAllocationCallbacks` to every create and destroy; the runtime counts live bytes, the peak and +bytes per allocation scope (`lvk_ac_bytes`, `lvk_ac_peak`, `lvk_ac_scope_bytes`, `Vk.alloc_bytes`) +for the memory fence to read. MoltenVK 1.4.2 routes few of its own objects through them. A pipeline +made the first time a variant is drawn frees its seventeen create infos. `actor_init` makes 512 actor +records, so an actor first placed in play takes one instead of allocating. diff --git a/examples/rendering/steady.ludic b/examples/rendering/steady.ludic index e9951510..cd17b5c4 100644 --- a/examples/rendering/steady.ludic +++ b/examples/rendering/steady.ludic @@ -163,6 +163,18 @@ program Steady { let am = gltf_load(render3d_st, "packages/ludic.lab/plate", "plate.gltf", "plate") actor_rounds(render3d_st, am, 20) let grew_a = actor_rounds(render3d_st, am, 2000) + # with R3D_ALLOC_VK=1 MoltenVK's own host allocations are counted (plan 25.1c): what a frame + # leaves among them, read the same way + var vk_live: long = 0 + var vk_grew: long = 0 + if render3d_st.gvk_ac != null { + frame_rounds(render3d_st, 60) + let v0 = Vk.alloc_bytes() + frame_rounds(render3d_st, 600) + vk_live = Vk.alloc_bytes() + vk_grew = vk_live - v0 + print(`steady: Vulkan's host allocations {vk_live} bytes live (object {Vk.alloc_scope_bytes(1)}, command {Vk.alloc_scope_bytes(0)}), a frame kept {vk_grew} over 600`) + } let grew_r = ramp_rounds(render3d_st, am) let grew_s = stream_rounds(render3d_st, am) let text = Fs.read_text("packages/ludic.lab/plate/plate.gltf") @@ -194,6 +206,11 @@ program Steady { ok = false print("steady: FAILED - a frame drawing more than before leaves memory behind") } + # MoltenVK 1.4.2 honours the callbacks for few of its objects (it read 0 live): only growth fails + if render3d_st.gvk_ac != null and vk_grew > 0 { + ok = false + print("steady: FAILED - Vulkan's counted host allocations grow with the frame") + } if grew_s >= 4096 { ok = false print("steady: FAILED - streaming new ground leaves memory behind") diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 40af73a1..91bd697b 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -41,6 +41,7 @@ property Actor { outline: float = 0.0, # float bits: metres of rim drawn around it (0 = none) ocol: floats # the rim's colour (null = white) } +const ACTOR_POOL: int = 512 # a program and its uniform locations property AcProg { prog: int = 0, @@ -75,6 +76,16 @@ function ac_prog_new(render3d_st: mut Render3dState, vs: string, fs: string, def return a } function actor_init(render3d_st: mut Render3dState) -> void { + # the records play takes, made now: an actor first placed in play (a fish, a sign, a bed) made its + # record, matrix and tint then (actor_release gives them back to this pool) + if render3d_st.ac_spare == null { render3d_st.ac_spare = new []Actor } + let sp = render3d_st.ac_spare + for i in 0 .. ACTOR_POOL { + let a = new Actor + a.tint = v3_new(1.0, 1.0, 1.0) + a.mat = m4_new() + push(sp, a) + } render3d_st.ac_lit = ac_prog_new(render3d_st, "skin.vert", "model.frag", "") render3d_st.ac_lit_cut = ac_prog_new(render3d_st, "skin.vert", "model.frag", "#define ALPHA_TEST\n") render3d_st.ac_sh = ac_prog_new(render3d_st, "skin.vert", "shadow.frag", "#define SHADOW_PASS\n") diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index 446217d9..1c06c86b 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -691,6 +691,8 @@ export state Render3dState { stream_walks: int = 0 # streams that walked their whole ring this frame stream_debug_n: int = 0 stream_scratch: floats = null + gvk_pipe_bufs: words = null # a pipeline's vertex buffers, while it is made + gvk_ac: pointer = null # the VkAllocationCallbacks every create and destroy is given (R3D_ALLOC_VK=1) gvk_prime_b: words = null # buffers made since the frame began, read once so MoltenVK makes their gvk_prime_h: []long = null # Metal buffers now (gvk_prime), and their handles gvk_prime_n: int = 0 diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index 70f53a5e..f6b19194 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -60,6 +60,9 @@ function gvk_no_command_pooling() -> void { function gvk_init(render3d_st: mut Render3dState) -> bool { if render3d_st.gvk_ready { return true } gvk_no_command_pooling() + # MoltenVK's host allocations counted for the memory fence (plan 25.1c); every create and destroy + # is given the same callbacks, as Vulkan asks, so it is decided once, before the instance + if r3d_env_has(render3d_st, "R3D_ALLOC_VK") { render3d_st.gvk_ac = Vk.alloc_callbacks() } gsl_boot(render3d_st) if Vk.open() == 0 { render3d_st.gvk_why = "no Vulkan loader"; return false } gsl_init(render3d_st) @@ -106,7 +109,7 @@ function gvk_init(render3d_st: mut Render3dState) -> bool { Vk.put_ptr(ici, VkInstanceCreateInfo_ppEnabledExtensionNames, iext_names) } let out = bytes(8) - var r = Vk.create_instance(ici, null, out) + var r = Vk.create_instance(ici, render3d_st.gvk_ac, out) if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateInstance", r) } render3d_st.gvk_inst = Vk.get_ptr(out, 0) @@ -261,7 +264,7 @@ function gvk_init(render3d_st: mut Render3dState) -> bool { Vk.put_i32(dci, VkDeviceCreateInfo_enabledExtensionCount, n_dext) Vk.put_ptr(dci, VkDeviceCreateInfo_ppEnabledExtensionNames, dext_names) } - r = Vk.create_device(render3d_st.gvk_pd, dci, null, out) + r = Vk.create_device(render3d_st.gvk_pd, dci, render3d_st.gvk_ac, out) if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateDevice", r) } render3d_st.gvk_dev = Vk.get_ptr(out, 0) Vk.get_device_queue(render3d_st.gvk_dev, render3d_st.gvk_family, 0, out) @@ -313,7 +316,7 @@ function gvk_alloc(render3d_st: mut Render3dState, req: bytes, want: int) -> lon Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t) let out = gvk_tmp(render3d_st, 8) render3d_st.gvk_mk_mem += 1 - let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out) + let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, render3d_st.gvk_ac, out) if r != VK_SUCCESS { print(`r3d: vulkan: vkAllocateMemory failed (VkResult {r})`); return zero } render3d_st.gvk_n_allocs += 1 return gvk_handle(out) @@ -354,7 +357,7 @@ function gvk_mem_raw(render3d_st: mut Render3dState, t: int, size: int, host: bo Vk.put_i32(mai, VkMemoryAllocateInfo_memoryTypeIndex, t) let out = gvk_tmp(render3d_st, 8) render3d_st.gvk_mk_mem += 1 - let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, null, out) + let r = Vk.allocate_memory(render3d_st.gvk_dev, mai, render3d_st.gvk_ac, out) if r != VK_SUCCESS { gvk_note(render3d_st, `r3d: vulkan: vkAllocateMemory of {size} bytes failed (VkResult {r}, {render3d_st.gvk_n_allocs} allocations live)`); return zero } render3d_st.gvk_n_allocs += 1 let mem = gvk_handle(out) @@ -473,7 +476,7 @@ function gvk_mem_free(render3d_st: mut Render3dState, a: int) -> void { if b < 0 { if render3d_st.gvk_al_map[a] != null { Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a]) } render3d_st.gvk_mk_x_mem += 1 - Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a], null) + Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_al_mem[a], render3d_st.gvk_ac) render3d_st.gvk_n_allocs -= 1 } else { var off = render3d_st.gvk_al_off[a] @@ -493,7 +496,7 @@ function gvk_mem_free(render3d_st: mut Render3dState, a: int) -> void { # the block is empty again: give it back, or a world swapped out keeps its memory for good if render3d_st.gvk_blk_map[b] != null { Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b]) } render3d_st.gvk_mk_x_mem += 1 - Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b], null) + Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_blk_mem[b], render3d_st.gvk_ac) render3d_st.gvk_n_allocs -= 1 render3d_st.gvk_blk_mem[b] = zero; render3d_st.gvk_blk_map[b] = null; render3d_st.gvk_blk_kind[b] = -1; render3d_st.gvk_blk_size[b] = 0 } else { @@ -515,17 +518,17 @@ function gvk_cmd_init(render3d_st: mut Render3dState) -> bool { Vk.put_i32(cpi, VkCommandPoolCreateInfo_flags, VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT) Vk.put_i32(cpi, VkCommandPoolCreateInfo_queueFamilyIndex, render3d_st.gvk_family) let out = bytes(8) - var r = Vk.create_command_pool(render3d_st.gvk_dev, cpi, null, out) + var r = Vk.create_command_pool(render3d_st.gvk_dev, cpi, render3d_st.gvk_ac, out) if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateCommandPool", r) } render3d_st.gvk_pool = gvk_handle(out) 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_fence = bytes(8) - r = Vk.create_fence(render3d_st.gvk_dev, fci, null, render3d_st.gvk_fence) + r = Vk.create_fence(render3d_st.gvk_dev, fci, render3d_st.gvk_ac, render3d_st.gvk_fence) if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateFence", r) } render3d_st.gvk_frame_fence = bytes(8) - r = Vk.create_fence(render3d_st.gvk_dev, fci, null, render3d_st.gvk_frame_fence) + r = Vk.create_fence(render3d_st.gvk_dev, fci, render3d_st.gvk_ac, render3d_st.gvk_frame_fence) if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkCreateFence", r) } return true } @@ -645,12 +648,12 @@ function gvk_shutdown(render3d_st: mut Render3dState) -> void { gvk_frame_wait(render3d_st) Vk.device_wait_idle(render3d_st.gvk_dev) gsl_shutdown(render3d_st) - Vk.destroy_fence(render3d_st.gvk_dev, Vk.get_i64(render3d_st.gvk_fence, 0), null) - Vk.destroy_fence(render3d_st.gvk_dev, Vk.get_i64(render3d_st.gvk_frame_fence, 0), null) - Vk.destroy_command_pool(render3d_st.gvk_dev, render3d_st.gvk_pool, null) - if render3d_st.gvk_prime_dst != 0 { Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_prime_dst, null); render3d_st.gvk_prime_dst = 0 } - Vk.destroy_device(render3d_st.gvk_dev, null) - Vk.destroy_instance(render3d_st.gvk_inst, null) + Vk.destroy_fence(render3d_st.gvk_dev, Vk.get_i64(render3d_st.gvk_fence, 0), render3d_st.gvk_ac) + Vk.destroy_fence(render3d_st.gvk_dev, Vk.get_i64(render3d_st.gvk_frame_fence, 0), render3d_st.gvk_ac) + Vk.destroy_command_pool(render3d_st.gvk_dev, render3d_st.gvk_pool, render3d_st.gvk_ac) + if render3d_st.gvk_prime_dst != 0 { Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_prime_dst, render3d_st.gvk_ac); render3d_st.gvk_prime_dst = 0 } + Vk.destroy_device(render3d_st.gvk_dev, render3d_st.gvk_ac) + Vk.destroy_instance(render3d_st.gvk_inst, render3d_st.gvk_ac) render3d_st.gvk_ready = false } @@ -668,7 +671,7 @@ function gvk_query_new(render3d_st: mut Render3dState, n: int, ids: words) -> vo Vk.put_i32(qci, VkQueryPoolCreateInfo_queryType, VK_QUERY_TYPE_TIMESTAMP) Vk.put_i32(qci, VkQueryPoolCreateInfo_queryCount, n * 2) let out = gvk_tmp(render3d_st, 8) - if Vk.create_query_pool(render3d_st.gvk_dev, qci, null, out) != VK_SUCCESS { return } + if Vk.create_query_pool(render3d_st.gvk_dev, qci, render3d_st.gvk_ac, out) != VK_SUCCESS { return } render3d_st.gvk_qpool = gvk_handle(out) Vk.reset_query_pool(render3d_st.gvk_dev, render3d_st.gvk_qpool, 0, n * 2) } diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index b56bd38d..d4045aa3 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -36,7 +36,7 @@ function gvk_module(render3d_st: mut Render3dState, path: string) -> long { 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, null, out) + let r = Vk.create_shader_module(render3d_st.gvk_dev, smci, render3d_st.gvk_ac, out) if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateShaderModule {path}`, r); return zero } return gvk_handle(out) } @@ -103,7 +103,7 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di 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, null, dsl) + 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) @@ -111,7 +111,7 @@ function gvk_program(render3d_st: mut Render3dState, p: int, key: string, spv_di 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, null, out) + r = Vk.create_pipeline_layout(render3d_st.gvk_dev, plci, render3d_st.gvk_ac, 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] = Vk.get_i64(dsl, 0); render3d_st.gvk_prog_layout[p] = gvk_handle(out) @@ -153,7 +153,7 @@ function gvk_compute_new_tex(render3d_st: mut Render3dState, name: string, n_buf 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, null, dsl) + 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) @@ -161,7 +161,7 @@ function gvk_compute_new_tex(render3d_st: mut Render3dState, name: string, n_buf 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, null, out) + 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) @@ -173,7 +173,7 @@ function gvk_compute_new_tex(render3d_st: mut Render3dState, name: string, n_buf 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, null, out) + r = Vk.create_compute_pipelines(render3d_st.gvk_dev, zero, 1, cpci, render3d_st.gvk_ac, out) if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateComputePipelines {name}`, r); return 0 } push(render3d_st.gvk_cp_pipe, gvk_handle(out)); push(render3d_st.gvk_cp_layout, layout); push(render3d_st.gvk_cp_dsl, Vk.get_i64(dsl, 0)); push(render3d_st.gvk_cp_nbuf, n_bufs) push(render3d_st.gvk_cp_ntex, n_tex) @@ -393,7 +393,7 @@ function gvk_pipeline(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkSt let v = render3d_st.gvk_prog_var[p] if v == null { return zero } let key = `{p}|{gvk_layout_key(m)}|{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}` - if render3d_st.gvk_pipe_keys == null { render3d_st.gvk_pipe_keys = new []string; render3d_st.gvk_pipe = new []long } + 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 @@ -419,7 +419,7 @@ function gvk_pipeline(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkSt var na = 0 var nbd = 0 if m != null and m.attrs != null { - let bufs = words(GPU_MAX_VBUFS) + 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 } @@ -566,9 +566,12 @@ function gvk_pipeline(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkSt 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, null, out) - if r != VK_SUCCESS { gvk_fail(render3d_st, `vkCreateGraphicsPipelines for {v.vs} + {v.fs}`, r); return zero } + 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) @@ -706,7 +709,7 @@ function gvk_frame_init(render3d_st: mut Render3dState) -> bool { 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, null, out) + 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)) } @@ -761,7 +764,7 @@ function gvk_kpool_make(render3d_st: mut Render3dState) -> bool { 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, null, out) + 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) @@ -1042,7 +1045,7 @@ function gvk_view_of(render3d_st: mut Render3dState, tex: int, layer1: int) -> l let out = bytes(8) let zero: long = 0 render3d_st.gvk_mk_view += 1 - if Vk.create_image_view(render3d_st.gvk_dev, vci, null, out) != VK_SUCCESS { return zero } + if Vk.create_image_view(render3d_st.gvk_dev, vci, render3d_st.gvk_ac, out) != VK_SUCCESS { return zero } push(render3d_st.gvk_layer_views, key) push(render3d_st.gvk_layer_view, gvk_handle(out)) push(render3d_st.gvk_layer_view_tex, tex) @@ -1636,7 +1639,7 @@ function gvk_surface_os(render3d_st: mut Render3dState, out: pointer) -> bool { 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, null, out) + 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 } @@ -1647,7 +1650,7 @@ function gvk_surface_os(render3d_st: mut Render3dState, out: pointer) -> bool { 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, null, out) + 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 } @@ -1665,7 +1668,7 @@ function gvk_surface_make(render3d_st: mut Render3dState) -> bool { 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, null, render3d_st.gvk_acq_fence) == VK_SUCCESS + 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. @@ -1736,9 +1739,9 @@ function gvk_swap_make(render3d_st: mut Render3dState, w: int, h: int) -> bool { 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, null, out) + 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, null) } + 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) diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index 7eab6624..317b43b3 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -154,7 +154,7 @@ function gvk_staging(render3d_st: mut Render3dState, n: int, usage: int) -> poin Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE) let out = gvk_tmp(render3d_st, 8) render3d_st.gvk_mk_buf += 1 - if Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) != VK_SUCCESS { return null } + if Vk.create_buffer(render3d_st.gvk_dev, bci, render3d_st.gvk_ac, out) != VK_SUCCESS { return null } render3d_st.gvk_st_buf = gvk_handle(out) let req = gvk_tmp(render3d_st, VkMemoryRequirements_sizeof) Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, render3d_st.gvk_st_buf, req) @@ -169,9 +169,9 @@ function gvk_staging_free(render3d_st: mut Render3dState) -> void { gvk_frame_wait(render3d_st) # the frame in flight may still read it Vk.unmap_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem) render3d_st.gvk_mk_x_buf += 1 - Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_st_buf, null) + Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_st_buf, render3d_st.gvk_ac) render3d_st.gvk_mk_x_mem += 1 - Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, null) + Vk.free_memory(render3d_st.gvk_dev, render3d_st.gvk_st_mem, render3d_st.gvk_ac) render3d_st.gvk_n_allocs -= 1 } @@ -211,7 +211,7 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool, Vk.put_i32(ici, VkImageCreateInfo_initialLayout, VK_IMAGE_LAYOUT_UNDEFINED) let out = bytes(8) render3d_st.gvk_mk_img += 1 - var r = Vk.create_image(render3d_st.gvk_dev, ici, null, out) + var r = Vk.create_image(render3d_st.gvk_dev, ici, render3d_st.gvk_ac, out) # the create infos are read by the call they are handed to and go straight after (a texture # made in play once left four of them behind) free(ici) @@ -230,7 +230,7 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool, # the card is full): say so instead of binding a null allocation, which the driver may accept if mem == 0 { render3d_st.gvk_mk_x_img += 1 - Vk.destroy_image(render3d_st.gvk_dev, image, null) + Vk.destroy_image(render3d_st.gvk_dev, image, render3d_st.gvk_ac) free(out) return gvk_fail(render3d_st, `no device memory for a {w}x{h}x{layers} image ({render3d_st.gvk_n_allocs} allocations live)`, VK_ERROR_OUT_OF_DEVICE_MEMORY) } @@ -250,7 +250,7 @@ function gvk_tex_storage(render3d_st: mut Render3dState, tex: int, array: bool, Vk.put_i32(vci, sr + VkImageSubresourceRange_levelCount, levels) Vk.put_i32(vci, sr + VkImageSubresourceRange_layerCount, layers) render3d_st.gvk_mk_view += 1 - r = Vk.create_image_view(render3d_st.gvk_dev, vci, null, out) + r = Vk.create_image_view(render3d_st.gvk_dev, vci, render3d_st.gvk_ac, out) free(vci) let view = gvk_handle(out) free(out) @@ -382,9 +382,9 @@ function gvk_tex_grow_mips(render3d_st: mut Render3dState, tex: int, w: int, h: gvk_barrier(render3d_st, cb, render3d_st.gvk_tex_image[tex], false, 0, 1, layers, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) let ok = gvk_once_end(render3d_st, cb) render3d_st.gvk_mk_x_view += 1 - Vk.destroy_image_view(render3d_st.gvk_dev, old_view, null) + Vk.destroy_image_view(render3d_st.gvk_dev, old_view, render3d_st.gvk_ac) render3d_st.gvk_mk_x_img += 1 - Vk.destroy_image(render3d_st.gvk_dev, old_image, null) + Vk.destroy_image(render3d_st.gvk_dev, old_image, render3d_st.gvk_ac) gvk_mem_free(render3d_st, gvk_mem_id(old_mem)) return ok } @@ -483,9 +483,9 @@ function gvk_tex_release(render3d_st: mut Render3dState, tex: int) -> void { if render3d_st.gvk_tex_image[tex] == 0 { return } let zero: long = 0 render3d_st.gvk_mk_x_view += 1 - Vk.destroy_image_view(render3d_st.gvk_dev, render3d_st.gvk_tex_view[tex], null) + Vk.destroy_image_view(render3d_st.gvk_dev, render3d_st.gvk_tex_view[tex], render3d_st.gvk_ac) render3d_st.gvk_mk_x_img += 1 - Vk.destroy_image(render3d_st.gvk_dev, render3d_st.gvk_tex_image[tex], null) + Vk.destroy_image(render3d_st.gvk_dev, render3d_st.gvk_tex_image[tex], render3d_st.gvk_ac) gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_tex_mem[tex])) gvk_layer_views_drop(render3d_st, tex) render3d_st.gvk_tex_image[tex] = zero; render3d_st.gvk_tex_view[tex] = zero; render3d_st.gvk_tex_mem[tex] = zero @@ -504,7 +504,7 @@ function gvk_layer_views_drop(render3d_st: mut Render3dState, tex: int) -> void for i in 0 .. len(keys) { if texs[i] == tex { render3d_st.gvk_mk_x_view += 1 - Vk.destroy_image_view(render3d_st.gvk_dev, views[i], null) + Vk.destroy_image_view(render3d_st.gvk_dev, views[i], render3d_st.gvk_ac) } else { keys[w] = keys[i]; views[w] = views[i]; texs[w] = texs[i] w += 1 @@ -615,7 +615,7 @@ function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wra let out = bytes(8) let zero: long = 0 render3d_st.gvk_mk_smp += 1 - let r = Vk.create_sampler(render3d_st.gvk_dev, sci, null, out) + let r = Vk.create_sampler(render3d_st.gvk_dev, sci, render3d_st.gvk_ac, out) if r != VK_SUCCESS { gvk_fail(render3d_st, "vkCreateSampler", r); return zero } let s = gvk_handle(out) push(ks, min_f); push(ks, mag_f); push(ks, wrap_s); push(ks, wrap_t); push(ks, compare); push(ks, aniso); push(ks, bias) @@ -673,7 +673,7 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { push(render3d_st.gvk_retired_frame, render3d_st.gvk_buf_used[b]) } else { render3d_st.gvk_mk_x_buf += 1 - Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], null) + Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_buf[b], render3d_st.gvk_ac) gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_buf_mem[b])) } render3d_st.gvk_buf[b] = zero; render3d_st.gvk_buf_mem[b] = zero; render3d_st.gvk_buf_size[b] = 0; render3d_st.gvk_buf_map[b] = null; render3d_st.gvk_buf_used[b] = 0 @@ -702,7 +702,7 @@ function gvk_retire_upto(render3d_st: mut Render3dState, done: int) -> void { w += 1 } else { render3d_st.gvk_mk_x_buf += 1 - Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], null) + Vk.destroy_buffer(render3d_st.gvk_dev, render3d_st.gvk_retired_buf[i], render3d_st.gvk_ac) gvk_mem_free(render3d_st, gvk_mem_id(render3d_st.gvk_retired_mem[i])) } } @@ -741,14 +741,14 @@ function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE) let out = gvk_tmp(render3d_st, 8) render3d_st.gvk_mk_buf += 1 - var r = Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) + var r = Vk.create_buffer(render3d_st.gvk_dev, bci, render3d_st.gvk_ac, out) if r != VK_SUCCESS { return gvk_fail(render3d_st, `vkCreateBuffer ({size} bytes)`, r) } let buf = gvk_handle(out) let req = gvk_tmp(render3d_st, VkMemoryRequirements_sizeof) Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, buf, req) let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, false) let zero: long = 0 - if ma == 0 { Vk.destroy_buffer(render3d_st.gvk_dev, buf, null); return false } + if ma == 0 { Vk.destroy_buffer(render3d_st.gvk_dev, buf, render3d_st.gvk_ac); return false } let mem: long = ma r = Vk.bind_buffer_memory(render3d_st.gvk_dev, buf, gvk_mem_handle(render3d_st, ma), gvk_mem_offset(render3d_st, ma)) if r != VK_SUCCESS { return gvk_fail(render3d_st, "vkBindBufferMemory", r) } @@ -810,13 +810,13 @@ function gvk_prime_target(render3d_st: mut Render3dState) -> bool { Vk.put_i32(bci, VkBufferCreateInfo_usage, VK_BUFFER_USAGE_TRANSFER_DST_BIT) Vk.put_i32(bci, VkBufferCreateInfo_sharingMode, VK_SHARING_MODE_EXCLUSIVE) let out = gvk_tmp(render3d_st, 8) - if Vk.create_buffer(render3d_st.gvk_dev, bci, null, out) != VK_SUCCESS { return false } + if Vk.create_buffer(render3d_st.gvk_dev, bci, render3d_st.gvk_ac, out) != VK_SUCCESS { return false } let buf = gvk_handle(out) let req = gvk_tmp(render3d_st, VkMemoryRequirements_sizeof) Vk.get_buffer_memory_requirements(render3d_st.gvk_dev, buf, req) let ma = gvk_mem_new(render3d_st, req, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, false) if ma == 0 { - Vk.destroy_buffer(render3d_st.gvk_dev, buf, null) + Vk.destroy_buffer(render3d_st.gvk_dev, buf, render3d_st.gvk_ac) return false } Vk.bind_buffer_memory(render3d_st.gvk_dev, buf, gvk_mem_handle(render3d_st, ma), gvk_mem_offset(render3d_st, ma)) diff --git a/runtime/native/vk.ludic b/runtime/native/vk.ludic index cb5dc026..e85d9f72 100644 --- a/runtime/native/vk.ludic +++ b/runtime/native/vk.ludic @@ -30,6 +30,11 @@ import "vk_api.ludic" extern function vk_frame_pool() = "lvk_frame_pool" # malloc's live bytes (macOS; 0 elsewhere): a test that a path allocates nothing, frame after frame extern function vk_heap_bytes() -> long = "lvk_heap_bytes" +# the VkAllocationCallbacks that count MoltenVK's host allocations (memory plan 25.1c; null on +# Windows), and what they count: live bytes now, and per VkSystemAllocationScope +extern function vk_alloc_callbacks() -> pointer = "lvk_ac_ptr" +extern function vk_alloc_bytes() -> long = "lvk_ac_bytes" +extern function vk_alloc_scope_bytes(scope: int) -> long = "lvk_ac_scope_bytes" extern function vk_sl_prefer(on: int) = "lvk_sl_prefer" extern function vk_sl_active() -> int = "lvk_sl_active" extern function vk_sl_init(pref: pointer, sdk_version: long) -> int = "lsl_slInit" diff --git a/runtime/native/vk_mac.ll b/runtime/native/vk_mac.ll index 2f7732a4..5f768280 100644 --- a/runtime/native/vk_mac.ll +++ b/runtime/native/vk_mac.ll @@ -264,3 +264,144 @@ entry: %n = load i64, ptr %p ret i64 %n } + +; VkAllocationCallbacks (memory plan 25.1c): render3d hands @lvk_ac to every Vulkan create and +; destroy when R3D_ALLOC_VK=1, so MoltenVK's own host allocations are counted - live bytes, the peak, +; allocations made and bytes per VkSystemAllocationScope - for the fence to read (lvk_ac_*). +; Metal's own allocations are never seen here. libc underneath, never the fence's allocator. Each +; block carries a 16-byte header before it: scope (i32), the offset to malloc's block (i32), size +; (i64); the offset is the alignment asked, at least 16, so the block keeps it. The counters are +; atomics: MoltenVK allocates from its completion handlers' threads too. +@lvk_ac_live = global i64 0 +@lvk_ac_peak_v = global i64 0 +@lvk_ac_n = global i64 0 +@lvk_ac_scope = global [5 x i64] zeroinitializer +@lvk_ac = global [6 x ptr] [ptr null, ptr @lvk_ac_alloc, ptr @lvk_ac_realloc, ptr @lvk_ac_free, ptr null, ptr null] +declare i32 @posix_memalign(ptr, i64, i64) +declare void @free(ptr) +declare ptr @memcpy(ptr, ptr, i64) + +define internal void @lvk_ac_count(i32 %scope, i64 %d, i64 %n) { +entry: + %old = atomicrmw add ptr @lvk_ac_live, i64 %d seq_cst + %new = add i64 %old, %d + %pk = atomicrmw max ptr @lvk_ac_peak_v, i64 %new seq_cst + %nn = atomicrmw add ptr @lvk_ac_n, i64 %n seq_cst + %lo = icmp slt i32 %scope, 0 + %hi = icmp sgt i32 %scope, 4 + %bad = or i1 %lo, %hi + %sc = select i1 %bad, i32 1, i32 %scope + %sp = getelementptr [5 x i64], ptr @lvk_ac_scope, i32 0, i32 %sc + %sv = atomicrmw add ptr %sp, i64 %d seq_cst + ret void +} +define internal ptr @lvk_ac_alloc(ptr %ud, i64 %size, i64 %align, i32 %scope) { +entry: + %z = icmp eq i64 %size, 0 + br i1 %z, label %none, label %go +none: + ret ptr null +go: + %small = icmp ult i64 %align, 16 + %a = select i1 %small, i64 16, i64 %align + %tot = add i64 %size, %a + %slot = alloca ptr, align 8 + %r = call i32 @posix_memalign(ptr %slot, i64 %a, i64 %tot) + %ok = icmp eq i32 %r, 0 + br i1 %ok, label %have, label %none +have: + %base = load ptr, ptr %slot + %p = getelementptr i8, ptr %base, i64 %a + %h = getelementptr i8, ptr %p, i64 -16 + store i32 %scope, ptr %h + %ha = getelementptr i8, ptr %h, i64 4 + %a32 = trunc i64 %a to i32 + store i32 %a32, ptr %ha + %hs = getelementptr i8, ptr %h, i64 8 + store i64 %size, ptr %hs + call void @lvk_ac_count(i32 %scope, i64 %size, i64 1) + ret ptr %p +} +define internal void @lvk_ac_free(ptr %ud, ptr %p) { +entry: + %z = icmp eq ptr %p, null + br i1 %z, label %done, label %go +done: + ret void +go: + %h = getelementptr i8, ptr %p, i64 -16 + %scope = load i32, ptr %h + %ha = getelementptr i8, ptr %h, i64 4 + %a32 = load i32, ptr %ha + %hs = getelementptr i8, ptr %h, i64 8 + %size = load i64, ptr %hs + %neg = sub i64 0, %size + call void @lvk_ac_count(i32 %scope, i64 %neg, i64 0) + %a = zext i32 %a32 to i64 + %na = sub i64 0, %a + %base = getelementptr i8, ptr %p, i64 %na + call void @free(ptr %base) + ret void +} +; a new block at the asked alignment, the old one's bytes copied, the old one freed: realloc itself +; only keeps 16 +define internal ptr @lvk_ac_realloc(ptr %ud, ptr %old, i64 %size, i64 %align, i32 %scope) { +entry: + %nold = icmp eq ptr %old, null + br i1 %nold, label %fresh, label %chk +fresh: + %f = call ptr @lvk_ac_alloc(ptr %ud, i64 %size, i64 %align, i32 %scope) + ret ptr %f +chk: + %z = icmp eq i64 %size, 0 + br i1 %z, label %drop, label %move +drop: + call void @lvk_ac_free(ptr %ud, ptr %old) + ret ptr null +move: + %nb = call ptr @lvk_ac_alloc(ptr %ud, i64 %size, i64 %align, i32 %scope) + %nz = icmp eq ptr %nb, null + br i1 %nz, label %fail, label %copy +fail: + ret ptr null +copy: + %hs = getelementptr i8, ptr %old, i64 -8 + %osz = load i64, ptr %hs + %less = icmp ult i64 %osz, %size + %n = select i1 %less, i64 %osz, i64 %size + %cp = call ptr @memcpy(ptr %nb, ptr %old, i64 %n) + call void @lvk_ac_free(ptr %ud, ptr %old) + ret ptr %nb +} +define ptr @lvk_ac_ptr() { +entry: + ret ptr @lvk_ac +} +define i64 @lvk_ac_bytes() { +entry: + %v = load atomic i64, ptr @lvk_ac_live seq_cst, align 8 + ret i64 %v +} +define i64 @lvk_ac_peak() { +entry: + %v = load atomic i64, ptr @lvk_ac_peak_v seq_cst, align 8 + ret i64 %v +} +define i64 @lvk_ac_allocs() { +entry: + %v = load atomic i64, ptr @lvk_ac_n seq_cst, align 8 + ret i64 %v +} +define i64 @lvk_ac_scope_bytes(i32 %scope) { +entry: + %lo = icmp slt i32 %scope, 0 + %hi = icmp sgt i32 %scope, 4 + %bad = or i1 %lo, %hi + br i1 %bad, label %none, label %read +none: + ret i64 0 +read: + %sp = getelementptr [5 x i64], ptr @lvk_ac_scope, i32 0, i32 %scope + %v = load atomic i64, ptr %sp seq_cst, align 8 + ret i64 %v +} diff --git a/runtime/native/vk_win.ll b/runtime/native/vk_win.ll index a3ae3f37..5567d367 100644 --- a/runtime/native/vk_win.ll +++ b/runtime/native/vk_win.ll @@ -375,3 +375,26 @@ define i64 @lvk_heap_bytes() { entry: ret i64 0 } + +; VkAllocationCallbacks (memory plan 25.1c): not measured on Windows yet - render3d gets null and +; passes it, and the fence reads 0 (the residual there is a later step). +define ptr @lvk_ac_ptr() { +entry: + ret ptr null +} +define i64 @lvk_ac_bytes() { +entry: + ret i64 0 +} +define i64 @lvk_ac_peak() { +entry: + ret i64 0 +} +define i64 @lvk_ac_allocs() { +entry: + ret i64 0 +} +define i64 @lvk_ac_scope_bytes(i32 %scope) { +entry: + ret i64 0 +}