render3d: VkAllocationCallbacks counted (R3D_ALLOC_VK), pipeline create infos freed, actor pool at init

Plan 25.1c: vk_mac.ll's @lvk_ac (posix_memalign under a 16-byte header of scope/offset/size, atomic
counters) passed at every render3d create/destroy (49 sites; the caps probe keeps its own null pair);
lvk_ac_bytes/_peak/_allocs/_scope_bytes for the fence, Vk.alloc_bytes. vk_win.ll: null and 0.
MoltenVK 1.4.2 counted 0 live bytes through them in steady. Fence findings: gvk_pipeline freed its 17
create infos (and reuses one bufs list); actor_init fills ac_spare with 512 records (actor_fresh,
m4_new, v3_new at first placement in play). Compiled, not run (the user's call).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 15:46:19 +03:00
parent edb33400b3
commit df6ea046c3
10 changed files with 266 additions and 52 deletions

View file

@ -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.

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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))

View file

@ -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"

View file

@ -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
}

View file

@ -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
}