Two settings for things arriving: a foliage distance row, and a ramp for lights
**Foliage Distance** (`foliageDistance`, Advanced Graphics, 70..300 yd). The reference draws ground clutter to a hardcoded 70 yd (`[0x867958]`) and we inherited it, so grass, reeds and small bushes arrive at a fixed ring around the player — which is exactly where the eye is drawn to them appearing. The row moves the ring; it changes nothing about what grass looks like. Measured on an Elwynn fence line (`fence-shadowline-day`, clutter density 2), p50 frame time / main thread: 70 yd **3.25/3.26 ms**, 140 **3.32/3.24**, 200 **3.48/3.38**, 300 **3.69/3.42**, with the GPU flat throughout. Doubling it is free; past 140 it costs main thread, which is the budget this client is short of. The registered default IS the reference's own 70 and the row is in no preset ladder, so nothing moves until a player asks it to. It commits on Apply: `fade_far` is baked into the clutter material key, so a change drops every built patch to be re-cut (338 meshes on one measured write) and a drag would do that thirty times a second. `clutter::remesh_on_cutout_change` is now `remesh_on_material_change` and watches both the alpha ref and the fade for that reason. What it buys in a STILL is small — 70 -> 300 moves 0.6% of a frame, the far tufts being sub-pixel — and that is the wrong instrument: the complaint is grass ARRIVING, which no static capture can show. **Light arrival fade** (`lighting::global_light`). No setting: a correction, not a taste. A fixture enters the packed light table for several unrelated reasons — the portal walk reaching its room, its WMO group streaming in, the camera crossing `INTERIOR_NEAR_ADMIT` or `POINT_PACK_RADIUS`, the 255-entry capacity cut re-sorting — and every one of them was a hard edge that switched a light on at full brightness between one frame and the next. Measured in Ironforge by diffing the admitted set between static vantages: walking **3 yd exchanged 2 of 172 admitted lights, and 10 yd exchanged 8** — about one arriving per yard, which at a walk is several a second. The table ran 110-196 across five vantages and never reached the cap, so this is admission churn and NOT capacity; the cap does bind transiently during a world load (305 candidates seen once), which is why the boundary fade below is kept. Arrivals now ramp over 0.35 s, whichever rule admitted them. The scale lands on the light's colour before the pack, so the GPU table, the CPU mirror, the fog lamps and the froxel volume all follow from one write. Deliberately NOT symmetric: a departing fixture still goes at once, because keeping it alive to dim means keeping its room claim alive with it, and a stale claim gates a live light with a dead building's identity. **Spell lights are exempt**, and that is the exemption that matters: a fireball's light lives about as long as the ramp does, so ramping it would spend the whole effect at half strength. Beside it the capacity cut itself is softened — the last 24 admitted ranks scale toward zero across the distance band they span — and nothing fades while the table fits, which is what makes the control possible: with the ramp on against off, the Goldshire inn and the Great Forge both settle BYTE-IDENTICAL. Also recorded in docs/GRAPHICS.md: captures force `WOW_CLUTTER_DENSITY=0` for byte-stable baselines, so a run that sets only `WOW_CLUTTER_FADE` measures a scene with no grass in it and reports every horizon as free. Three rounds of this measurement were worthless before the starved control caught it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
+121
-4
@@ -485,11 +485,61 @@ reason this file's determinism table now needs a caveat: **it screens as determi
|
||||
default tier and is not at tier 1**, where it differs from *itself* by the bottom bit. Determinism
|
||||
is a property of a scenario AND the settings it is rendered with, not of the scenario alone.
|
||||
|
||||
### 6. Runtime probe GI
|
||||
### 6. Runtime probe GI — **measured; it is a smaller and more specific job than this said**
|
||||
|
||||
A toroidal voxel clipmap of the static world plus cascaded probe grids traced in compute, order-1 SH
|
||||
with hysteresis. The biggest lighting change available and the most work; it belongs after the five
|
||||
above, not before.
|
||||
This section used to read "a toroidal voxel clipmap of the static world plus cascaded probe grids
|
||||
traced in compute, order-1 SH with hysteresis". That is the textbook answer to a question this
|
||||
renderer is not asking. Before building any of it, two things were measured.
|
||||
|
||||
**First: almost all of the consumer side already exists.** `lighting::sh::prop_probe_coeffs` folds
|
||||
the reference's own `Model2.bls` accumulators into a 7-row order-2 SH probe;
|
||||
`lighting::prop_probes` keeps an 8192-slot table with a sharing allocator and a despawn hook;
|
||||
**both** `static_gx.wgsl` (terrain and WMO) and `wow_model.wgsl` (M2s, props, characters) already
|
||||
bind that table and already evaluate the basis per fragment. Three call sites already fold probes
|
||||
at runtime (`terrain_stream::spawn`, `interior.rs` twice). What is missing is not the SH, the
|
||||
upload, the table or the evaluation — it is **where the rows come from**: they are folded from
|
||||
baked MODD/MOCV colour and addressed per prop, not traced and addressed by position.
|
||||
|
||||
**Second, and this is the part that redirects the work: the gap is indoors, it is large, and we
|
||||
made it ourselves.** `interiorLight` 1 is our fixture-lit WMO interior; `interiorLight` 0 is the
|
||||
reference's baked interior path. The rig replaced the reference's baked lighting — whose vertex
|
||||
colour carries the artist's implied bounce — with physical DIRECT light from the fixtures, and
|
||||
nothing ever put the bounce back. Measured at noon, Goldshire inn, gamma medians:
|
||||
|
||||
| view | our rig (`interiorLight` 1) | reference baked (0) |
|
||||
|---|---|---|
|
||||
| common room | **31** | 56 |
|
||||
| storeroom, facing the hearth | **12** | 58 |
|
||||
| storeroom, facing away from it | **8** | 33 |
|
||||
|
||||
Two to five times darker, consistently. In the storeroom the WALLS read about right and the
|
||||
**props go black** — hanging meat, barrels and crates reduced to silhouettes — because a WMO wall
|
||||
gets the rig's per-fragment fixture light and an M2 prop gets only its probe.
|
||||
|
||||
**The existing bounce knob cannot close it.** `interiorFill` is one scalar, a half-desaturated mix
|
||||
of the fixture colour applied uniformly. Swept on the storeroom against the baked target of 58:
|
||||
0.08 (shipped) → 12, 0.3 → 17, 0.8 → 25, **2.0 (the maximum) → 46**. It nearly reaches the
|
||||
brightness and lands the hue wrong — at 2.0 the mean is (66, 46, 30) against the reference's
|
||||
(92, 58, 29), a red channel 26 short while blue is already over. A scalar cannot carry a direction
|
||||
or a colour. **A 7-row SH probe is exactly the thing that can**, which is why this lane is the
|
||||
answer rather than a bigger `interiorFill`.
|
||||
|
||||
**And SSILVB cannot cover for it, because it is screen-space.** Same room, same eye, two facings:
|
||||
with the hearth on screen the indirect lane moves **13.85% of the frame** (max 44/255); with the
|
||||
same hearth two yards BEHIND the camera it moves **0.00% of pixels, max 2**. Facing-dependent
|
||||
light is not a dimmer version of GI, it is a different room depending on where you look.
|
||||
|
||||
So the job here is **not** a voxel clipmap of the static world. It is: fold a bounce term into the
|
||||
probe rows the shaders already read, from the lights we already resolve and the surfaces the WMO
|
||||
already tells us about, and calibrate it against the reference's baked interior the same way
|
||||
`daylight::daylight_intensity` already calibrates the threshold — by solving for the value that
|
||||
matches the reference's luminance at a surface both laws render. The exterior case (sunlit ground
|
||||
bouncing onto a shaded wall) is the second increment, and it is the one that would want traced
|
||||
visibility; the interior case needs only what is already in memory.
|
||||
|
||||
The calibration target is a judgement call and belongs to whoever plays it: matching the baked
|
||||
path exactly restores the reference's brightness, and anything short of it keeps some of the rig's
|
||||
contrast. That decision should be made against a screenshot, not derived.
|
||||
|
||||
### 7. Upscaling (FSR) — **not now**
|
||||
|
||||
@@ -609,6 +659,73 @@ do not reproduce. Its before/after delta is 0.00% of pixels at a maximum of 1
|
||||
but a scenario that cannot reproduce itself cannot be a control, and the number to check first is
|
||||
always the baseline against itself.
|
||||
|
||||
### 10. Foliage distance — **built**
|
||||
|
||||
`benilla_world::clutter`, cvar `foliageDistance`, Foliage Distance on the Advanced Graphics page.
|
||||
|
||||
The reference draws ground clutter to a hardcoded **70 yd** (`[0x867958]`) and we inherited it, so
|
||||
grass, reeds and small bushes arrive at a fixed ring around the player — which is exactly where the
|
||||
eye is drawn to them appearing. The row moves that ring; it does not change what grass looks like.
|
||||
|
||||
Cost, measured on an Elwynn fence line (`fence-shadowline-day`, clutter density 2), p50 frame time
|
||||
and main thread:
|
||||
|
||||
| horizon | frame | main | GPU |
|
||||
|---|---|---|---|
|
||||
| **70 yd** (the registered default, the reference's own) | 3.25 ms | 3.26 ms | 1.13 ms |
|
||||
| 140 yd | 3.32 | 3.24 | 1.10 |
|
||||
| 200 yd | 3.48 | 3.38 | 1.13 |
|
||||
| 300 yd | 3.69 | 3.42 | 1.12 |
|
||||
|
||||
**Doubling it is free** and the GPU never moves; past 140 yd it costs main thread, which is the
|
||||
budget this client is short of. What it buys in a STILL is small — 70 -> 140 moves 0.57% of that
|
||||
frame and 70 -> 300 only 0.64%, because the far tufts are sub-pixel — and that is the wrong
|
||||
measurement for it: the complaint is grass *arriving*, which a static capture cannot show at all.
|
||||
The row exists so the ring can be pushed out to where the arrivals are too small to notice.
|
||||
|
||||
It commits on **Apply**, not on the drag: `fade_far` is baked into the clutter material key, so a
|
||||
change drops every built patch to be re-cut (measured: **338 meshes** on one write), and doing that
|
||||
on every tick of a drag would re-cut the world thirty times.
|
||||
|
||||
The default is the reference's own 70 and it is in **no preset ladder**, deliberately — it trades
|
||||
main thread for grass, which is a judgement about a machine rather than a quality tier.
|
||||
|
||||
**A trap worth keeping**, which cost two rounds of measurement here: captures force
|
||||
`WOW_CLUTTER_DENSITY=0` (`lib.rs`, clutter scatters per-run and would break byte-stable baselines),
|
||||
so a run that sets `WOW_CLUTTER_FADE` **without also setting the density** measures a scene with no
|
||||
grass in it and reports the horizon as free at every value. The starved control — clutter off
|
||||
against clutter on — is what catches it.
|
||||
|
||||
### 11. Light arrival fade — **built**
|
||||
|
||||
`lighting::global_light`, no setting: it is a correction, not a taste.
|
||||
|
||||
A fixture can enter the packed light table for several unrelated reasons — the portal walk reaching
|
||||
its room, its WMO group streaming in, the camera crossing `INTERIOR_NEAR_ADMIT` (90 yd) or
|
||||
`POINT_PACK_RADIUS` (300 yd), or the 255-entry capacity cut re-sorting — and every one of them was
|
||||
a hard edge that switched a light on at full brightness between one frame and the next.
|
||||
|
||||
**Measured in Ironforge**, by diffing the admitted set between static vantages: walking **3 yd
|
||||
exchanged 2 of 172 admitted lights, and 10 yd exchanged 8** — about one arriving per yard, which at
|
||||
a walk is several a second. The table was at 172 of 255 throughout, so this is admission churn and
|
||||
**not** the capacity cap; a count sampled across five vantages ran 110–196 and never reached it.
|
||||
(The cap does bind transiently during a world load — 305 candidates were seen once — which is why
|
||||
the boundary fade below is kept.)
|
||||
|
||||
So arrivals ramp over **0.35 s**, about three yards at a walk, whichever rule admitted them. The
|
||||
scale lands on the light's colour before the pack, so the GPU table, the CPU mirror, the fog lamps
|
||||
and the froxel volume all follow from one write. It is deliberately **not** symmetric: a departing
|
||||
fixture still goes at once, because keeping it alive to dim means keeping its room claim alive with
|
||||
it, and a stale claim gates a live light with a dead building's identity.
|
||||
|
||||
Beside it, the capacity cut itself is softened: the last 24 admitted ranks scale toward zero across
|
||||
the distance band they span, so a fixture the cut is about to drop is already dark when it goes.
|
||||
**Nothing fades while the table fits**, which is what makes the control possible.
|
||||
|
||||
Control: with the ramp on against off, the Goldshire inn settles **byte-identical**, and the Great
|
||||
Forge to **max 3/255 on 0.008% of pixels** — the residue being lights still arriving there even at a
|
||||
standstill, which is the symptom restating itself.
|
||||
|
||||
## What is not in scope
|
||||
|
||||
`benilla-bourbon` implements most of §2–§7 already, on **Bevy 0.19.1** with a vendored wgpu 29 and a
|
||||
|
||||
Reference in New Issue
Block a user