render3d: actors cast only into the cascades they can shade; Vulkan GPU timings and a mean frame split in R3D_PROF

Where the frame goes, before optimising it further. R3D_PROF now prints the average frame: CPU before
the swap (the game's share apart), GPU and swap, and the renderer's CPU phases in frame order - the
shadow pass split into cascade fit, scatter casters and actor casters. On Vulkan the per-pass GPU
table comes from timestamp queries (host query reset asked for where the device has it); MoltenVK's
attribution is tile-based and not to be trusted per pass, the PC's is.

What it showed on the PC (camp): 4.3 ms CPU and 5.3 ms GPU a frame; the shadow pass was the largest
CPU phase (1.8 ms) and actors half of that. Every actor within 300 m was drawn into all five cascades,
though the outer two only shade receivers from 212 and 935 m out: 160 actors and 300 draws into each.
cast_band_reaches - the flowers' reach test, now shared - skips an actor for a cascade it cannot shade
(receivers counted from 0.85 of the previous split, where sunShadow's cross-fade begins).

PC camp, two runs each: mean frame 9553/9601 -> 8664/8656 us; CPU 4.3 -> 3.7 ms; shadow GPU 1.14 ->
0.79 ms; actor-shadow CPU 1.02 -> 0.60 ms; 2039 -> 1411 draws; self-tests 59/59, validation 0.
Mac: OpenGL shot viewpoints and the camp byte-identical; town 19 px at <= 2/255 on two flower stems a
few metres from the camera - the accepted leftover-binding difference, no shadow; self-tests 59/59 on
OpenGL and Vulkan. R3D_CAST_ALL=1 draws every caster into every cascade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-15 18:03:31 +03:00
parent ef745a141d
commit 5b9403c343
6 changed files with 134 additions and 10 deletions

View file

@ -370,10 +370,35 @@ function actor_draw_casters(light_vp: words) -> void {
let r = f_max(f_mul(a.radius, a.scale), hh)
if not ac_in_light(light_vp, a.x, f_add(a.y, hh), a.z, f_mul(r, fl(1.5))) { continue }
}
# Inside the light box is not the same as able to shade anything this cascade covers: every actor
# within 300 m sits inside the far cascades' boxes, whose receivers start 250 and 1100 m out - at
# the camp 160 actors cast 300 draws into each of those, against 41 into the nearest.
let dxa = f_sub(a.x, cam_pos[0]); let dza = f_sub(a.z, cam_pos[2])
let da = f_sqrt(f_add(f_mul(dxa, dxa), f_mul(dza, dza)))
let ra = f_mul(a.radius, a.scale)
if not cast_band_reaches(f_max(f_sub(da, ra), F_ZERO), f_add(da, ra), f_mul(a.model.height, a.scale)) { continue }
var ap = ac_sh
if a.cutout { ap = ac_sh_cut }
if ac_census_at > 0 and ac_frame == ac_census_at { ac_caster_count(a) }
actor_draw_one(a, ap, true)
}
if ac_census_at > 0 and ac_frame == ac_census_at {
print(`actor census, cascade {sh_cascade}: {ac_cc_actors} actors cast {ac_cc_draws} draws ({ac_cc_skinned} of them skinned, {ac_cc_cut} cutout)`)
ac_cc_actors = 0; ac_cc_draws = 0; ac_cc_skinned = 0; ac_cc_cut = 0
}
prof_cpu_mark("shadow actors")
}
var ac_cc_actors: int = 0
var ac_cc_draws: int = 0
var ac_cc_skinned: int = 0
var ac_cc_cut: int = 0
function ac_caster_count(a: Actor) -> void {
var shown = 0
for p in 0 .. len(a.model.prims) { if a.hide == null or a.hide[p] == 0 { shown += 1 } }
ac_cc_actors += 1
ac_cc_draws += shown
if a.skin != null or a.model.skin != null { ac_cc_skinned += shown }
if a.cutout { ac_cc_cut += shown }
}
# every actor off the stage at once, for a world being replaced. Actors own no GL objects;