242 lines
10 KiB
Text
242 lines
10 KiB
Text
# 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) once the device is idle; 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 { actor_draw(render3d_st) }
|
|
function scene_draw_casters(render3d_st: mut Render3dState, light_vp: floats) -> void { }
|
|
# a chunk of 20 to 51 instances, different for every cell
|
|
function stream_fill(render3d_st: mut Render3dState, s: Stream, cx: int, cz: int, band: int) -> void {
|
|
let n = 20 + ((cx * 7 + cz * 13 + band) & 31)
|
|
for i in 0 .. n { stream_emit(render3d_st, s, float(cx) * 32.0 + float(i), 0.0, float(cz) * 32.0, 1.0, 0.0, 0.5, 0.0) }
|
|
}
|
|
|
|
# The heap once the device is idle and MoltenVK's completion handlers, which release a finished
|
|
# command buffer on their own thread, have caught up: two reads 20 ms apart that agree. Read at
|
|
# once, a machine busy with other work showed ~80 KB of buffers still being handed back.
|
|
function settled(render3d_st: Render3dState) -> long {
|
|
Vk.device_wait_idle(render3d_st.gvk_dev)
|
|
var last = Vk.heap_bytes()
|
|
for k in 0 .. 100 {
|
|
Time.sleep_us(20000)
|
|
let now = Vk.heap_bytes()
|
|
if now == last { return now }
|
|
last = now
|
|
}
|
|
return last
|
|
}
|
|
|
|
# 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 = settled(render3d_st)
|
|
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 settled(render3d_st) - before
|
|
}
|
|
function frame_rounds(render3d_st: mut Render3dState, n: int) -> long {
|
|
let before = settled(render3d_st)
|
|
for i in 0 .. n {
|
|
r3d_frame(render3d_st, float(i) / 60.0)
|
|
r3d_present(render3d_st)
|
|
}
|
|
return settled(render3d_st) - before
|
|
}
|
|
|
|
# bytes gained over n loads and releases of one model: its parse, its meshes and its textures
|
|
function model_rounds(render3d_st: mut Render3dState, n: int) -> long {
|
|
let before = settled(render3d_st)
|
|
for i in 0 .. n {
|
|
let m = gltf_load(render3d_st, "packages/ludic.lab/plate", "plate.gltf", "plate")
|
|
model_release(render3d_st, m)
|
|
}
|
|
return settled(render3d_st) - before
|
|
}
|
|
|
|
# bytes gained over n actors placed and released (actor_release): the records come round again
|
|
function actor_rounds(render3d_st: mut Render3dState, m: Model, n: int) -> long {
|
|
let before = settled(render3d_st)
|
|
for i in 0 .. n {
|
|
let a = actor_new(render3d_st, m)
|
|
actor_place(a, float(i % 7), 0.0, 0.0, 0.0)
|
|
actor_hide_part(a, "none", true)
|
|
actor_release(render3d_st, a)
|
|
}
|
|
return settled(render3d_st) - before
|
|
}
|
|
|
|
# bytes gained while a frame draws more and more: 20 actors to 200, 30 frames at each count.
|
|
# MoltenVK's command pooling kept every command a frame had recorded (~650 bytes a draw more).
|
|
function ramp_rounds(render3d_st: mut Render3dState, m: Model) -> long {
|
|
let acts = new []Actor
|
|
for i in 0 .. 200 {
|
|
let a = actor_new(render3d_st, m)
|
|
a.cull = 0.0
|
|
a.visible = false
|
|
actor_place(a, float(i % 20) - 10.0, 0.0, -float(i / 20), 0.0)
|
|
push(acts, a)
|
|
}
|
|
for i in 0 .. 20 { acts[i].visible = true }
|
|
frame_rounds(render3d_st, 30)
|
|
let before = settled(render3d_st)
|
|
for k in 2 .. 11 {
|
|
for i in 0 .. k * 20 { acts[i].visible = true }
|
|
frame_rounds(render3d_st, 30)
|
|
}
|
|
let grew = settled(render3d_st) - before
|
|
for i in 0 .. 200 { actor_release(render3d_st, acts[i]) }
|
|
return grew
|
|
}
|
|
|
|
# the camera over `cells` new cells from cell `from` on, two frames a cell; the heap's gain across them
|
|
function stream_window(render3d_st: mut Render3dState, from: int, cells: int) -> long {
|
|
let before = settled(render3d_st)
|
|
for step in from .. from + cells {
|
|
cam_set(render3d_st, float(step) * 32.0, 3.0, float(step % 7) * 32.0, 0.0, -10.0)
|
|
for f in 0 .. 2 {
|
|
r3d_frame(render3d_st, float(step * 2 + f) / 60.0)
|
|
r3d_present(render3d_st)
|
|
}
|
|
}
|
|
return settled(render3d_st) - before
|
|
}
|
|
|
|
# bytes gained while the camera crosses new ground: a streamed layer's cache holds at most 256
|
|
# chunks, so over a warm-up of 300 cells it fills, evicts and compacts; then the least of three
|
|
# windows of 150 more. A leak grows in every window; a pool finding a new high-water mark, or the
|
|
# GPU's lag on a busy machine (one sample read +75 KB where its twin read -5 KB), does not repeat
|
|
# three times. Each chunk once brought its own record and copy, made as the ground was found.
|
|
function stream_rounds(render3d_st: mut Render3dState, m: Model) -> long {
|
|
render3d_st.STREAM_MAX_CHUNKS = 256
|
|
let l = layer_new(render3d_st, m, 20000, false, 0.0, 0.0, 400.0)
|
|
let st = stream_new(render3d_st, l, 32.0, 96.0, 32.0, 64.0, 80.0, 96.0)
|
|
cam_set(render3d_st, 0.0, 3.0, 0.0, 0.0, -10.0)
|
|
frame_rounds(render3d_st, 10)
|
|
stream_window(render3d_st, 1, 300)
|
|
var grew = stream_window(render3d_st, 301, 150)
|
|
for w in 1 .. 3 {
|
|
let g = stream_window(render3d_st, 301 + w * 150, 150)
|
|
if g < grew { grew = g }
|
|
}
|
|
# every chunk kept, after all that evicting and compacting, still holds its own cell's instances
|
|
var wrong = 0
|
|
for i in 0 .. st.n {
|
|
let c = st.chunks[i]
|
|
let cx = c.key / 4 / 8192 - 4096
|
|
if c.count < 20 or float_from_bits(st.arena[c.off]) != float(cx) * 32.0 { wrong += 1 }
|
|
}
|
|
if wrong > 0 or st.n == 0 or l.count == 0 {
|
|
print(`steady: FAILED - {wrong} of {st.n} kept chunks hold another cell's instances ({l.count} drawn)`)
|
|
return 1000000
|
|
}
|
|
return grew
|
|
}
|
|
|
|
# bytes gained over n parses of a glTF document, each freed whole (Json.free_all): strings too
|
|
function parse_rounds(render3d_st: Render3dState, text: string, n: int) -> long {
|
|
let before = settled(render3d_st)
|
|
for i in 0 .. n {
|
|
let doc = Json.parse(text)
|
|
Json.free_all(doc)
|
|
}
|
|
return settled(render3d_st) - 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)
|
|
# the least of three windows: a leak grows in every one, while a busy GPU's lag (a valley
|
|
# self-test beside it once read 87 KB) does not repeat three times
|
|
var grew_f = frame_rounds(render3d_st, 600)
|
|
for w in 0 .. 2 {
|
|
let g = frame_rounds(render3d_st, 600)
|
|
if g < grew_f { grew_f = g }
|
|
}
|
|
model_rounds(render3d_st, 20)
|
|
let grew_m = model_rounds(render3d_st, 200)
|
|
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")
|
|
parse_rounds(render3d_st, text, 20)
|
|
let grew_p = parse_rounds(render3d_st, text, 200)
|
|
print(`steady: the buffer path gained {grew_b} bytes over 5000 rounds, the frame {grew_f} over 600, a glTF parsed and freed {grew_p} over 200, a model loaded and let go {grew_m} over 200, an actor placed and released {grew_a} over 2000, a frame drawing 20 to 200 actors {grew_r}, a stream over 150 new cells (the least of three windows) {grew_s}`)
|
|
# 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 >= 4096 {
|
|
ok = false
|
|
print("steady: FAILED - a frame leaves memory behind")
|
|
}
|
|
if grew_p >= 16384 {
|
|
ok = false
|
|
print("steady: FAILED - a parsed document freed whole leaves memory behind")
|
|
}
|
|
# a load and a release leave nothing: 0 over 200 (a document's strings were ~640 KB a model, a
|
|
# texture's create infos and its .dds path ~700 bytes a round)
|
|
if grew_m >= 4096 {
|
|
ok = false
|
|
print("steady: FAILED - a model loaded and let go leaves memory behind")
|
|
}
|
|
if grew_a >= 4096 {
|
|
ok = false
|
|
print("steady: FAILED - an actor placed and released leaves memory behind")
|
|
}
|
|
if grew_r >= 16384 {
|
|
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")
|
|
}
|
|
# 4 KB over 150 new cells, in the least of three windows: a chunk's own record or copy kept would be
|
|
# 20 to 51 instances' worth (over 150 bytes) a cell, some 25 KB a window
|
|
if grew_s >= 4096 {
|
|
ok = false
|
|
print("steady: FAILED - streaming new ground 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)
|
|
}
|
|
}
|