Merge branch 'r3d/recycle' into lang/uifree

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 12:01:33 +03:00
commit db3fa68052
14 changed files with 208 additions and 39 deletions

View file

@ -0,0 +1,7 @@
bump: minor
type: fix
**A handle, a model and an actor let go come round again.** A texture or buffer freed gives its id
back and the next one made takes it, so the per-handle lists stop growing; `model_release` frees a
glTF model whole (its primitives, meshes, names, skeleton) and takes it out of `gltf_cached`'s
cache; and `actor_release` takes an actor off the stage and keeps its record for the next
`actor_new`. `examples/rendering/steady.ludic` holds 2000 actors placed and released at 0 bytes.

View file

@ -44,7 +44,6 @@ program Release {
let diff = a.prims[0].diff
ok = check("the plates share their textures", diff != 0 and b.prims[0].diff == diff and render3d_st.gltf_tex_n == 2) and ok
model_release(render3d_st, a)
ok = check("the first plate is emptied", len(a.prims) == 0) and ok
ok = check("what the second still uses stays", tex_live(render3d_st, diff) and render3d_st.gltf_tex_n == 2) and ok
model_release(render3d_st, b)
ok = check("the last user frees the textures", not tex_live(render3d_st, diff) and render3d_st.gltf_tex_n == 0) and ok

View file

@ -43,6 +43,18 @@ program Steady {
return Vk.heap_bytes() - 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 = Vk.heap_bytes()
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 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()
@ -71,10 +83,13 @@ program Steady {
let grew_f = frame_rounds(render3d_st, 600)
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)
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`)
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 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") }
@ -92,6 +107,10 @@ program Steady {
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 ok { print("STEADY OK") } else { print("STEADY FAILED") }
quit()
}

View file

@ -86,6 +86,7 @@ function read_one(anim_st: AnimState, render3d_st: mut Render3dState, a: Val) ->
c.ckeys[k] = nk
c.coff[k] = at
for i in 0 .. nk { c.times[at + i] = tb[i] }
free(tb)
if nk > 0 and c.times[at + nk - 1] > c.dur { c.dur = c.times[at + nk - 1] }
at += nk
let vb = gltf_accessor_floats(render3d_st, value_as_int(value_get(sp, "output")))
@ -93,6 +94,7 @@ function read_one(anim_st: AnimState, render3d_st: mut Render3dState, a: Val) ->
if c.cpath[k] == AC_ROT { comps = 4 }
c.cvoff[k] = av
for i in 0 .. nk * comps { c.vals[av + i] = vb[i] }
free(vb)
av += nk * comps
k += 1
}

View file

@ -122,12 +122,10 @@ function actor_hide_part(a: Actor, name: string, hidden: bool) -> void {
}
function actor_new(render3d_st: mut Render3dState, model: Model) -> Actor {
let a = new Actor
let a = actor_fresh(render3d_st)
a.model = model
a.scale = 1.0
a.tint = v3_new(1.0, 1.0, 1.0)
a.rough = 1.0
a.mat = m4_new()
render3d_st.ac_next_id += 1; a.id = render3d_st.ac_next_id
if model != null { a.radius = Math.max(model.radius, model.height) + 1.0 }
a.cull = 450.0
@ -417,3 +415,36 @@ function actor_clear_all(render3d_st: mut Render3dState) -> void {
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.
function actor_fresh(render3d_st: mut Render3dState) -> Actor {
let sp = render3d_st.ac_spare
if sp == null or len(sp) == 0 {
let a = new Actor
a.tint = v3_new(1.0, 1.0, 1.0)
a.mat = m4_new()
return a
}
let a = List.pop(sp)
a.x = 0.0; a.y = 0.0; a.z = 0.0; a.yaw = 0.0; a.scale = 0.0; a.rough = 0.0
a.tint[0] = 1.0; a.tint[1] = 1.0; a.tint[2] = 1.0
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
return a
}
# Off the stage for good, and its record kept for the next actor_new: whoever released it never
# touches it again. What it was lent stays its lender's (a skin clone, a rim colour); what it made
# for itself (a part's tint, a part hidden) goes.
function actor_release(render3d_st: mut Render3dState, a: Actor) -> void {
if a == null or a.id == 0 { return } # released already
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 }
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)
}

View file

@ -13,6 +13,7 @@ export state Render3dState {
ac_out_cut: AcProg = null
ac_actors: []Actor = null
ac_next_id: int = 0
ac_spare: []Actor = null # released actors, taken again by actor_new (actor_release)
ac_frame: int = 0
ac_oq: []OutlineReq = null
ac_oq_n: int = 0 # live entries; the array is kept and reused
@ -211,6 +212,8 @@ export state Render3dState {
gvk_lay_flat: []int = null # every layout known, end to end; gvk_lay_off / _len find each
gvk_lay_off: []int = null
gvk_lay_len: []int = null
gvk_tex_spare: []int = null # ids of freed textures, handed out again by gvk_tex_new
gvk_buf_spare: []int = null # the same for buffers (gvk_buf_delete)
gvk_has_bc: bool = false # textureCompressionBC: .dds textures are uploaded compressed
# R3D_VK_PROF: Vulkan objects made and destroyed, reported every 120 frames
gvk_mk_img: int = 0
@ -805,7 +808,6 @@ export state Render3dState {
tex_depth: int = 0 # bits per sample (8 or 16)
tex_file_len: int = 0
tex_anisotropy: fixed = 16.0
tex_size_ids: []int = new []int
tex_size_ws: []int = new []int
tex_size_hs: []int = new []int
hdr_max_lum: float = 0.0 # float bits of the brightest texel (sun finding)

View file

@ -45,7 +45,9 @@ function gltf_mat_find(render3d_st: Render3dState, name: string) -> int {
function gltf_mat_remember(render3d_st: mut Render3dState, name: string, diff: int, nrm: int, arm: int) -> void {
if render3d_st.gltf_mat_names == null { render3d_st.gltf_mat_names = new []string; render3d_st.gltf_mat_diff = words(256); render3d_st.gltf_mat_nrm = words(256); render3d_st.gltf_mat_arm = words(256) }
if gltf_mat_find(render3d_st, name) >= 0 or render3d_st.gltf_mat_n >= 256 { return }
push(render3d_st.gltf_mat_names, name); render3d_st.gltf_mat_diff[render3d_st.gltf_mat_n] = diff; render3d_st.gltf_mat_nrm[render3d_st.gltf_mat_n] = nrm; render3d_st.gltf_mat_arm[render3d_st.gltf_mat_n] = arm
# its own copy of the name: a primitive frees its own with it (model_release)
push(render3d_st.gltf_mat_names, name[0 .. len(name)])
render3d_st.gltf_mat_diff[render3d_st.gltf_mat_n] = diff; render3d_st.gltf_mat_nrm[render3d_st.gltf_mat_n] = nrm; render3d_st.gltf_mat_arm[render3d_st.gltf_mat_n] = arm
render3d_st.gltf_mat_n += 1
}
@ -53,14 +55,23 @@ function gltf_texture(render3d_st: mut Render3dState, uri: string, srgb: bool) -
if render3d_st.gltf_tex_paths == null { render3d_st.gltf_tex_paths = new []pointer; render3d_st.gltf_tex_ids = words(256); render3d_st.gltf_tex_refs = words(256) }
var png: string = uri
let n = len(uri)
if n > 4 and uri[n - 4] == '.' and uri[n - 3] == 'j' { png = uri[0 .. n - 4] + ".png" }
var made = false
if n > 4 and uri[n - 4] == '.' and uri[n - 3] == 'j' { png = uri[0 .. n - 4] + ".png"; made = true }
let path = render3d_st.gltf_dir + "/" + png
if made { free(png) }
var i = 0
while i < render3d_st.gltf_tex_n { if render3d_st.gltf_tex_paths[i] == path { render3d_st.gltf_tex_refs[i] += 1; return render3d_st.gltf_tex_ids[i] }; i += 1 }
while i < render3d_st.gltf_tex_n {
if render3d_st.gltf_tex_paths[i] == path {
free(path) # the cache holds its own
render3d_st.gltf_tex_refs[i] += 1
return render3d_st.gltf_tex_ids[i]
}
i += 1
}
var dil = 0
if render3d_st.gltf_cutout { dil = 24 }
let id = tex_load_ex(render3d_st, path, srgb, dil)
if render3d_st.gltf_tex_n < 256 { push(render3d_st.gltf_tex_paths, path); render3d_st.gltf_tex_ids[render3d_st.gltf_tex_n] = id; render3d_st.gltf_tex_refs[render3d_st.gltf_tex_n] = 1; render3d_st.gltf_tex_n += 1 }
if render3d_st.gltf_tex_n < 256 { push(render3d_st.gltf_tex_paths, path); render3d_st.gltf_tex_ids[render3d_st.gltf_tex_n] = id; render3d_st.gltf_tex_refs[render3d_st.gltf_tex_n] = 1; render3d_st.gltf_tex_n += 1 } else { free(path) }
return id
}
@ -165,7 +176,9 @@ function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim {
# Load the mesh of the node called `node_name` from dir/file.
function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model {
render3d_st.gltf_dir = dir
let text = Fs.read_text(dir + "/" + file)
let at = dir + "/" + file
let text = Fs.read_text(at)
free(at)
if text == null { print(`gltf: cannot read {dir}/{file}`); return null }
# the last file's tree goes first (it is read only until the next load: ludic.anim's read.ludic),
# and the text once parsed - nothing gives memory back on its own (plan 23 of maroon-lake)
@ -174,7 +187,9 @@ function gltf_load(render3d_st: mut Render3dState, dir: string, file: string, no
free(text)
let buffers = value_get(render3d_st.gltf_doc, "buffers")
let bin_uri = value_as_str(value_get(value_at(buffers, 0), "uri"))
render3d_st.gltf_bin = file_open(dir + "/" + bin_uri, "rb")
let bin_at = dir + "/" + bin_uri
render3d_st.gltf_bin = file_open(bin_at, "rb")
free(bin_at)
if render3d_st.gltf_bin == null { print(`gltf: cannot open {bin_uri}`); return null }
let nodes = value_get(render3d_st.gltf_doc, "nodes")
var mesh_idx = -1

View file

@ -2,7 +2,7 @@
# model the first load made, found by comparing the names in place, so asking allocates nothing. For
# props a game places while it plays (a fish, a sign an animal leaves); a model whose document is
# read after the load (ludic.anim's clips) still goes through gltf_load. A model loaded here is shared
# by every actor drawing it, and is never released.
# by every actor drawing it; model_release takes it out of the cache as it lets it go.
export function gltf_cached(render3d_st: mut Render3dState, dir: string, file: string, node_name: string) -> Model {
if render3d_st.gltf_kept == null {
render3d_st.gltf_kept = new []Model
@ -21,6 +21,23 @@ export function gltf_cached(render3d_st: mut Render3dState, dir: string, file: s
return m
}
# a model let go (model_release) leaves the cache, so the next ask loads it afresh
function gltf_cached_forget(render3d_st: mut Render3dState, m: Model) -> void {
if render3d_st.gltf_kept == null { return }
let kept = render3d_st.gltf_kept
let dirs = render3d_st.gltf_kept_dir
let files = render3d_st.gltf_kept_file
let nodes = render3d_st.gltf_kept_node
for i in 0 .. len(kept) {
if kept[i] == m {
let last = len(kept) - 1
kept[i] = kept[last]; dirs[i] = dirs[last]; files[i] = files[last]; nodes[i] = nodes[last]
List.pop(kept); List.pop(dirs); List.pop(files); List.pop(nodes)
return
}
}
}
export function gltf_cached_count(render3d_st: Render3dState) -> int {
if render3d_st.gltf_kept == null { return 0 }
return len(render3d_st.gltf_kept)

View file

@ -27,6 +27,7 @@ function gltf_tex_unref(render3d_st: mut Render3dState, id: int) -> void {
function gltf_tex_forget(render3d_st: mut Render3dState, i: int) -> void {
let last = render3d_st.gltf_tex_n - 1
let paths = render3d_st.gltf_tex_paths
free(paths[i])
paths[i] = paths[last]
render3d_st.gltf_tex_ids[i] = render3d_st.gltf_tex_ids[last]
render3d_st.gltf_tex_refs[i] = render3d_st.gltf_tex_refs[last]
@ -42,6 +43,7 @@ function gltf_mats_forget(render3d_st: mut Render3dState, id: int) -> void {
if render3d_st.gltf_mat_diff[k] == id or render3d_st.gltf_mat_nrm[k] == id or render3d_st.gltf_mat_arm[k] == id {
let last = render3d_st.gltf_mat_n - 1
let names = render3d_st.gltf_mat_names
free(names[k])
names[k] = names[last]
render3d_st.gltf_mat_diff[k] = render3d_st.gltf_mat_diff[last]
render3d_st.gltf_mat_nrm[k] = render3d_st.gltf_mat_nrm[last]
@ -54,17 +56,31 @@ function gltf_mats_forget(render3d_st: mut Render3dState, id: int) -> void {
}
}
# Let a model go: every primitive's mesh, and its textures once no other model uses them. The
# caller says when - no actor may draw the model afterwards (a released model's prims are emptied,
# so a stale draw of it draws nothing).
# Let a model go: every primitive's mesh, and its textures once no other model uses it; the
# primitives, their meshes and the model itself are freed, and a model gltf_cached handed out
# leaves its cache. The caller says when: nothing may draw the model, or hold it, afterwards. For
# a model gltf_load or gltf_cached made (a procedural one - a card, a blade - owns its parts otherwise).
function model_release(render3d_st: mut Render3dState, m: Model) -> void {
if m == null or m.prims == null { return }
for i in 0 .. len(m.prims) {
let pr = m.prims[i]
if pr.mesh != null { gpu_mesh_free(render3d_st, pr.mesh); pr.mesh = null }
gltf_tex_unref(render3d_st, pr.diff); gltf_tex_unref(render3d_st, pr.nrm); gltf_tex_unref(render3d_st, pr.arm)
pr.diff = 0; pr.nrm = 0; pr.arm = 0
if m == null { return }
gltf_cached_forget(render3d_st, m)
if m.prims != null {
for i in 0 .. len(m.prims) {
let pr = m.prims[i]
gltf_tex_unref(render3d_st, pr.diff); gltf_tex_unref(render3d_st, pr.nrm); gltf_tex_unref(render3d_st, pr.arm)
if pr.mesh != null { mesh_release(render3d_st, pr.mesh) }
if pr.name != null { free(pr.name) }
free(pr)
}
free(m.prims)
}
let prims = m.prims
while len(prims) > 0 { List.pop(prims) }
skin_free(m.skin)
free(m)
}
# a mesh and what it holds: its buffers (their handles given back), its layout and buffer lists
function mesh_release(render3d_st: mut Render3dState, me: Mesh) -> void {
gpu_mesh_free(render3d_st, me)
if me.attrs != null { free(me.attrs) }
if me.vbufs != null { free(me.vbufs) }
free(me)
}

View file

@ -599,7 +599,9 @@ function gpu_tex_free(render3d_st: mut Render3dState, tex: int) -> void {
if tex == 0 { return }
let ids = gpu_tmp(render3d_st)
ids[0] = tex
gvk_flush(render3d_st); if tex < len(render3d_st.gvk_tex_image) { gvk_tex_release(render3d_st, tex) }
gvk_flush(render3d_st)
gvk_tex_give_back(render3d_st, tex)
tex_note_forget(render3d_st, tex)
if render3d_st.gpu_unit_2d != null { for i in 0 .. 32 { if render3d_st.gpu_unit_2d[i] == tex { render3d_st.gpu_unit_2d[i] = 0 } } }
if render3d_st.gpu_bound_2d == tex { render3d_st.gpu_bound_2d = 0 }
let o = gpu_tx_at(render3d_st, tex)
@ -699,7 +701,8 @@ function gpu_fb_free(render3d_st: mut Render3dState, fb: int) -> void {
gvk_fb_forget(render3d_st, fb); let fo = gpu_fb_at(render3d_st, fb); if fo >= 0 { for i in 0 .. GPU_FB_W { render3d_st.gpu_fb[fo + i] = 0 } }
}
function gpu_rb_free(render3d_st: mut Render3dState, rb: int) -> void {
gvk_flush(render3d_st); if rb > 0 and rb < len(render3d_st.gvk_tex_image) { gvk_tex_release(render3d_st, rb) }
gvk_flush(render3d_st)
gvk_tex_give_back(render3d_st, rb)
}
function gpu_viewport(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void { gvk_viewport(render3d_st, x, y, w, h) }

View file

@ -1460,10 +1460,10 @@ function gvk_program_new(render3d_st: mut Render3dState, vs: string, fs: string,
function gvk_mesh_free(render3d_st: mut Render3dState, m: Mesh) -> void {
if m == null { return }
if m.vbufs != null { for i in 0 .. m.n_vbufs { if m.vbufs[i] > 0 { gvk_buf_release(render3d_st, m.vbufs[i]) } } }
if m.vbufs != null { for i in 0 .. m.n_vbufs { if m.vbufs[i] > 0 { gvk_buf_delete(render3d_st, m.vbufs[i]) } } }
m.n_vbufs = 0
m.vbo = 0
if m.ebo > 0 { gvk_buf_release(render3d_st, m.ebo); m.ebo = 0 }
if m.ebo > 0 { gvk_buf_delete(render3d_st, m.ebo); m.ebo = 0 }
}
function gvk_scissor(render3d_st: mut Render3dState, x: int, y: int, w: int, h: int) -> void {

View file

@ -57,6 +57,14 @@ function gvk_channel_bytes(ifmt: int) -> int {
# renderer asks for mipmaps after the upload; an image made without pixels is a target and gets
# one level. Every level sits in SHADER_READ_ONLY_OPTIMAL between uses.
# a texture freed for good: its image goes, and its id is handed out again by gvk_tex_new - once, and
# only if it held an image, so a texture freed twice cannot give two new ones the same id
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)
}
function gvk_tex_new(render3d_st: mut Render3dState) -> int {
let zero: long = 0
if render3d_st.gvk_tex_image == null {
@ -73,6 +81,18 @@ function gvk_tex_new(render3d_st: mut Render3dState) -> int {
push(render3d_st.gvk_tex_image, zero); push(render3d_st.gvk_tex_view, zero); push(render3d_st.gvk_tex_mem, zero)
push(render3d_st.gvk_tex_levels, 0); push(render3d_st.gvk_tex_layers, 0); push(render3d_st.gvk_tex_vkfmt, 0)
}
# an id a freed texture gave back first: its generation goes on counting, so every cache keyed by
# (id, generation) - views, sets, samplers - tells the new texture from the old
if render3d_st.gvk_tex_spare != null and len(render3d_st.gvk_tex_spare) > 0 {
let sp = render3d_st.gvk_tex_spare
let t = sp[len(sp) - 1]
List.pop(sp)
render3d_st.gvk_tex_image[t] = zero; render3d_st.gvk_tex_view[t] = zero; render3d_st.gvk_tex_mem[t] = zero
render3d_st.gvk_tex_levels[t] = 0; render3d_st.gvk_tex_layers[t] = 0; render3d_st.gvk_tex_vkfmt[t] = 0
render3d_st.gvk_tex_dims_w[t] = 0; render3d_st.gvk_tex_dims_h[t] = 0; render3d_st.gvk_tex_glfmt[t] = 0; render3d_st.gvk_tex_array[t] = 0
render3d_st.gvk_tex_smp_sig[t] = 0; render3d_st.gvk_tex_smp[t] = zero; render3d_st.gvk_tex_samples[t] = 0
return t
}
push(render3d_st.gvk_tex_image, zero); push(render3d_st.gvk_tex_view, zero); push(render3d_st.gvk_tex_mem, zero)
push(render3d_st.gvk_tex_levels, 0); push(render3d_st.gvk_tex_layers, 0); push(render3d_st.gvk_tex_vkfmt, 0)
push(render3d_st.gvk_tex_dims_w, 0); push(render3d_st.gvk_tex_dims_h, 0)
@ -605,10 +625,29 @@ function gvk_buf_new(render3d_st: mut Render3dState) -> int {
# handle 0 is "no buffer", as it is on OpenGL
push(render3d_st.gvk_buf, zero); push(render3d_st.gvk_buf_mem, zero); push(render3d_st.gvk_buf_size, 0); push(render3d_st.gvk_buf_map, null); push(render3d_st.gvk_buf_used, 0)
}
# a handle a deleted buffer gave back first (gvk_buf_delete)
if render3d_st.gvk_buf_spare != null and len(render3d_st.gvk_buf_spare) > 0 {
let sp = render3d_st.gvk_buf_spare
let b = sp[len(sp) - 1]
List.pop(sp)
return b
}
push(render3d_st.gvk_buf, zero); push(render3d_st.gvk_buf_mem, zero); push(render3d_st.gvk_buf_size, 0); push(render3d_st.gvk_buf_map, null); push(render3d_st.gvk_buf_used, 0)
return len(render3d_st.gvk_buf) - 1
}
# 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.
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)
}
function gvk_buf_release(render3d_st: mut Render3dState, b: int) -> void {
if render3d_st.gvk_buf[b] == 0 { return }
let zero: long = 0

View file

@ -227,3 +227,21 @@ function skin_clone(src: Skin) -> Skin {
skin_pose(sk)
return sk
}
# 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).
function skin_free(sk: Skin) -> void {
if sk == null { return }
if sk.names != null {
for i in 0 .. len(sk.names) { if sk.names[i] != null and len(sk.names[i]) > 0 { free(sk.names[i]) } }
free(sk.names)
}
free(sk.par); free(sk.walk); free(sk.rest_t); free(sk.rest_r); free(sk.rest_s); free(sk.rest_g)
free(sk.pose_r); free(sk.pose_t); free(sk.gmat); free(sk.joints); free(sk.inv_bind); free(sk.bones)
free(sk.tmp_l); free(sk.tmp_q); free(sk.tmp_a); free(sk.tmp_b); free(sk.tmp_c); free(sk.tmp_v)
if sk.gmat_v != null { free(sk.gmat_v) }
if sk.inv_v != null { free(sk.inv_v) }
if sk.bones_v != null { free(sk.bones_v) }
free(sk)
}

View file

@ -284,22 +284,23 @@ function tex_load_ex(render3d_st: mut Render3dState, path: pointer, srgb: bool,
return id
}
# the size each loaded texture was, by id: a nine-slice or a UI image asks
# a texture's size, by its id: a slot per id (ids are reused), not a list that grows by every load
function tex_note_size(render3d_st: mut Render3dState, id: int) -> void {
push(render3d_st.tex_size_ids, id)
push(render3d_st.tex_size_ws, render3d_st.tex_w)
push(render3d_st.tex_size_hs, render3d_st.tex_h)
if id <= 0 { return }
while len(render3d_st.tex_size_ws) <= id { push(render3d_st.tex_size_ws, 0); push(render3d_st.tex_size_hs, 0) }
render3d_st.tex_size_ws[id] = render3d_st.tex_w
render3d_st.tex_size_hs[id] = render3d_st.tex_h
}
function tex_note_forget(render3d_st: mut Render3dState, id: int) -> void {
if id > 0 and id < len(render3d_st.tex_size_ws) { render3d_st.tex_size_ws[id] = 0; render3d_st.tex_size_hs[id] = 0 }
}
function tex_width(render3d_st: Render3dState, id: int) -> int {
for i in 0 .. len(render3d_st.tex_size_ids) {
if render3d_st.tex_size_ids[i] == id { return render3d_st.tex_size_ws[i] }
}
return 0
if id <= 0 or id >= len(render3d_st.tex_size_ws) { return 0 }
return render3d_st.tex_size_ws[id]
}
function tex_height(render3d_st: Render3dState, id: int) -> int {
for i in 0 .. len(render3d_st.tex_size_ids) {
if render3d_st.tex_size_ids[i] == id { return render3d_st.tex_size_hs[i] }
}
return 0
if id <= 0 or id >= len(render3d_st.tex_size_hs) { return 0 }
return render3d_st.tex_size_hs[id]
}
# A small solid-colour fallback texture (linear rgb 0..255), for missing maps.