render3d: a fog wall nearer than a layer's cards lets its impostor atlases go, and a fog that opens bakes them again
Inside the wall every instance is a mesh whose LOD2 casts its shadow, so a card is neither drawn nor cast there: fog_impostors.ludic frees a layer's atlases while wall + margin < its near (trees 420-480 m, rocks 320 m) and paints them again from the model when it opens past it (impostor_paint, split out of the bake). Impostor atlases get memory of their own, so a release goes back to the driver instead of leaving a hole in a shared block. R3D_FOG_AT=<frame> with R3D_FOG_TO=<m> changes the wall mid-run. At the overlook: 2102 MB off, 1824 MB at 30 m (249 -> 67 MB of atlases; the rest is the ground cover's card atlases, drawn inside the wall); opening re-bakes 16 layers in 42-46 ms, back to 2102 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6289564382
commit
7024f42789
6 changed files with 88 additions and 7 deletions
|
|
@ -199,6 +199,11 @@ export state Render3dState {
|
||||||
gvk_al_tag: []int = null # per allocation id: owner * 4 + image * 2 + host (vkmem_report.ludic)
|
gvk_al_tag: []int = null # per allocation id: owner * 4 + image * 2 + host (vkmem_report.ludic)
|
||||||
gvk_tag: int = 0 # the owner allocations are made for now (VKM_*)
|
gvk_tag: int = 0 # the owner allocations are made for now (VKM_*)
|
||||||
vkmem_at: int = 0 # R3D_VKMEM=<frame>: the report is printed at that frame
|
vkmem_at: int = 0 # R3D_VKMEM=<frame>: the report is printed at that frame
|
||||||
|
fog_imp_released: int = 0 # impostor atlases let go under a fog wall (fog_impostors.ludic)
|
||||||
|
fog_imp_rebaked: int = 0
|
||||||
|
fog_imp_rebake_us: int = 0
|
||||||
|
fog_at: int = 0 # R3D_FOG_AT=<frame>:<m>, a change of the wall mid-run (a dev switch)
|
||||||
|
fog_at_m: float = 0.0
|
||||||
@max(4096) gvk_al_spare: []int = null # ids to reuse
|
@max(4096) gvk_al_spare: []int = null # ids to reuse
|
||||||
gvk_pool: long = 0
|
gvk_pool: long = 0
|
||||||
gvk_fence: bytes = null
|
gvk_fence: bytes = null
|
||||||
|
|
|
||||||
55
packages/ludic.render3d/fog_impostors.ludic
Normal file
55
packages/ludic.render3d/fog_impostors.ludic
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# fog_impostors.ludic — a layer's impostor atlases are let go while the fog wall is nearer than its
|
||||||
|
# cards begin (the layer's `near`), and baked again when the fog opens past it. Inside the wall every
|
||||||
|
# instance is a mesh, and the meshes' LOD2 cast its shadows, so a card is neither drawn nor cast.
|
||||||
|
|
||||||
|
# does the wall let any of this layer's instances be drawn as a card?
|
||||||
|
function fog_needs_cards(render3d_st: Render3dState, l: Layer) -> bool {
|
||||||
|
let w = render3d_st.r3d_fog_wall
|
||||||
|
if w <= 0.0 or l.near <= 0.0 { return true }
|
||||||
|
return w + R3D_FOG_MARGIN >= l.near
|
||||||
|
}
|
||||||
|
|
||||||
|
# one layer brought in line with the wall: its atlases released, or baked again
|
||||||
|
function fog_impostor(render3d_st: mut Render3dState, l: Layer) -> void {
|
||||||
|
let im = l.imp
|
||||||
|
if im == null or im.model == null { return }
|
||||||
|
let want = fog_needs_cards(render3d_st, l)
|
||||||
|
if not want and im.albedo != 0 {
|
||||||
|
gpu_tex_free(render3d_st, im.albedo); gpu_tex_free(render3d_st, im.normal)
|
||||||
|
im.albedo = 0; im.normal = 0
|
||||||
|
render3d_st.fog_imp_released += 1
|
||||||
|
} else if want and im.albedo == 0 {
|
||||||
|
let t0 = gl_now_us()
|
||||||
|
let was = render3d_st.gvk_tag
|
||||||
|
render3d_st.gvk_tag = VKM_IMPOSTOR
|
||||||
|
impostor_paint(render3d_st, im)
|
||||||
|
render3d_st.gvk_tag = was
|
||||||
|
render3d_st.fog_imp_rebake_us = render3d_st.fog_imp_rebake_us + (gl_now_us() - t0)
|
||||||
|
render3d_st.fog_imp_rebaked += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# every layer, on a change of the wall
|
||||||
|
function fog_impostors(render3d_st: mut Render3dState) -> void {
|
||||||
|
render3d_st.fog_imp_rebake_us = 0
|
||||||
|
for i in 0 .. len(render3d_st.sc_layers) { fog_impostor(render3d_st, render3d_st.sc_layers[i]) }
|
||||||
|
if render3d_st.r3d_debug and render3d_st.fog_imp_rebaked > 0 { fog_say_rebake(render3d_st.fog_imp_rebaked, render3d_st.fog_imp_rebake_us) }
|
||||||
|
}
|
||||||
|
function fog_say_rebake(n: int, us: int) -> void { print(`r3d: fog: {n} impostor atlases baked again in {us / 1000} ms`) }
|
||||||
|
|
||||||
|
# R3D_FOG_AT=<frame> with R3D_FOG_TO=<m>: the wall changed mid-run, as the Settings row does
|
||||||
|
function fog_at_tick(render3d_st: mut Render3dState) -> void {
|
||||||
|
if render3d_st.fog_at == 0 {
|
||||||
|
render3d_st.fog_at = -1
|
||||||
|
if r3d_env_has(render3d_st, "R3D_FOG_AT") and r3d_env_has(render3d_st, "R3D_FOG_TO") {
|
||||||
|
render3d_st.fog_at = Text.to_int(r3d_env(render3d_st, "R3D_FOG_AT"))
|
||||||
|
render3d_st.fog_at_m = float(Text.to_int(r3d_env(render3d_st, "R3D_FOG_TO")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if render3d_st.fog_at > 0 and render3d_st.r3d_test_frame == render3d_st.fog_at {
|
||||||
|
let t0 = gl_now_us()
|
||||||
|
r3d_fog_wall(render3d_st, render3d_st.fog_at_m)
|
||||||
|
fog_say_change(render3d_st.fog_at_m, gl_now_us() - t0, render3d_st.fog_imp_released, render3d_st.fog_imp_rebaked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function fog_say_change(m: float, us: int, rel: int, re: int) -> void { print(`r3d: fog: wall {int(m)} m set in {us / 1000} ms ({rel} atlases let go, {re} baked again so far)`) }
|
||||||
|
|
@ -429,7 +429,9 @@ function gvk_mem_new(render3d_st: mut Render3dState, req: bytes, want: int, imag
|
||||||
# aligned: carved at the bare size, every small buffer left a sliver before the next one that no
|
# aligned: carved at the bare size, every small buffer left a sliver before the next one that no
|
||||||
# later one could use - 3,718 of them among 3,968 buffers, and the free list past its bound
|
# later one could use - 3,718 of them among 3,968 buffers, and the free list past its bound
|
||||||
let span = (size + align - 1) / align * align
|
let span = (size + align - 1) / align * align
|
||||||
if size <= gvk_own_over(render3d_st) {
|
# an impostor atlas has memory of its own: a fog wall lets it go (fog_impostors.ludic), and inside a
|
||||||
|
# shared block its room went back to nobody while the block's other tenants stayed
|
||||||
|
if size <= gvk_own_over(render3d_st) and render3d_st.gvk_tag != VKM_IMPOSTOR {
|
||||||
var kind = t * 2
|
var kind = t * 2
|
||||||
if image { kind += 1 }
|
if image { kind += 1 }
|
||||||
var i = 0
|
var i = 0
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import "gltf_cached.ludic"
|
||||||
import "skin.ludic"
|
import "skin.ludic"
|
||||||
import "scatter.ludic"
|
import "scatter.ludic"
|
||||||
import "fog_casters.ludic"
|
import "fog_casters.ludic"
|
||||||
|
import "fog_impostors.ludic"
|
||||||
import "actor.ludic"
|
import "actor.ludic"
|
||||||
import "stream.ludic"
|
import "stream.ludic"
|
||||||
import "grass.ludic"
|
import "grass.ludic"
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export function r3d_fog_wall(render3d_st: mut Render3dState, dist: float) -> voi
|
||||||
# every layer and stream re-gathers for the new reach
|
# every layer and stream re-gathers for the new reach
|
||||||
render3d_st.sc_view_gen += 1
|
render3d_st.sc_view_gen += 1
|
||||||
cam_update(render3d_st)
|
cam_update(render3d_st)
|
||||||
|
fog_impostors(render3d_st)
|
||||||
}
|
}
|
||||||
# how far anything is drawn: the fog wall and a margin, or `far` (0: no limit of its own) without one
|
# how far anything is drawn: the fog wall and a margin, or `far` (0: no limit of its own) without one
|
||||||
function r3d_reach(render3d_st: Render3dState, far: float) -> float {
|
function r3d_reach(render3d_st: Render3dState, far: float) -> float {
|
||||||
|
|
@ -221,6 +222,7 @@ function r3d_frame__t(render3d_st: mut Render3dState, time: float) -> void {
|
||||||
# a stale attachment or a texture freed twice shows up.
|
# a stale attachment or a texture freed twice shows up.
|
||||||
render3d_st.r3d_test_frame += 1
|
render3d_st.r3d_test_frame += 1
|
||||||
vkmem_tick(render3d_st)
|
vkmem_tick(render3d_st)
|
||||||
|
fog_at_tick(render3d_st)
|
||||||
if render3d_st.r3d_test_resize == 0 and r3d_env_has(render3d_st, "R3D_RESIZE_AT") { render3d_st.r3d_test_resize = Text.to_int(r3d_env(render3d_st, "R3D_RESIZE_AT")) }
|
if render3d_st.r3d_test_resize == 0 and r3d_env_has(render3d_st, "R3D_RESIZE_AT") { render3d_st.r3d_test_resize = Text.to_int(r3d_env(render3d_st, "R3D_RESIZE_AT")) }
|
||||||
# R3D_RESIZE_AT=<n>: from frame n on, rebuild every screen-sized buffer every few
|
# R3D_RESIZE_AT=<n>: from frame n on, rebuild every screen-sized buffer every few
|
||||||
# frames at a different size, as dragging a window edge or entering fullscreen does.
|
# frames at a different size, as dragging a window edge or entering fullscreen does.
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ property Impostor {
|
||||||
normal: int = 0,
|
normal: int = 0,
|
||||||
tiles: int = 16,
|
tiles: int = 16,
|
||||||
radius: float = 0.0,
|
radius: float = 0.0,
|
||||||
height: float = 0.0
|
height: float = 0.0,
|
||||||
|
model: Model, # what it was baked from, to bake it again (fog_impostors.ludic)
|
||||||
|
tw: int = 0,
|
||||||
|
th: int = 0,
|
||||||
|
flower: bool = false
|
||||||
}
|
}
|
||||||
property Layer {
|
property Layer {
|
||||||
model: Model,
|
model: Model,
|
||||||
|
|
@ -323,6 +327,18 @@ function impostor_bake__t(render3d_st: mut Render3dState, model: Model, tiles: i
|
||||||
im.tiles = tiles
|
im.tiles = tiles
|
||||||
im.radius = model.radius * 1.02
|
im.radius = model.radius * 1.02
|
||||||
im.height = model.height
|
im.height = model.height
|
||||||
|
im.model = model; im.tw = tw; im.th = th; im.flower = render3d_st.sc_bake_flower
|
||||||
|
impostor_paint(render3d_st, im)
|
||||||
|
return im
|
||||||
|
}
|
||||||
|
|
||||||
|
# the atlases drawn from the model: at the bake, and again when a fog that let them go opens
|
||||||
|
@alloc_ok("a world being set up (a stream, a layer, water, post, a bake): the map loading or swapping, not a frame")
|
||||||
|
function impostor_paint(render3d_st: mut Render3dState, im: Impostor) -> void {
|
||||||
|
let model = im.model
|
||||||
|
let tiles = im.tiles
|
||||||
|
let tw = im.tw
|
||||||
|
let th = im.th
|
||||||
let aw = tiles * tw
|
let aw = tiles * tw
|
||||||
im.albedo = tex_target(render3d_st, aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
|
im.albedo = tex_target(render3d_st, aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
|
||||||
im.normal = tex_target(render3d_st, aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
|
im.normal = tex_target(render3d_st, aw, th, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR)
|
||||||
|
|
@ -349,7 +365,7 @@ function impostor_bake__t(render3d_st: mut Render3dState, model: Model, tiles: i
|
||||||
let r = im.radius
|
let r = im.radius
|
||||||
let hh = model.height * 0.5
|
let hh = model.height * 0.5
|
||||||
var bake = render3d_st.sc_bake_prog
|
var bake = render3d_st.sc_bake_prog
|
||||||
if render3d_st.sc_bake_flower { bake = render3d_st.sc_bake_flower_prog }
|
if im.flower { bake = render3d_st.sc_bake_flower_prog }
|
||||||
gpu_use_program(render3d_st, bake)
|
gpu_use_program(render3d_st, bake)
|
||||||
for t in 0 .. tiles {
|
for t in 0 .. tiles {
|
||||||
let a = 2.0 * PI * (float(t) / float(tiles))
|
let a = 2.0 * PI * (float(t) / float(tiles))
|
||||||
|
|
@ -387,7 +403,6 @@ function impostor_bake__t(render3d_st: mut Render3dState, model: Model, tiles: i
|
||||||
render3d_st.tex_dump_alpha = true; tex_dump(render3d_st, im.albedo, aw, th, `build/atlas_{render3d_st.sc_dump_n}_alpha.ppm`); render3d_st.tex_dump_alpha = false
|
render3d_st.tex_dump_alpha = true; tex_dump(render3d_st, im.albedo, aw, th, `build/atlas_{render3d_st.sc_dump_n}_alpha.ppm`); render3d_st.tex_dump_alpha = false
|
||||||
tex_dump(render3d_st, im.albedo, aw, th, `build/atlas_{render3d_st.sc_dump_n}_color.ppm`)
|
tex_dump(render3d_st, im.albedo, aw, th, `build/atlas_{render3d_st.sc_dump_n}_color.ppm`)
|
||||||
}
|
}
|
||||||
return im
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Give a layer a LOD chain. `dists` (float bits) are the outer distances of each level;
|
# Give a layer a LOD chain. `dists` (float bits) are the outer distances of each level;
|
||||||
|
|
@ -410,9 +425,10 @@ function layer_set_lods(render3d_st: mut Render3dState, l: Layer, models: []Mode
|
||||||
# mark level k as the layer's crossed card (drawn with the card program and its atlas)
|
# mark level k as the layer's crossed card (drawn with the card program and its atlas)
|
||||||
function layer_lod_card(l: Layer, k: int) -> void { l.lod_card[k] = 1 }
|
function layer_lod_card(l: Layer, k: int) -> void { l.lod_card[k] = 1 }
|
||||||
|
|
||||||
function layer_set_impostor(render3d_st: Render3dState, l: Layer, im: Impostor) -> void {
|
function layer_set_impostor(render3d_st: mut Render3dState, l: Layer, im: Impostor) -> void {
|
||||||
l.imp = im
|
l.imp = im
|
||||||
scatter_attach(render3d_st, render3d_st.sc_card, l.imp_buf)
|
scatter_attach(render3d_st, render3d_st.sc_card, l.imp_buf)
|
||||||
|
fog_impostor(render3d_st, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---- per frame -----------------------------------------------------------------------
|
# ---- per frame -----------------------------------------------------------------------
|
||||||
|
|
@ -952,7 +968,7 @@ function layer_draw_model(render3d_st: mut Render3dState, l: Layer, model: Model
|
||||||
}
|
}
|
||||||
|
|
||||||
function layer_draw_far(render3d_st: mut Render3dState, l: Layer, shadow: bool, light_vp: floats) -> void {
|
function layer_draw_far(render3d_st: mut Render3dState, l: Layer, shadow: bool, light_vp: floats) -> void {
|
||||||
if l.imp == null or (l.n_far == 0 and not l.g_on) { return }
|
if l.imp == null or l.imp.albedo == 0 or (l.n_far == 0 and not l.g_on) { return }
|
||||||
var p = render3d_st.sc_imp_prog
|
var p = render3d_st.sc_imp_prog
|
||||||
if shadow { p = render3d_st.sc_imp_prog_shadow }
|
if shadow { p = render3d_st.sc_imp_prog_shadow }
|
||||||
gpu_use_program(render3d_st, p)
|
gpu_use_program(render3d_st, p)
|
||||||
|
|
@ -994,7 +1010,7 @@ function layer_draw_far(render3d_st: mut Render3dState, l: Layer, shadow: bool,
|
||||||
# as you approach it. The card is also far the cheaper of the two, which is what pays for
|
# as you approach it. The card is also far the cheaper of the two, which is what pays for
|
||||||
# casting the whole set into all five cascades.
|
# casting the whole set into all five cascades.
|
||||||
function layer_draw_shadow(render3d_st: mut Render3dState, l: Layer, light_vp: floats) -> void {
|
function layer_draw_shadow(render3d_st: mut Render3dState, l: Layer, light_vp: floats) -> void {
|
||||||
if l.n_sh == 0 or l.imp == null { return }
|
if l.n_sh == 0 or l.imp == null or l.imp.albedo == 0 { return }
|
||||||
let p = render3d_st.sc_imp_prog_shadow
|
let p = render3d_st.sc_imp_prog_shadow
|
||||||
gpu_use_program(render3d_st, p)
|
gpu_use_program(render3d_st, p)
|
||||||
let im = l.imp
|
let im = l.imp
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue