Merge branch 'r3d/zero-leak' into lang/uifree
This commit is contained in:
commit
0ec39f8e4e
9 changed files with 119 additions and 19 deletions
9
changes/render3d-zero-per-frame.md
Normal file
9
changes/render3d-zero-per-frame.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
bump: patch
|
||||
type: fix
|
||||
**render3d allocates nothing in its steady state.** A buffer released and made again - what a
|
||||
streamed or growing buffer does every few frames - left about 224 bytes behind each time: the
|
||||
Vulkan allocator made a one-slot list per allocation, turned two 64-bit sizes into strings to read
|
||||
them as ints, and rebuilt its spare-record list whole to drop the last entry. It now keeps one
|
||||
slot, converts with `int()`, and pops. `Vk.heap_bytes()` reads malloc's live bytes (macOS), and
|
||||
`examples/rendering/steady.ludic` holds render3d to zero: 5000 buffer rounds and 600 whole frames,
|
||||
0 bytes each (the unfixed allocator: 1,120,000 over the 5000).
|
||||
67
examples/rendering/steady.ludic
Normal file
67
examples/rendering/steady.ludic
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# steady.ludic - render3d allocates nothing in its steady state: a buffer released and made again
|
||||
# (what a streamed or grown buffer does every few frames) and a whole frame, drawn over and over,
|
||||
# leave malloc's live bytes where they were. Prints STEADY OK. The heap is read on the Mac
|
||||
# (Vk.heap_bytes); elsewhere it reads 0 and the check passes on the run alone.
|
||||
#
|
||||
# bin/ludic build examples/rendering/steady.ludic --headless && ./build/steady_headless
|
||||
program Steady {
|
||||
numbers float
|
||||
import "ludic.render3d/r3d.ludic"
|
||||
|
||||
property Marker { on: int = 1 }
|
||||
model Anchor { Marker }
|
||||
|
||||
function scene_draw(render3d_st: mut Render3dState) -> void { }
|
||||
function scene_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void { }
|
||||
function stream_fill(s: Stream, cx: int, cz: int, band: int) -> void { }
|
||||
|
||||
# bytes gained by the heap over n rounds of the buffer path, after a warm-up of its own
|
||||
function buffer_rounds(render3d_st: mut Render3dState, b: int, n: int) -> long {
|
||||
let before = Vk.heap_bytes()
|
||||
for i in 0 .. n {
|
||||
gpu_buffer_upload(render3d_st, b, 4096 + (i % 16) * 4096, null, GPU_DYNAMIC)
|
||||
gpu_buffer_free(render3d_st, b)
|
||||
}
|
||||
return Vk.heap_bytes() - before
|
||||
}
|
||||
function frame_rounds(render3d_st: mut Render3dState, n: int) -> long {
|
||||
let before = Vk.heap_bytes()
|
||||
for i in 0 .. n {
|
||||
r3d_frame(render3d_st, float(i) / 60.0)
|
||||
r3d_present(render3d_st)
|
||||
}
|
||||
return Vk.heap_bytes() - before
|
||||
}
|
||||
|
||||
handler Boot(render3d_st: mut Render3dState) phase Start {
|
||||
spawn Anchor {}
|
||||
r3d_on_draw(render3d_st, fn scene_draw)
|
||||
r3d_on_casters(render3d_st, fn scene_draw_casters)
|
||||
r3d_on_stream_fill(render3d_st, fn stream_fill)
|
||||
r3d_plate_mode(render3d_st, true)
|
||||
render3d_st.r3d_sky_path = "packages/ludic.lab/plate/sky.hdr"
|
||||
if not r3d_init(render3d_st, 320, 180, "Steady") {
|
||||
quit()
|
||||
return
|
||||
}
|
||||
let b = gpu_buffer_new(render3d_st)
|
||||
buffer_rounds(render3d_st, b, 500)
|
||||
let grew_b = buffer_rounds(render3d_st, b, 5000)
|
||||
frame_rounds(render3d_st, 120)
|
||||
let grew_f = frame_rounds(render3d_st, 600)
|
||||
print(`steady: the buffer path gained {grew_b} bytes over 5000 rounds, the frame {grew_f} over 600`)
|
||||
# a few KB of slack for what the system's own libraries keep (Metal's caches, the allocator)
|
||||
var ok = grew_b < 16384
|
||||
if not ok { print("steady: FAILED - releasing and making a buffer again leaves memory behind") }
|
||||
if grew_f >= 65536 {
|
||||
ok = false
|
||||
print("steady: FAILED - a frame leaves memory behind")
|
||||
}
|
||||
if ok { print("STEADY OK") } else { print("STEADY FAILED") }
|
||||
quit()
|
||||
}
|
||||
|
||||
handler Present(render3d_st: mut Render3dState) phase Render {
|
||||
r3d_present(render3d_st)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ import "vk_api.ludic"
|
|||
# Once a frame: on macOS drains what MoltenVK and the layer autoreleased during the last one (a
|
||||
# program pumping its own events has no other pool); nothing elsewhere.
|
||||
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"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -250,3 +250,17 @@ push:
|
|||
store ptr %p, ptr @lvk_pool
|
||||
ret void
|
||||
}
|
||||
|
||||
; ---- the heap in use --------------------------------------------------------------------------
|
||||
; malloc's live bytes across the default zones, for a test that proves a path allocates nothing in
|
||||
; its steady state (render3d's examples/rendering/steady.ludic). malloc_statistics_t is four
|
||||
; words: blocks_in_use (u32, padded), size_in_use, max_size_in_use, size_allocated.
|
||||
declare void @malloc_zone_statistics(ptr, ptr)
|
||||
define i64 @lvk_heap_bytes() {
|
||||
entry:
|
||||
%st = alloca [4 x i64], align 8
|
||||
call void @malloc_zone_statistics(ptr null, ptr %st)
|
||||
%p = getelementptr [4 x i64], ptr %st, i32 0, i32 1
|
||||
%n = load i64, ptr %p
|
||||
ret i64 %n
|
||||
}
|
||||
|
|
|
|||
|
|
@ -369,3 +369,9 @@ define void @lvk_frame_pool() {
|
|||
entry:
|
||||
ret void
|
||||
}
|
||||
|
||||
; the heap's live bytes are asked for on the Mac only (vk_mac.ll); 0 here, and a test skips its check
|
||||
define i64 @lvk_heap_bytes() {
|
||||
entry:
|
||||
ret i64 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1054,6 +1054,7 @@ function cmd_dev_test() -> int {
|
|||
headless_case("rendering/ui_render3d", "ok", "ui_render3d.ludic (ludic.ui's render3d backend builds against the renderer)")
|
||||
headless_line_case("rendering/release", "RELEASE OK", "release.ludic (model_release: meshes go, a shared texture stays until its last user goes)")
|
||||
headless_line_case("rendering/bc", "BC OK", "bc.ludic (a .dds beside a .png arrives BC7-compressed with every mip level)")
|
||||
headless_line_case("rendering/steady", "STEADY OK", "steady.ludic (a buffer made again and a whole frame leave malloc's live bytes where they were)")
|
||||
migrate_component_case()
|
||||
migrate_foreign_case()
|
||||
reject_case("rejected/runtime_type_clash", "PadButton is the runtime's enum", "a program's type named like one of the runtime's is refused")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue