fix(render3d): nothing allocated in the steady state - the Vulkan allocator reuses its records

gvk_mem_new made a one-slot []pointer per allocation, and turned the requirement's size and
alignment into strings to read them as ints; gvk_list_drop_last rebuilt the spare-record list to
drop its last entry; gvk_mem_id did the string round trip on every free. One slot is kept
(gvk_map_slot), int() truncates a long, the spare list pops. Every Text.to_int(string(x)) in
render3d is int(x) now.

Vk.heap_bytes() (vk_mac.ll: malloc_zone_statistics' size_in_use; 0 on Windows) and
examples/rendering/steady.ludic, in the suite: a buffer released and made again 5000 times and
600 whole frames gain 0 bytes each - the allocator before this, 1,120,000 over the 5000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 11:14:56 +03:00
parent 3915af34c2
commit 866037a0d7
9 changed files with 119 additions and 19 deletions

View file

@ -201,6 +201,7 @@ export state Render3dState {
gvk_inflight: int = -1
gvk_nopool: bool = false
gvk_bc_said: int = 0 # attachments refused for being compressed, said so far
gvk_map_slot: []pointer = null # gvk_mem_new's mapped-pointer slot, made once
gvk_has_bc: bool = false # textureCompressionBC: .dds textures are uploaded compressed
# R3D_VK_PROF: Vulkan objects made and destroyed, reported every 120 frames
gvk_mk_img: int = 0

View file

@ -371,14 +371,19 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
if t < 0 and want == VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT { t = gvk_mem_type(render3d_st, allowed, 0) }
if t < 0 { gvk_note(render3d_st, `r3d: vulkan: no memory type for properties {want}`); return 0 }
let host = (want & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0
let size = Text.to_int(string(Vk.get_i64(req, VkMemoryRequirements_size)))
var align = Text.to_int(string(Vk.get_i64(req, VkMemoryRequirements_alignment)))
let size = int(Vk.get_i64(req, VkMemoryRequirements_size))
var align = int(Vk.get_i64(req, VkMemoryRequirements_alignment))
if align < 1 { align = 1 }
var blk = -1
var off = 0
let zero: long = 0
let mp = new []pointer
push(mp, null)
# the mapped pointer comes back through a one-slot list made once, not one per allocation
if render3d_st.gvk_map_slot == null {
render3d_st.gvk_map_slot = new []pointer
push(render3d_st.gvk_map_slot, null)
}
let mp = render3d_st.gvk_map_slot
mp[0] = null
if size <= GVK_OWN_OVER {
var kind = t * 2
if image { kind += 1 }
@ -420,8 +425,9 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
}
var a = 0
if len(render3d_st.gvk_al_spare) > 0 {
a = render3d_st.gvk_al_spare[len(render3d_st.gvk_al_spare) - 1]
render3d_st.gvk_al_spare = gvk_list_drop_last(render3d_st.gvk_al_spare)
let spare = render3d_st.gvk_al_spare
a = spare[len(spare) - 1]
List.pop(spare)
} else {
push(render3d_st.gvk_al_blk, -1); push(render3d_st.gvk_al_mem, zero); push(render3d_st.gvk_al_off, 0); push(render3d_st.gvk_al_len, 0); push(render3d_st.gvk_al_map, null)
a = len(render3d_st.gvk_al_blk) - 1
@ -430,12 +436,6 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
return a
}
function gvk_list_drop_last(l: []int) -> []int {
let out = new []int
for i in 0 .. len(l) - 1 { push(out, l[i]) }
return out
}
function gvk_mem_handle(render3d_st: Render3dState, a: int) -> long {
if render3d_st.gvk_al_blk[a] < 0 { return render3d_st.gvk_al_mem[a] }
return render3d_st.gvk_blk_mem[render3d_st.gvk_al_blk[a]]
@ -484,7 +484,7 @@ function gvk_mem_free(render3d_st: mut Render3dState, a: int) -> void {
render3d_st.gvk_al_len[a] = 0; render3d_st.gvk_al_mem[a] = zero; render3d_st.gvk_al_map[a] = null; render3d_st.gvk_al_blk[a] = -1
push(render3d_st.gvk_al_spare, a)
}
function gvk_mem_id(x: long) -> int { return Text.to_int(string(x)) }
function gvk_mem_id(x: long) -> int { return int(x) }
# ---- one-shot commands --------------------------------------------------------------------
# Uploads, bakes and read-backs record into a command buffer, submit it and wait. The frame
@ -669,7 +669,7 @@ function gvk_query_result(render3d_st: mut Render3dState, id: int, out: words) -
let size: long = 16
let stride: long = 8
if Vk.get_query_pool_results(render3d_st.gvk_dev, render3d_st.gvk_qpool, id * 2, 2, size, data, stride, VK_QUERY_RESULT_64_BIT) != VK_SUCCESS { return false }
let ticks = Text.to_int(string(Vk.get_i64(data, 8) - Vk.get_i64(data, 0)))
let ticks = int(Vk.get_i64(data, 8) - Vk.get_i64(data, 0))
if ticks < 0 { return false }
out[0] = int(float(ticks) * render3d_st.gvk_ts_period)
return true

View file

@ -678,7 +678,7 @@ 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 = Text.to_int(string(al))
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
@ -908,7 +908,7 @@ function gvk_sc_make(render3d_st: mut Render3dState, p: int, nt: int) -> long {
}
let iw = VkDescriptorImageInfo_sizeof
for t in 0 .. nt {
let tex = Text.to_int(string(render3d_st.gvk_sc_tmp[t * 2] / 65536))
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)
@ -1855,9 +1855,9 @@ function gvk_prof_frame(render3d_st: mut Render3dState) -> void {
render3d_st.gvk_prof_frames += 1
if render3d_st.gvk_prof_frames < 120 { return }
let f = render3d_st.gvk_prof_frames
let pipe = Text.to_int(string(render3d_st.gvk_us_pipe)) / f
let set = Text.to_int(string(render3d_st.gvk_us_set)) / f
let draw = Text.to_int(string(render3d_st.gvk_us_draw)) / f
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)