- Json.free_all (value_free_all): a parsed tree's nodes, lists and strings. render3d frees each glTF document that way at the next load; the names kept out of it are copies (a primitive's material, a skin's joints, an animation clip's name in ludic.anim) - a model's strings were ~640 KB left behind per load - jp_number made a digits list per decimal in a document and never freed it - gvk_layout_id matches a mesh's layout as numbers in a scratch made once, against the layouts known end to end; a new mesh no longer builds a key string - gvk_mem_new puts a new block into the record of one given back rather than appending, so a buffer made again every few frames no longer grows the block lists steady.ludic adds a glTF parsed and freed whole 200 times: 0 bytes (38,400 before the digits fix), beside the buffer path and the frame, still 0. A model loaded and let go still keeps ~2 KB a round (texture and buffer handles are not reused yet); it is bounded at 8 KB a round. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
4.1 KiB
Text
102 lines
4.1 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); 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
|
|
}
|
|
|
|
# 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 = Vk.heap_bytes()
|
|
for i in 0 .. n {
|
|
let m = gltf_load(render3d_st, "packages/ludic.lab/plate", "plate.gltf", "plate")
|
|
model_release(render3d_st, m)
|
|
}
|
|
return Vk.heap_bytes() - before
|
|
}
|
|
|
|
# bytes gained over n parses of a glTF document, each freed whole (Json.free_all): strings too
|
|
function parse_rounds(text: string, n: int) -> long {
|
|
let before = Vk.heap_bytes()
|
|
for i in 0 .. n {
|
|
let doc = Json.parse(text)
|
|
Json.free_all(doc)
|
|
}
|
|
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)
|
|
model_rounds(render3d_st, 20)
|
|
let grew_m = model_rounds(render3d_st, 200)
|
|
let text = Fs.read_text("packages/ludic.lab/plate/plate.gltf")
|
|
parse_rounds(text, 20)
|
|
let grew_p = parse_rounds(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`)
|
|
# 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 grew_p >= 16384 {
|
|
ok = false
|
|
print("steady: FAILED - a parsed document freed whole leaves memory behind")
|
|
}
|
|
# a load keeps a few records of its own for now (handles are not reused); a document's strings
|
|
# left behind were ~640 KB a model, so a bound of 8 KB a round is what this holds
|
|
if grew_m >= 200 * 8192 {
|
|
ok = false
|
|
print("steady: FAILED - a model loaded and let go 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)
|
|
}
|
|
}
|