From 46000362ba6064239820264e5c7a66531d24bb03 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 16:46:17 +0300 Subject: [PATCH] render3d: frame keeps 32 to 16 - tables and queues sized at start-up, the rest declared; birth leaks freed The texture and framebuffer tables grow to 4096/1024 and gvk_prime's queue is made in gvk_startup_state; gpu_unit_2d's and the actor lists' lazy starts go. @alloc_ok on layer_room (a layer outgrowing its cap), overlay_init, gvk_read_screen, gvk_hdr_metadata (a settings change), DLSS's gsl_struct/gsl_fn, an actor's part tables (given back by actor_release) and the pool's fallback. Birth leaks freed: gpu_caps_probe's create structs, gpu_caps_fake's text, gvk_note's line, gvk_layout_key's table and each key it grew from, ov_text_wrap's slices, the default sky path. (ludic deps --births still lists freed sites: its walk does not see free().) Compiled. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/actor.ludic | 8 +++++--- packages/ludic.render3d/gpu.ludic | 13 ++++++++++--- packages/ludic.render3d/gpu_vk.ludic | 5 +++++ packages/ludic.render3d/gpu_vk_draw.ludic | 14 ++++++++++++-- packages/ludic.render3d/gpu_vk_res.ludic | 18 ++++++++++-------- packages/ludic.render3d/overlay.ludic | 6 +++++- packages/ludic.render3d/render.ludic | 13 ++++++++++--- packages/ludic.render3d/scatter.ludic | 1 + packages/ludic.render3d/streamline.ludic | 2 ++ 9 files changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 59d63b10..3b0b84ae 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -118,17 +118,18 @@ function actor_remove(render3d_st: mut Render3dState, a: Actor) -> void { # back on the stage (an actor kept across a world swap) function actor_keep(render3d_st: mut Render3dState, a: Actor) -> void { if a == null { return } - if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor } a.row = len(render3d_st.ac_actors) push(render3d_st.ac_actors, a) } # colour one named part of the model (a material name from the file) +@alloc_ok("an actor's part tint or hidden-part table, made once per actor and given back by actor_release") function actor_tint_part(a: Actor, name: string, r: float, g: float, b: float) -> void { if a.model == null { return } let n = len(a.model.prims) if a.ptint == null { a.ptint = words(n * 4); for i in 0 .. n * 4 { a.ptint[i] = 0 } } for i in 0 .. n { if a.model.prims[i].name == name { a.ptint[i * 4] = 1; a.ptint[i * 4 + 1] = float_bits(r); a.ptint[i * 4 + 2] = float_bits(g); a.ptint[i * 4 + 3] = float_bits(b) } } } +@alloc_ok("an actor's part tint or hidden-part table, made once per actor and given back by actor_release") function actor_hide_part(a: Actor, name: string, hidden: bool) -> void { if a.model == null { return } let n = len(a.model.prims) @@ -428,13 +429,14 @@ function ac_caster_count(render3d_st: mut Render3dState, a: Actor) -> void { # every actor off the stage at once, for a world being replaced. Actors own no GL objects; # their models belong to whoever loaded them. function actor_clear_all(render3d_st: mut Render3dState) -> void { - if render3d_st.ac_actors == null { render3d_st.ac_actors = new []Actor } + if render3d_st.ac_actors == null { return } for i in 0 .. len(render3d_st.ac_actors) { render3d_st.ac_actors[i].row = -1 } List.clear(render3d_st.ac_actors) outline_clear(render3d_st) } # A record as `new Actor` makes it, with its matrix and tint: a released one when there is one. +@alloc_ok("the actor pool ran dry: one more record, which actor_release keeps in the pool after") function actor_fresh(render3d_st: mut Render3dState) -> Actor { let sp = render3d_st.ac_spare if sp == null or len(sp) == 0 { @@ -465,7 +467,7 @@ function actor_release(render3d_st: mut Render3dState, a: Actor) -> void { a.skin_owned = false a.model = null; a.skin = null; a.ocol = null a.id = 0 - if render3d_st.ac_spare == null { render3d_st.ac_spare = new []Actor } + if render3d_st.ac_spare == null { return } # made in actor_init, with its room push(render3d_st.ac_spare, a) } diff --git a/packages/ludic.render3d/gpu.ludic b/packages/ludic.render3d/gpu.ludic index 19d54b38..c6ef609d 100644 --- a/packages/ludic.render3d/gpu.ludic +++ b/packages/ludic.render3d/gpu.ludic @@ -231,7 +231,9 @@ function gpu_caps_fake(render3d_st: mut Render3dState, kind: string) -> void { render3d_st.gpu_cap_vulkan = true; render3d_st.gpu_cap_floor = true; render3d_st.gpu_cap_hdr = true if kind == "amd" or kind == "intel" { render3d_st.gpu_cap_rt = true; render3d_st.gpu_cap_mesh = true; render3d_st.gpu_cap_device = `test {kind} GPU`; return } render3d_st.gpu_cap_nvidia = true; render3d_st.gpu_cap_rt = true; render3d_st.gpu_cap_mesh = true; render3d_st.gpu_cap_reflex = true - render3d_st.gpu_cap_rtx = gpu_rtx_generation(`RTX {kind[3 .. 5]}`) + let rtx = `RTX {kind[3 .. 5]}` + render3d_st.gpu_cap_rtx = gpu_rtx_generation(rtx) + free(rtx) render3d_st.gpu_cap_device = `test NVIDIA GeForce RTX {kind[3 .. 5]}` } @@ -265,8 +267,14 @@ function gpu_caps_probe(render3d_st: mut Render3dState) -> void { inst = render3d_st.gvk_inst } else { let out = bytes(8) - if Vk.create_instance(ici, null, out) != VK_SUCCESS { return } + let made = Vk.create_instance(ici, null, out) + free(app); free(ici) + if made != VK_SUCCESS { + free(out) + return + } inst = Vk.get_ptr(out, 0) + free(out) own = true } Vk.put_i32(cnt, 0, 0) @@ -534,7 +542,6 @@ function gpu_tex_bind(render3d_st: mut Render3dState, kind: int, tex: int) -> vo ds_tex_bind(render3d_st) if kind == GPU_TEX2D_ARRAY { render3d_st.gpu_bound_array = tex } else { render3d_st.gpu_bound_2d = tex } if kind != GPU_TEX2D_ARRAY and render3d_st.gpu_unit_cur < 32 { - if render3d_st.gpu_unit_2d == null { render3d_st.gpu_unit_2d = words(32); for i in 0 .. 32 { render3d_st.gpu_unit_2d[i] = -1 } } render3d_st.gpu_unit_2d[render3d_st.gpu_unit_cur] = tex } } diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index d7b827e3..d777bd29 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -30,6 +30,7 @@ function gvk_note(render3d_st: mut Render3dState, msg: string) -> void { let line = msg + "\n" file_write(f, line, len(line)) file_close(f) + free(line) } function gvk_handle(out: bytes) -> long { return Vk.get_i64(out, 0) } @@ -747,6 +748,10 @@ function gvk_startup_state(render3d_st: mut Render3dState) -> void { if render3d_st.gvk_retired_frame == null { render3d_st.gvk_retired_frame = new []int } if render3d_st.gvk_buf_gpu == null { render3d_st.gvk_buf_gpu = new []int } if render3d_st.gvk_retired_buf == null { render3d_st.gvk_retired_buf = new []long; render3d_st.gvk_retired_mem = new []long } + # the prime queue, and the texture and framebuffer tables grown to their working size, now + gvk_prime_tables(render3d_st) + gpu_tx_at(render3d_st, 4095) + gpu_fb_at(render3d_st, 1023) # room made now in every list play pushes into, so a push never grows one (plan 25.2) gvk_room_int(render3d_st.gvk_retired_frame, GVK_LIST_ROOM) gvk_room_long(render3d_st.gvk_retired_buf, GVK_LIST_ROOM) diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index 9508c285..93d4b7a1 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -375,7 +375,7 @@ function gvk_depth_op(f: int) -> int { # at another instance buffer keeps its layout, and so its pipeline. function gvk_layout_key(m: Mesh) -> string { if m == null or m.attrs == null { return "none" } - var k = "" + var k: string = null # each step frees the key it grew from let seen = words(GPU_MAX_ATTRS) var ns = 0 for i in 0 .. m.n_attrs { @@ -384,8 +384,16 @@ function gvk_layout_key(m: Mesh) -> string { var bi = -1 for q in 0 .. ns { if seen[q] == m.attrs[o] and bi < 0 { bi = q } } if bi < 0 { bi = ns; seen[ns] = m.attrs[o]; ns += 1 } - k = k + `{i}:{bi}:{m.attrs[o + 1]}:{m.attrs[o + 2]}:{m.attrs[o + 3]}:{m.attrs[o + 4]}:{m.attrs[o + 5]}:{m.attrs[o + 6]};` + let part = `{i}:{bi}:{m.attrs[o + 1]}:{m.attrs[o + 2]}:{m.attrs[o + 3]}:{m.attrs[o + 4]}:{m.attrs[o + 5]}:{m.attrs[o + 6]};` + if k == null { k = part } else { + let nk = k + part + free(k) + free(part) + k = nk + } } + free(seen) + if k == null { return "" } return k } @@ -982,6 +990,7 @@ function r3d_hdr_calibrate(render3d_st: mut Render3dState, peak: float, paper: f } # what the picture is: graded in BT.709 around D65, highlights to the calibrated peak, paper white average +@alloc_ok("a settings change (HDR output): made once per change") function gvk_hdr_metadata(render3d_st: Render3dState) -> void { let md = bytes(VkHdrMetadataEXT_sizeof) Vk.zero(md, VkHdrMetadataEXT_sizeof) @@ -1615,6 +1624,7 @@ function gvk_blit(render3d_st: mut Render3dState, w: int, h: int, mask: int) -> } # the screen as RGB8, bottom row first, as glReadPixels hands it back (a photograph) +@alloc_ok("asked for by the player or a tool, not by the frame") function gvk_read_screen(render3d_st: mut Render3dState, w: int, h: int, out: pointer) -> void { gvk_present(render3d_st) let px = bytes(render3d_st.gvk_screen_w * render3d_st.gvk_screen_h * 4) diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index 92b0f9be..18d17f17 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -761,14 +761,7 @@ function gvk_buf_reserve(render3d_st: mut Render3dState, b: int, n: int) -> bool # read once (4 bytes, copied out) at the start of the next frame's commands instead. const GVK_PRIME_MAX: int = 16384 function gvk_prime(render3d_st: mut Render3dState, b: int, buf: long) -> void { - if Os.platform() != "macos" { return } - if render3d_st.gvk_prime_b == null { - render3d_st.gvk_prime_b = words(GVK_PRIME_MAX) - render3d_st.gvk_prime_h = new []long - let hs = render3d_st.gvk_prime_h - let none: long = 0 - for i in 0 .. GVK_PRIME_MAX { push(hs, none) } - } + if Os.platform() != "macos" or render3d_st.gvk_prime_b == null { return } # before start-up: its first draw makes it if render3d_st.gvk_prime_n >= GVK_PRIME_MAX { return } render3d_st.gvk_prime_b[render3d_st.gvk_prime_n] = b render3d_st.gvk_prime_h[render3d_st.gvk_prime_n] = buf @@ -839,3 +832,12 @@ function gvk_say_no_conversion(bin: int, bout: int) -> void { print(`r3d: vulkan function gvk_say_no_readback(ifmt: int, fmt: int, ty: int) -> void { print(`r3d: vulkan: no read-back conversion for GL format {ifmt} as {fmt}/{ty}`) } @alloc_ok("a message, built only when it is said: a failure, a warning or a debug switch") function gvk_fail_create_buffer(render3d_st: mut Render3dState, size: int, r: int) -> bool { return gvk_fail(render3d_st, `vkCreateBuffer ({size} bytes)`, r) } + +# gvk_prime's queue: GVK_PRIME_MAX buffers and their handles, made with the device +function gvk_prime_tables(render3d_st: mut Render3dState) -> void { + render3d_st.gvk_prime_b = words(GVK_PRIME_MAX) + render3d_st.gvk_prime_h = new []long + let hs = render3d_st.gvk_prime_h + let none: long = 0 + for i in 0 .. GVK_PRIME_MAX { push(hs, none) } +} diff --git a/packages/ludic.render3d/overlay.ludic b/packages/ludic.render3d/overlay.ludic index cbb44b12..f9c4ec1d 100644 --- a/packages/ludic.render3d/overlay.ludic +++ b/packages/ludic.render3d/overlay.ludic @@ -45,6 +45,7 @@ function ov_pick_prog(render3d_st: mut Render3dState) -> void { render3d_st.ov_prog = render3d_st.ov_prog_sdr } +@alloc_ok("start-up: the overlay's tables are made once") function overlay_init(render3d_st: mut Render3dState, font_dir: string) -> bool { r3d_program_log(render3d_st, "overlay.vert", "overlay.frag", "") render3d_st.ov_prog = gpu_program(render3d_st, "overlay.vert", "overlay.frag", "") @@ -340,11 +341,14 @@ function ov_text_wrap(render3d_st: mut Render3dState, x: int, y: int, size: int, if sp[i] == 10 { stop = i; break } if sp[i] == 32 { last_space = i } let piece: string = s[first .. i + 1] - if ov_text_w(render3d_st, size, piece) > maxw and last_space > first { stop = last_space; break } + let pw = ov_text_w(render3d_st, size, piece) + free(piece) + if pw > maxw and last_space > first { stop = last_space; break } i += 1 } let line: string = s[first .. stop] ov_text(render3d_st, x, ly, size, line, r, g, b, a) + free(line) ly += size * 13 / 10 first = stop while first < n and (sp[first] == 32 or sp[first] == 10) { first += 1 } diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index ef448996..ee32c899 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.ludic @@ -100,9 +100,16 @@ function r3d_load_count() -> int { return 4 + TERRAIN_INIT_STEPS } function r3d_load_step(render3d_st: mut Render3dState, i: int) -> bool { let t = i - 1 if i == 0 { - var sky = render3d_st.r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr" - if render3d_st.r3d_sky_path != null { sky = render3d_st.r3d_sky_path } - if not sky_load(render3d_st, sky) { return false } + # the default sky's path is made only when no path was given, and goes once the sky is read + var sky = render3d_st.r3d_sky_path + var made: string = null + if sky == null { + made = render3d_st.r3d_assets + "/hdri/kloofendal_48d_partly_cloudy_puresky_4k.hdr" + sky = made + } + let got = sky_load(render3d_st, sky) + if made != null { free(made) } + if not got { return false } daylight_init(render3d_st) } else if t >= 0 and t < TERRAIN_INIT_STEPS { if render3d_st.r3d_plate { return true } diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 39a41359..be8d06fd 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -407,6 +407,7 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia # room for n instances (at most the layer's cap), doubling what there is so a fill costs a few copies; # a caller writing l.inst itself reserves first function layer_reserve(l: Layer, n: int) -> void { layer_room(l, n) } +@alloc_ok("a layer outgrowing its capacity, grown once to what it now holds (rare, and bounded by the scene)") function layer_room(l: Layer, n: int) -> void { if n <= l.have { return } var want = max(l.have * 2, n) diff --git a/packages/ludic.render3d/streamline.ludic b/packages/ludic.render3d/streamline.ludic index dde0f1fe..5df5d7d6 100644 --- a/packages/ludic.render3d/streamline.ludic +++ b/packages/ludic.render3d/streamline.ludic @@ -42,6 +42,7 @@ function gsl_header(p: pointer, a: int, b: int, c: int, d: int, e: int, f: int, let v: long = version Vk.put_i64(p, 24, v) } +@alloc_ok("DLSS set-up: its structs and entry points, made once") function gsl_struct(size: int) -> bytes { let p = bytes(size) Vk.zero(p, size) @@ -49,6 +50,7 @@ function gsl_struct(size: int) -> bytes { } # a feature's own function (slDLSSSetOptions, slReflexSleep, ...), or null +@alloc_ok("DLSS set-up: its structs and entry points, made once") function gsl_fn(feature: int, name: string) -> pointer { let out = bytes(8) let zero: long = 0