From bc50bd9b9a7d3906a020811551ea14a1670f774d Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 17:08:15 +0300 Subject: [PATCH 1/2] render3d: @owns on the handles records hold Target's framebuffer and its colour and depth textures, a Mesh's index buffer (gvk_buf_new / gvk_buf_delete now @creates / @releases GpuBuffer too), a Prim's mesh and an Actor's own skin clone. ludic deps --resources over main: resource_drops 0, owned_leaks 0; a probe freeing a Target's fbo alone was reported for its colour and depth (2), then removed. Compiled (steady). Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/actor.ludic | 2 +- packages/ludic.render3d/gltf.ludic | 2 +- packages/ludic.render3d/gpu_vk_res.ludic | 2 ++ packages/ludic.render3d/mesh.ludic | 2 +- packages/ludic.render3d/programs.ludic | 6 +++--- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/ludic.render3d/actor.ludic b/packages/ludic.render3d/actor.ludic index 665a5b48..0d53a1e9 100644 --- a/packages/ludic.render3d/actor.ludic +++ b/packages/ludic.render3d/actor.ludic @@ -33,7 +33,7 @@ property Actor { row: int = -1, # where it stands in ac_actors, so it leaves in O(1); -1 off the stage 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 + @owns(SkinClone) 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) diff --git a/packages/ludic.render3d/gltf.ludic b/packages/ludic.render3d/gltf.ludic index f6ce8f02..8544b1e4 100644 --- a/packages/ludic.render3d/gltf.ludic +++ b/packages/ludic.render3d/gltf.ludic @@ -6,7 +6,7 @@ # ============================================================================ property Prim { - mesh: Mesh, + @owns(Mesh) mesh: Mesh, diff: int = 0, nrm: int = 0, arm: int = 0, diff --git a/packages/ludic.render3d/gpu_vk_res.ludic b/packages/ludic.render3d/gpu_vk_res.ludic index 18d17f17..588f7b5c 100644 --- a/packages/ludic.render3d/gpu_vk_res.ludic +++ b/packages/ludic.render3d/gpu_vk_res.ludic @@ -634,6 +634,7 @@ function gvk_sampler(render3d_st: mut Render3dState, min_f: int, mag_f: int, wra # destroyed at gvk_retire_flush, after the frame's work is done. @alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") +@creates(GpuBuffer) function gvk_buf_new(render3d_st: mut Render3dState) -> int { let zero: long = 0 if render3d_st.gvk_buf == null { @@ -655,6 +656,7 @@ function gvk_buf_new(render3d_st: mut Render3dState) -> int { # a buffer done with for good (a mesh freed): its storage goes, and its handle is handed out again # by gvk_buf_new. gvk_buf_release alone keeps the handle, for a caller that fills it again. +@releases(GpuBuffer) 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) diff --git a/packages/ludic.render3d/mesh.ludic b/packages/ludic.render3d/mesh.ludic index 058289f4..ceb4bb55 100644 --- a/packages/ludic.render3d/mesh.ludic +++ b/packages/ludic.render3d/mesh.ludic @@ -7,7 +7,7 @@ property Mesh { vao: int = 0, # OpenGL's vertex array object (gpu.ludic's, on that backend) vbo: int = 0, # the first vertex buffer - ebo: int = 0, + @owns(GpuBuffer) ebo: int = 0, count: int = 0, # indices (ebo != 0) or vertices mode: int = 4, # GL_TRIANGLES itype: int = 0x1405, # GL_UNSIGNED_INT diff --git a/packages/ludic.render3d/programs.ludic b/packages/ludic.render3d/programs.ludic index 8ef33db4..4d101233 100644 --- a/packages/ludic.render3d/programs.ludic +++ b/packages/ludic.render3d/programs.ludic @@ -64,9 +64,9 @@ function r3d_bind_2d(render3d_st: mut Render3dState, prog: int, name: string, un # A framebuffer with one colour texture (and optionally a depth texture). property Target { - fbo: int = 0, - color: int = 0, - depth: int = 0, + @owns(GpuFramebuffer) fbo: int = 0, + @owns(GpuTexture) color: int = 0, + @owns(GpuTexture) depth: int = 0, w: int = 0, h: int = 0 } From 1bf347087318ee2efa2c7bd701a0afd16f528eab Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 17:09:00 +0300 Subject: [PATCH 2/2] render3d: a model owns its skin - @creates(Skin) skin_load, @releases(Skin) skin_free, @owns on Model.skin resource_drops 0 and owned_leaks 0 over main. Compiled (steady). Co-Authored-By: Claude Opus 5.5 --- packages/ludic.render3d/gltf.ludic | 2 +- packages/ludic.render3d/skin.ludic | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ludic.render3d/gltf.ludic b/packages/ludic.render3d/gltf.ludic index 8544b1e4..f3804237 100644 --- a/packages/ludic.render3d/gltf.ludic +++ b/packages/ludic.render3d/gltf.ludic @@ -19,7 +19,7 @@ property Model { height: float = 0.0, # float bits: y extent above ymin ymin: float = 0.0, tris: int = 0, - skin: Skin # the skeleton, for a skinned node (skin.ludic); null for a rigid model + @owns(Skin) skin: Skin # the skeleton, for a skinned node (skin.ludic); null for a rigid model } diff --git a/packages/ludic.render3d/skin.ludic b/packages/ludic.render3d/skin.ludic index 03175ca2..da91ee1d 100644 --- a/packages/ludic.render3d/skin.ludic +++ b/packages/ludic.render3d/skin.ludic @@ -73,6 +73,7 @@ function skin_attribs(render3d_st: mut Render3dState, m: Mesh, attrs: Val) -> bo # the skin `idx` of the document being loaded (gltf_load holds gltf_doc / gltf_bin open) @alloc_ok("loading a model, a texture or a font: a load, not a frame (a guest loading a teammate's look is one)") +@creates(Skin) function skin_load(render3d_st: mut Render3dState, idx: int) -> Skin { let sk = new Skin let nodes = value_get(render3d_st.gltf_doc, "nodes") @@ -233,6 +234,7 @@ function skin_clone(src: Skin) -> Skin { # A skin loaded with a model, let go with it (model_release): every array it owns, its joints' names # and itself. The views into gmat, inv_bind and bones are slices of those, and freeing a view would # free its owner's data again, so only the lists that hold them go (a few bytes a joint stay). +@releases(Skin) function skin_free(sk: Skin) -> void { if sk == null { return } if sk.names != null {