From f4e121ae944bd66ed9191e5784aed693632ddc54 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 16:02:21 +0300 Subject: [PATCH] render3d: actor_own_skin / skin_clone_free, and a frame's scratch and tables made with the device actor_own_skin(a) gives an actor its own clone of its model's skeleton and actor_release frees it (skin_clone_free: the pose, matrices, views and scratch the clone made; the rest data stays its source's). gvk_startup_state, at the end of gvk_init, makes the scratch and tables a draw used to make on first need (gvk_tmp_buf, the pipeline fast cache, the set key and per-program uniform offsets pre-sized to 4096 programs, skip/seen/size words, spare and retired lists, the grass cull's words), and those frame paths no longer start them. Compiled (steady). Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/actor.ludic | 16 ++++++++- packages/ludic.render3d/gpu_vk.ludic | 42 ++++++++++++++++++++++- packages/ludic.render3d/gpu_vk_draw.ludic | 18 +--------- packages/ludic.render3d/gpu_vk_res.ludic | 5 --- packages/ludic.render3d/grass_gpu.ludic | 6 +--- packages/ludic.render3d/skin.ludic | 11 ++++++ 6 files changed, 69 insertions(+), 29 deletions(-) diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 748584b4..c6fae5b3 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -34,6 +34,7 @@ property Actor { cutout: bool = false, # alpha-tested (a flame's cards) emissive: float = 0.0, # float bits: self-lit strength skin: Skin, # this instance's own pose (skin_clone); null: the model's + skin_owned: bool = false, # made by actor_own_skin: actor_release frees it ptint: words, # 4 per primitive: on flag, r, g, b (a part's own colour) hide: words, # 1 per primitive: skip it (a cap taken off) radius: float = 0.0, # float bits: bounding radius for culling (0 = never culled) @@ -443,7 +444,7 @@ function actor_fresh(render3d_st: mut Render3dState) -> Actor { m4_identity(a.mat) a.visible = true; a.cast_hidden = false; a.casts = true; a.row = -1 a.cutout = false; a.emissive = 0.0; a.radius = 0.0; a.cull = 0.0; a.outline = 0.0 - a.skin = null; a.ocol = null + a.skin = null; a.skin_owned = false; a.ocol = null return a } @@ -455,8 +456,21 @@ function actor_release(render3d_st: mut Render3dState, a: Actor) -> void { actor_remove(render3d_st, a) if a.ptint != null { free(a.ptint); a.ptint = null } if a.hide != null { free(a.hide); a.hide = null } + if a.skin_owned and a.skin != null { skin_clone_free(a.skin) } + 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 } push(render3d_st.ac_spare, a) } + +# The actor poses on its own copy of its model's skeleton (a person, an animal), which it keeps until +# actor_release gives it back with the record: a clone made by the caller and dropped with the actor +# was one kept skeleton per figure ever placed. +function actor_own_skin(a: Actor) -> Skin { + if a == null or a.model == null or a.model.skin == null { return null } + if a.skin_owned and a.skin != null { skin_clone_free(a.skin) } + a.skin = skin_clone(a.model.skin) + a.skin_owned = true + return a.skin +} diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index c338d05e..7070d36c 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -279,6 +279,7 @@ function gvk_init(render3d_st: mut Render3dState) -> bool { if not gvk_cmd_init(render3d_st) { return false } render3d_st.gvk_ready = true + gvk_startup_state(render3d_st) print(`r3d: vulkan on {render3d_st.gvk_device_name}, anisotropy up to {gvk_aniso_x(render3d_st.gvk_max_aniso)}x, multi-draw indirect {render3d_st.gvk_has_mdi}, indirect count {render3d_st.gvk_has_dic}, mesh shaders {render3d_st.gvk_has_mesh}`) return true } @@ -543,7 +544,6 @@ function gvk_cmd_init(render3d_st: mut Render3dState) -> bool { const GVK_TMP_BYTES: int = 1048576 function gvk_tmp(render3d_st: mut Render3dState, n: int) -> pointer { - if render3d_st.gvk_tmp_buf == null { render3d_st.gvk_tmp_buf = bytes(GVK_TMP_BYTES) } let sz = (n + 15) / 16 * 16 if sz > GVK_TMP_BYTES { print(`r3d: vulkan: {n} bytes of scratch asked for at once`); return null } if render3d_st.gvk_tmp_off + sz > GVK_TMP_BYTES { render3d_st.gvk_tmp_off = 0 } @@ -726,3 +726,43 @@ function gvk_labels_close(render3d_st: mut Render3dState) -> void { render3d_st.gvk_label_depth -= 1 } } + +# Everything a frame's draws use as scratch or as a table, made once the device is up: each was made +# on the first draw that needed it, which put an allocation in whichever frame that was (plan 25.2) +const GVK_PROGS_MAX: int = 4096 +function gvk_startup_state(render3d_st: mut Render3dState) -> void { + let zero: long = 0 + render3d_st.gvk_tmp_buf = bytes(GVK_TMP_BYTES) + render3d_st.gvk_skip_said = words(4096) + for i in 0 .. 4096 { render3d_st.gvk_skip_said[i] = 0 } + render3d_st.gvk_seen = words(GPU_MAX_VBUFS) + if render3d_st.gvk_state == null { render3d_st.gvk_state = new GvkState } + render3d_st.gvk_size_buf = words(4) + if render3d_st.gvk_tex_spare == null { render3d_st.gvk_tex_spare = new []int } + if render3d_st.gvk_buf_spare == null { render3d_st.gvk_buf_spare = new []int } + 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 } + render3d_st.gvk_sc_tmp = new []long + let sct = render3d_st.gvk_sc_tmp + for i in 0 .. 130 { push(sct, zero) } + render3d_st.gvk_set_offs = bytes(16) + render3d_st.gvk_ub_frame = new []int; render3d_st.gvk_ub_offv = new []int; render3d_st.gvk_ub_offf = new []int + let ubf = render3d_st.gvk_ub_frame + let ubv = render3d_st.gvk_ub_offv + let ubo = render3d_st.gvk_ub_offf + for i in 0 .. GVK_PROGS_MAX { + push(ubf, 0); push(ubv, 0); push(ubo, 0) + } + render3d_st.gvk_pc_prog = new []int; render3d_st.gvk_pc_layout = new []int; render3d_st.gvk_pc_state = new []int; render3d_st.gvk_pc_bias = new []int + render3d_st.gvk_pc_pass = new []int; render3d_st.gvk_pc_pipe = new []long + render3d_st.gvk_pc_last = words(4096) + for i in 0 .. 4096 { render3d_st.gvk_pc_last[i] = -1 } + render3d_st.gg_rb = words(4); render3d_st.gg_cb = words(1); render3d_st.gg_bufs = words(3) + render3d_st.gg_texs = words(3); render3d_st.gg_pr = words(48) + if render3d_st.gpu_u_tmp == null { render3d_st.gpu_u_tmp = words(4) } + 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 } + } + if render3d_st.cam_planes == null { render3d_st.cam_planes = floats(16) } +} diff --git a/packages/ludic.render3d/gpu_vk_draw.ludic b/packages/ludic.render3d/gpu_vk_draw.ludic index 7bfa552d..883da82b 100644 --- a/packages/ludic.render3d/gpu_vk_draw.ludic +++ b/packages/ludic.render3d/gpu_vk_draw.ludic @@ -799,11 +799,6 @@ function gvk_draw_set(render3d_st: mut Render3dState, p: int, tx: words, tx_w: i let zero: long = 0 let v = render3d_st.gvk_prog_var[p] gvk_uniform_blocks(render3d_st, p) - if render3d_st.gvk_sc_tmp == null { - render3d_st.gvk_sc_tmp = new []long - render3d_st.gvk_set_offs = bytes(16) - render3d_st.gvk_ub_frame = new []int; render3d_st.gvk_ub_offv = new []int; render3d_st.gvk_ub_offf = new []int - } while len(render3d_st.gvk_ub_frame) <= p { push(render3d_st.gvk_ub_frame, 0); push(render3d_st.gvk_ub_offv, 0); push(render3d_st.gvk_ub_offf, 0) } let nt = len(v.t_name) # the key: every texture as this draw resolves it, and its sampler @@ -1268,7 +1263,6 @@ function gvk_draw(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, if pipe == 0 { # Say so, once per program: a driver that refuses a pipeline other drivers accept (MoltenVK # rejected every skinned one) otherwise leaves a whole layer missing from the frame in silence. - if render3d_st.gvk_skip_said == null { render3d_st.gvk_skip_said = words(4096); for i in 0 .. 4096 { render3d_st.gvk_skip_said[i] = 0 } } if p < 4096 and render3d_st.gvk_skip_said[p] == 0 { render3d_st.gvk_skip_said[p] = 1 print(`r3d: vulkan: no pipeline for {gpu_program_key(render3d_st, p)}; its draws are skipped`) @@ -1291,7 +1285,6 @@ function gvk_draw(render3d_st: mut Render3dState, p: int, m: Mesh, st: GvkState, let bufs = gvk_tmp(render3d_st, 8 * (GPU_MAX_VBUFS + 1)) let offs = gvk_tmp(render3d_st, 8 * (GPU_MAX_VBUFS + 1)) Vk.zero(offs, 8 * (GPU_MAX_VBUFS + 1)) - if render3d_st.gvk_seen == null { render3d_st.gvk_seen = words(GPU_MAX_VBUFS) } let seen = render3d_st.gvk_seen var nbd = 0 for i in 0 .. m.n_attrs { @@ -1435,8 +1428,7 @@ function gvk_open(render3d_st: mut Render3dState, w: int, h: int, title: string) win_gl_resize(w, h) # macOS: the Metal layer sets the backing scale, so it exists before the drawable is measured if Os.platform() == "macos" and win_metal_layer() == null { render3d_st.gvk_why = "no Metal layer on the window"; return false } - if render3d_st.gvk_size_buf == null { render3d_st.gvk_size_buf = words(4) } - win_gl_drawable(render3d_st.gvk_size_buf) + win_gl_drawable(render3d_st.gvk_size_buf) if render3d_st.gvk_size_buf[0] > 0 and render3d_st.gvk_size_buf[1] > 0 { gl_set_drawable(render3d_st.gvk_size_buf[0], render3d_st.gvk_size_buf[1]) } gl_set_pixel_scale(win_gl_scale()) } @@ -1508,7 +1500,6 @@ function gvk_fb_forget(render3d_st: mut Render3dState, fb: int) -> void { # the render state gpu.ludic has cached, with OpenGL's defaults where nothing was set yet function gvk_state_now(render3d_st: mut Render3dState) -> GvkState { - if render3d_st.gvk_state == null { render3d_st.gvk_state = new GvkState } let st = render3d_st.gvk_state st.depth_test = 0; if render3d_st.gpu_s_depth_test == 1 { st.depth_test = 1 } st.depth_write = 1; if render3d_st.gpu_s_depth_write == 0 { st.depth_write = 0 } @@ -1833,7 +1824,6 @@ function gvk_present_window(render3d_st: mut Render3dState) -> void { # a window's client area changed: the screen images and the swapchain follow it function gvk_resize_check(render3d_st: mut Render3dState) -> bool { if render3d_st.gvk_swap == 0 { return false } - if render3d_st.gvk_size_buf == null { render3d_st.gvk_size_buf = words(4) } win_gl_drawable(render3d_st.gvk_size_buf) let w = render3d_st.gvk_size_buf[0] let h = render3d_st.gvk_size_buf[1] @@ -1967,12 +1957,6 @@ function gvk_pipeline_fast(render3d_st: mut Render3dState, p: int, m: Mesh, st: state = state | ((st.depth_func & 15) << 8) | ((st.cull_face & 15) << 12) | (gvk_blend_index(st.blend_src) << 16) | (gvk_blend_index(st.blend_dst) << 20) let bias = st.bias_factor * 31 + st.bias_units let pass = ((n_color * 1000 + color_fmt) * 1000 + depth_fmt) * 64 + samples - if render3d_st.gvk_pc_prog == null { - render3d_st.gvk_pc_prog = new []int; render3d_st.gvk_pc_layout = new []int; render3d_st.gvk_pc_state = new []int; render3d_st.gvk_pc_bias = new []int - render3d_st.gvk_pc_pass = new []int; render3d_st.gvk_pc_pipe = new []long - render3d_st.gvk_pc_last = words(4096) - for i in 0 .. 4096 { render3d_st.gvk_pc_last[i] = -1 } - } if p < 4096 { let k = render3d_st.gvk_pc_last[p] if k >= 0 and render3d_st.gvk_pc_layout[k] == layout and render3d_st.gvk_pc_state[k] == state and render3d_st.gvk_pc_pass[k] == pass and render3d_st.gvk_pc_bias[k] == bias { return render3d_st.gvk_pc_pipe[k] } diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index b6e89595..42a8a69b 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -62,7 +62,6 @@ function gvk_channel_bytes(ifmt: int) -> int { function gvk_tex_give_back(render3d_st: mut Render3dState, tex: int) -> void { if tex <= 0 or tex >= len(render3d_st.gvk_tex_image) or render3d_st.gvk_tex_image[tex] == 0 { return } gvk_tex_release(render3d_st, tex) - if render3d_st.gvk_tex_spare == null { render3d_st.gvk_tex_spare = new []int } push(render3d_st.gvk_tex_spare, tex) } @alloc_ok("made once per resource and kept for its life (a texture, program, sampler, view, layout or memory block is created when first asked for)") @@ -660,7 +659,6 @@ function gvk_buf_delete(render3d_st: mut Render3dState, b: int) -> void { if b <= 0 or b >= len(render3d_st.gvk_buf) { return } gvk_buf_release(render3d_st, b) if render3d_st.gvk_buf_gpu != null and b < len(render3d_st.gvk_buf_gpu) { render3d_st.gvk_buf_gpu[b] = 0 } - if render3d_st.gvk_buf_spare == null { render3d_st.gvk_buf_spare = new []int } let sp = render3d_st.gvk_buf_spare for i in 0 .. len(sp) { if sp[i] == b { return } } push(sp, b) @@ -672,7 +670,6 @@ function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void { if gvk_buf_busy(render3d_st, b) { # a draw recorded this frame (or the frame in flight) still reads it: destroy it once the frame has been submitted push(render3d_st.gvk_retired_buf, render3d_st.gvk_buf[b]); push(render3d_st.gvk_retired_mem, render3d_st.gvk_buf_mem[b]) - if render3d_st.gvk_retired_frame == null { render3d_st.gvk_retired_frame = new []int } while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) - 1 { push(render3d_st.gvk_retired_frame, 0) } push(render3d_st.gvk_retired_frame, render3d_st.gvk_buf_used[b]) } else { @@ -695,7 +692,6 @@ function gvk_retire_flush(render3d_st: mut Render3dState) -> void { gvk_retire_u # the frame being recorded read stays function gvk_retire_upto(render3d_st: mut Render3dState, done: int) -> void { if render3d_st.gvk_retired_buf == null { return } - if render3d_st.gvk_retired_frame == null { render3d_st.gvk_retired_frame = new []int } while len(render3d_st.gvk_retired_frame) < len(render3d_st.gvk_retired_buf) { push(render3d_st.gvk_retired_frame, 0) } # compacted in place: three fresh lists a frame were never given back var w = 0 @@ -721,7 +717,6 @@ function gvk_retire_upto(render3d_st: mut Render3dState, done: int) -> void { # A buffer a compute pass writes: a draw later in the same frame reads what the GPU put there, # so it is never swapped for fresh storage because it was used this frame (gvk_buf_reserve). function gvk_buf_gpu_owned(render3d_st: mut Render3dState, b: int) -> void { - if render3d_st.gvk_buf_gpu == null { render3d_st.gvk_buf_gpu = new []int } while len(render3d_st.gvk_buf_gpu) <= b { push(render3d_st.gvk_buf_gpu, 0) } render3d_st.gvk_buf_gpu[b] = 1 } diff --git a/packages/ludic.render3d/grass_gpu.ludic b/packages/ludic.render3d/grass_gpu.ludic index da9f026a..aef4afdd 100644 --- a/packages/ludic.render3d/grass_gpu.ludic +++ b/packages/ludic.render3d/grass_gpu.ludic @@ -102,11 +102,7 @@ function gg_cull_frame(render3d_st: mut Render3dState) -> void { gg_tiles(render3d_st, 4, 0.0, 16.0) gg_tiles(render3d_st, 8, 16.0, 40.0) gg_tiles(render3d_st, 16, 40.0, render3d_st.grass_radius) - # made once: a words() a frame is a calloc nothing frees - if render3d_st.gg_rb == null { - render3d_st.gg_rb = words(4); render3d_st.gg_cb = words(1); render3d_st.gg_bufs = words(3) - render3d_st.gg_texs = words(3); render3d_st.gg_pr = words(48) - } + # made once, with the device (gvk_startup_state) let rb = render3d_st.gg_rb rb[0] = GG_BANDS; rb[1] = 0; rb[2] = 0; rb[3] = 0 let cb = render3d_st.gg_cb diff --git a/packages/ludic.render3d/skin.ludic b/packages/ludic.render3d/skin.ludic index 1752d451..92d6f554 100644 --- a/packages/ludic.render3d/skin.ludic +++ b/packages/ludic.render3d/skin.ludic @@ -246,3 +246,14 @@ function skin_free(sk: Skin) -> void { if sk.bones_v != null { free(sk.bones_v) } free(sk) } + +# A skin_clone given back: what the clone made for itself (its pose, its matrices and their views, its +# scratch). The rest data, the joints and the names are its source's and stay. +function skin_clone_free(sk: Skin) -> void { + if sk == null { return } + free(sk.pose_r); free(sk.pose_t); free(sk.gmat); free(sk.bones) + if sk.gmat_v != null { free(sk.gmat_v) } + if sk.bones_v != null { free(sk.bones_v) } + free(sk.tmp_l); free(sk.tmp_q); free(sk.tmp_a); free(sk.tmp_b); free(sk.tmp_c); free(sk.tmp_v) + free(sk) +}