feat(render3d): 23.4 - model_release, shared textures counted, and scatter layers sized to what they hold
- model_release(model) frees each primitive's mesh, and each texture once no other model uses it: the glTF texture cache counts the primitives using each texture (a path loaded again, and a LOD chain borrowing its LOD0's material, each take a reference); the last one frees the texture and forgets it along with any remembered material naming it, so a later load is a fresh one - a scatter layer's instance and sort arrays start at 256 and double as layer_add or a stream fills them, to the layer's cap, instead of the whole cap up front (0.4 GB in Maroon Lake) - gvk_tex_read's copy struct comes from the scratch ring - examples/rendering/release.ludic holds it (RELEASE OK on Vulkan and OpenGL), in the suite smooth renders pixel-identical before and after (max |d| 0). ludic-dev test 306/306. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
70a92a8862
commit
e634177ca4
10 changed files with 189 additions and 7 deletions
|
|
@ -99,6 +99,7 @@ export state Render3dState {
|
|||
gltf_comps: int = 0
|
||||
gltf_tex_paths: []pointer = null
|
||||
gltf_tex_ids: words = null
|
||||
gltf_tex_refs: words = null # how many primitives use each cached texture (model_release)
|
||||
gltf_tex_n: int = 0
|
||||
gltf_white: int = 0
|
||||
gltf_flat: int = 0
|
||||
|
|
|
|||
|
|
@ -50,17 +50,17 @@ function gltf_mat_remember(render3d_st: mut Render3dState, name: string, diff: i
|
|||
}
|
||||
|
||||
function gltf_texture(render3d_st: mut Render3dState, uri: string, srgb: bool) -> int {
|
||||
if render3d_st.gltf_tex_paths == null { render3d_st.gltf_tex_paths = new []pointer; render3d_st.gltf_tex_ids = words(256) }
|
||||
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" }
|
||||
let path = render3d_st.gltf_dir + "/" + png
|
||||
var i = 0
|
||||
while i < render3d_st.gltf_tex_n { if render3d_st.gltf_tex_paths[i] == path { return render3d_st.gltf_tex_ids[i] }; i += 1 }
|
||||
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 }
|
||||
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_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 }
|
||||
return id
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +150,10 @@ function gltf_prim(render3d_st: mut Render3dState, p: Val) -> Prim {
|
|||
if ud != null { gltf_mat_remember(render3d_st, mname, pr.diff, pr.nrm, pr.arm) }
|
||||
else {
|
||||
let k = gltf_mat_find(render3d_st, mname)
|
||||
if k >= 0 { pr.diff = render3d_st.gltf_mat_diff[k]; pr.nrm = render3d_st.gltf_mat_nrm[k]; pr.arm = render3d_st.gltf_mat_arm[k] }
|
||||
if k >= 0 {
|
||||
pr.diff = render3d_st.gltf_mat_diff[k]; pr.nrm = render3d_st.gltf_mat_nrm[k]; pr.arm = render3d_st.gltf_mat_arm[k]
|
||||
gltf_tex_ref(render3d_st, pr.diff); gltf_tex_ref(render3d_st, pr.nrm); gltf_tex_ref(render3d_st, pr.arm)
|
||||
}
|
||||
else { print(`gltf: material {mname} has no textures and none were loaded before it`) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
70
packages/ludic.render3d/gltf_release.ludic
Normal file
70
packages/ludic.render3d/gltf_release.ludic
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# gltf_release.ludic - a loaded model let go of: its meshes, and each texture once nothing else uses
|
||||
# it. The glTF loader shares textures between models (a path loaded once, a LOD chain borrowing its
|
||||
# LOD0's material), so each cached texture carries a count of the primitives using it.
|
||||
|
||||
# one more primitive uses the cached texture `id` (0, or a texture the cache did not load: nothing)
|
||||
function gltf_tex_ref(render3d_st: mut Render3dState, id: int) -> void {
|
||||
if id == 0 or render3d_st.gltf_tex_refs == null { return }
|
||||
for i in 0 .. render3d_st.gltf_tex_n { if render3d_st.gltf_tex_ids[i] == id { render3d_st.gltf_tex_refs[i] += 1; return } }
|
||||
}
|
||||
|
||||
# one primitive fewer: the last one frees the texture and forgets it, with every material naming it
|
||||
function gltf_tex_unref(render3d_st: mut Render3dState, id: int) -> void {
|
||||
if id == 0 or render3d_st.gltf_tex_refs == null { return }
|
||||
for i in 0 .. render3d_st.gltf_tex_n {
|
||||
if render3d_st.gltf_tex_ids[i] == id {
|
||||
render3d_st.gltf_tex_refs[i] -= 1
|
||||
if render3d_st.gltf_tex_refs[i] > 0 { return }
|
||||
gpu_tex_free(render3d_st, id)
|
||||
gltf_tex_forget(render3d_st, i)
|
||||
gltf_mats_forget(render3d_st, id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# the cache entry i goes; the last entry takes its place
|
||||
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
|
||||
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]
|
||||
List.pop(paths)
|
||||
render3d_st.gltf_tex_n = last
|
||||
}
|
||||
|
||||
# a remembered material whose texture was freed is forgotten, so no later load borrows it
|
||||
function gltf_mats_forget(render3d_st: mut Render3dState, id: int) -> void {
|
||||
if render3d_st.gltf_mat_names == null { return }
|
||||
var k = 0
|
||||
while k < render3d_st.gltf_mat_n {
|
||||
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
|
||||
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]
|
||||
render3d_st.gltf_mat_arm[k] = render3d_st.gltf_mat_arm[last]
|
||||
List.pop(names)
|
||||
render3d_st.gltf_mat_n = last
|
||||
} else {
|
||||
k += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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).
|
||||
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
|
||||
}
|
||||
let prims = m.prims
|
||||
while len(prims) > 0 { List.pop(prims) }
|
||||
}
|
||||
|
|
@ -409,7 +409,7 @@ function gvk_tex_read(render3d_st: mut Render3dState, tex: int, ifmt: int, w: in
|
|||
let depth = gvk_is_depth(ifmt)
|
||||
let cb = gvk_once_begin(render3d_st)
|
||||
gvk_barrier(render3d_st, cb, image, depth, 0, 1, 1, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL)
|
||||
let bic = bytes(VkBufferImageCopy_sizeof)
|
||||
let bic = gvk_tmp(render3d_st, VkBufferImageCopy_sizeof)
|
||||
Vk.zero(bic, VkBufferImageCopy_sizeof)
|
||||
if depth { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_DEPTH_BIT) }
|
||||
else { Vk.put_i32(bic, VkBufferImageCopy_imageSubresource + VkImageSubresourceLayers_aspectMask, VK_IMAGE_ASPECT_COLOR_BIT) }
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import "shadow.ludic"
|
|||
import "post.ludic"
|
||||
import "quat.ludic"
|
||||
import "gltf.ludic"
|
||||
import "gltf_release.ludic"
|
||||
import "skin.ludic"
|
||||
import "scatter.ludic"
|
||||
import "actor.ludic"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ property Layer {
|
|||
inst: floats, # INST_FLOATS per instance
|
||||
count: int = 0,
|
||||
cap: int = 0,
|
||||
have: int = 0, # instances inst and scratch have room for now: grown toward cap as filled
|
||||
near: float = 0.0, # float bits; instances beyond it draw as impostors (or not at all)
|
||||
cull: float = 0.0, # float bits; instances beyond it are skipped (0 = never)
|
||||
buf: int = 0,
|
||||
|
|
@ -386,8 +387,11 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia
|
|||
l.near = near
|
||||
l.cull = cull
|
||||
l.tint = v3_new(1.0, 1.0, 1.0)
|
||||
l.inst = floats(cap * INST_FLOATS)
|
||||
l.scratch = floats(cap * INST_FLOATS)
|
||||
# room for what the layer holds, not for what it might: 45 layers at full capacity up front were
|
||||
# 0.4 GB, most of it never filled (plan 23 of maroon-lake)
|
||||
l.have = min(cap, 256)
|
||||
l.inst = floats(l.have * INST_FLOATS)
|
||||
l.scratch = floats(l.have * INST_FLOATS)
|
||||
l.last_cam = v3_new(100000.0, 0.0, 0.0)
|
||||
l.buf = gpu_buffer_new(render3d_st)
|
||||
l.imp_buf = gpu_buffer_new(render3d_st)
|
||||
|
|
@ -398,8 +402,23 @@ function layer_new(render3d_st: mut Render3dState, model: Model, cap: int, folia
|
|||
return l
|
||||
}
|
||||
|
||||
# room for n instances (at most the layer's cap), doubling what there is so a fill costs a few copies
|
||||
function layer_room(l: Layer, n: int) -> void {
|
||||
if n <= l.have { return }
|
||||
var want = max(l.have * 2, n)
|
||||
if want > l.cap { want = l.cap }
|
||||
let inst = floats(want * INST_FLOATS)
|
||||
if l.count > 0 { mem_copy(data_of(inst), data_of(l.inst), l.count * INST_FLOATS * 4) }
|
||||
free(l.inst)
|
||||
free(l.scratch)
|
||||
l.inst = inst
|
||||
l.scratch = floats(want * INST_FLOATS)
|
||||
l.have = want
|
||||
}
|
||||
|
||||
function layer_add(l: Layer, x: float, y: float, z: float, scale: float, yaw: float, seed: float, wind: float) -> void {
|
||||
if l.count >= l.cap { return }
|
||||
layer_room(l, l.count + 1)
|
||||
let o = l.count * INST_FLOATS
|
||||
l.inst[o] = x; l.inst[o + 1] = y; l.inst[o + 2] = z; l.inst[o + 3] = scale
|
||||
l.inst[o + 4] = Math.sin(yaw); l.inst[o + 5] = Math.cos(yaw); l.inst[o + 6] = seed; l.inst[o + 7] = wind
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ function stream_update(render3d_st: mut Render3dState, s: Stream, cam_x: float,
|
|||
}
|
||||
if c != null and c.count > 0 and l.count + c.count <= l.cap and stream_chunk_visible(render3d_st, s, cx, cz, c) {
|
||||
let tg = gl_now_us()
|
||||
layer_room(l, l.count + c.count)
|
||||
mem_copy(mem_off(l.inst, l.count * INST_FLOATS * 4), data_of(c.data), c.count * INST_FLOATS * 4)
|
||||
l.count += c.count
|
||||
render3d_st.stream_us_gather = render3d_st.stream_us_gather + (gl_now_us() - tg)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue