diff --git a/packages/ludic.render3d/env.ludic b/packages/ludic.render3d/env.ludic index be635281..5900e486 100644 --- a/packages/ludic.render3d/env.ludic +++ b/packages/ludic.render3d/env.ludic @@ -199,6 +199,11 @@ export state Render3dState { 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_*) vkmem_at: int = 0 # R3D_VKMEM=: 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=:, 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 gvk_pool: long = 0 gvk_fence: bytes = null diff --git a/packages/ludic.render3d/fog_impostors.ludic b/packages/ludic.render3d/fog_impostors.ludic new file mode 100644 index 00000000..53b6adfa --- /dev/null +++ b/packages/ludic.render3d/fog_impostors.ludic @@ -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= with R3D_FOG_TO=: 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)`) } diff --git a/packages/ludic.render3d/gpu_vk.ludic b/packages/ludic.render3d/gpu_vk.ludic index cd558857..130c456f 100644 --- a/packages/ludic.render3d/gpu_vk.ludic +++ b/packages/ludic.render3d/gpu_vk.ludic @@ -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 # 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 - 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 if image { kind += 1 } var i = 0 diff --git a/packages/ludic.render3d/r3d.ludic b/packages/ludic.render3d/r3d.ludic index 5b29e05e..87d9f40d 100644 --- a/packages/ludic.render3d/r3d.ludic +++ b/packages/ludic.render3d/r3d.ludic @@ -35,6 +35,7 @@ import "gltf_cached.ludic" import "skin.ludic" import "scatter.ludic" import "fog_casters.ludic" +import "fog_impostors.ludic" import "actor.ludic" import "stream.ludic" import "grass.ludic" diff --git a/packages/ludic.render3d/render.ludic b/packages/ludic.render3d/render.ludic index 42959189..cca08c88 100644 --- a/packages/ludic.render3d/render.ludic +++ b/packages/ludic.render3d/render.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 render3d_st.sc_view_gen += 1 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 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. render3d_st.r3d_test_frame += 1 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")) } # R3D_RESIZE_AT=: 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. diff --git a/packages/ludic.render3d/scatter.ludic b/packages/ludic.render3d/scatter.ludic index 9f489019..b872c317 100644 --- a/packages/ludic.render3d/scatter.ludic +++ b/packages/ludic.render3d/scatter.ludic @@ -12,7 +12,11 @@ property Impostor { normal: int = 0, tiles: int = 16, 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 { model: Model, @@ -323,6 +327,18 @@ function impostor_bake__t(render3d_st: mut Render3dState, model: Model, tiles: i im.tiles = tiles im.radius = model.radius * 1.02 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 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) @@ -349,7 +365,7 @@ function impostor_bake__t(render3d_st: mut Render3dState, model: Model, tiles: i let r = im.radius let hh = model.height * 0.5 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) for t in 0 .. 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 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; @@ -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) 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 scatter_attach(render3d_st, render3d_st.sc_card, l.imp_buf) + fog_impostor(render3d_st, l) } # ---- 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 { - 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 if shadow { p = render3d_st.sc_imp_prog_shadow } 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 # casting the whole set into all five cascades. 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 gpu_use_program(render3d_st, p) let im = l.imp