grom's 06:23 session died 183 s in on first world entry
(Diagnostics/crash-1792671984.txt): insert::<GroundShade> on 4431v0, "its
index now has generation 1", one frame after a 4301 ms world-entry hitch
during which the server sent a burst of creates and destroys.
The mechanism is structural, not a one-off. A command queue is applied at the
schedule's next sync point, not when the system returns, so a destroy handled
by a system that ran *earlier* in the same frame is still only queued while
attach_entity_visuals' query runs: the row looks alive, the whole visual is
built for it, and the despawn lands first at the sync point. The panicking
command form then kills the client for an entity the server had already taken
away.
So every write this system makes onto the net entity root is now a try_
command. add_child was the one that needed more than a rename: it queues its
command on the *parent*, so the conform node and RigPose::anchor_for's bone
anchors are related at spawn with ChildOf instead — the relationship hook
tolerates a missing target where a command on it does not.
The test builds a CommandQueue the way the existing rig test does, despawns the
root before applying it, and asserts the entity stays gone. It fails with
grom's exact panic text when any single write is put back, which is how I know
it bites.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
Two durations were being conflated. How long the thread has not moved is the defect; how long the
span it is inside has been open is only where it is stuck, and for an enclosing span that is the
whole session — which is why the live log said "has been running 360380 ms" and then "was in there
at least 360406 ms" about a 150 ms hiccup.
So both, labelled, and the thread's name beside them: the span it is stuck in may be unnamed, since
an enclosing `info_span!` carries no `name` field for `on_new_span` to record, and then the thread
is the only thing the report can name. Before this, such a stall printed `span #2473901162497` and
nothing else.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The watchdog keyed on "a span has been open past the threshold". Spans nest, and `on_exit`
republishes the enclosing one deliberately — so between any two inner spans the slot shows a span
that has been open since the first frame, and the watchdog said so.
In a 105-second play session that was 13,658 of the log's 15,335 lines: `STALL: span
#2473901162497 has been running 209147 ms`, alternating with `STALL ended`, every ~75 ms, each one
re-reading /proc for 39 threads — inside the session it was there to measure. The instrument whose
whole job is to be trustworthy when nothing else is was the loudest liar in the log, and it could
not even name what it was accusing, because an enclosing span has no entry in `names`.
So `Slot::beat`, bumped on every enter and every exit. A stall is now a thread that has not entered
or exited anything for the threshold — which is what "wedged" means and what the module header
already claimed to be reporting. The end line also gets the real total rather than the duration at
first sighting, which is why both lines used to print the same number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
`flush_static_gx` bakes a cell once it has been dirty and *quiet* for its
window, which staggers cells whose changes are staggered — and a world load
dirties hundreds within a few frames of each other, so their windows expire
together and the whole burst baked in one flush. A cold Elwynn vista measured
one run of 43.2 ms against 2.6 ms for the other nineteen in that second
(`WOW_SYSTEM_TIMING=1`): a visible stall, and a scheduling artefact rather than
work, since the same bakes cost the same total spread over later frames.
`BAKE_BUDGET` spends 4 ms of wall clock per flush — the figure
`terrain_stream::SPAWN_BUDGET` already paces placement spawns by, and the two
passes share a frame — and hands the rest to the next flush. A region that
yields stays `dirty` with its `last_change` untouched, so `bake_due` says yes
again next flush: yielding is the absence of a decision rather than one, which
is what keeps the budget to *when* a region bakes and never *whether*. The three
exemptions say the same thing. Nothing yields until one region has baked, so a
region larger than the whole budget cannot stall behind itself. `flush_now` gets
the full flush it asked for. The `MAX_DIRTY_FRAMES` age cap outranks the budget,
so the backstop that already bounded a region's wait stays absolute instead of
becoming advisory. And an empty region is an unload — free to publish, and
deferring one would leave dead geometry on screen — so removals are never
budgeted at all.
Measured after, same vista: every budgeted flush landed 4.0–4.5 ms, draining a
backlog that peaked at 398 held regions at ~30 a frame over 16 consecutive
flushes. Worst visible flush 43.2 → 4.5 ms.
One flush still runs 58.4 ms and should. `terrain_stream.rs:908` asks for a
`flush_now` while `!focus.paced`, because a region that cannot draw yet is "a
hole in the reveal", and `ViewFocus::paced` is false only "through entry, a
teleport and a world swap, while the loading cover absorbs the burst". So the
budget moves that stall out of play time and behind the cover, which is the
point; it is not a leftover to fix.
`WOW_GX_BAKE_MS=<ms>` overrides the budget and an explicit `0` retires it, so
the A/B runs in both directions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
`WorldPlugins` has disabled `MassPropertyPlugin` for as long as the world has
had colliders, with the reason written beside it: there are no dynamic bodies,
so no mass is ever read. The pass kept running anyway. `ColliderBackendPlugin`,
which we keep for `update_collider_scale`, registers
`update_collider_mass_properties` itself (`collision/collider/backend.rs:232`),
and that system's query filters `Without<Sensor>` and nothing about the body
type — so every collider that *changed* paid `Collider::mass_properties`, a
full parry volume integral over the trimesh, for a value with no reader.
Terrain streams its colliders as trimeshes by the tile, so the cost lands in a
world load. Measured on a cold Elwynn vista with `WOW_SYSTEM_TIMING=1`, before
and after, same viewpoint:
before run_fixed_main_schedule worst 183.0 ms 200.9 ms/s
after run_fixed_main_schedule worst 1.3 ms 25.9 ms/s
`update_collider_mass_properties` no longer appears in the report at all, and
its 181.3 ms worst run was the whole of that schedule's spike — the nesting is
what identified it.
A run condition rather than another `.disable()`, because the system cannot be
removed individually and shares a `.chain()` with `update_collider_scale`.
`MassPropertySystems` is `pub` but not in avian's prelude, hence the full path.
The invariant the whole physics trim rests on now has a wall of its own:
`tests/no_dynamic_bodies_wall.rs` fails if `RigidBody::Dynamic` appears in the
workspace. This bug was exactly an invariant that lived only in prose — the
`.disable()` comment stated it correctly while a system slipped through the gap
between two plugins — so the assert is the point. Verified by planting a
dynamic body and watching it fail, then reverting.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The interior prop-probe table holds 8192 and cannot be widened — 13 mesh-tag
bits, with every bit of that u32 already spoken for. It keyed on exact
coefficient bits, so two props shared a slot only when no MOLR light reached
either *and* their baked MODD colours matched to the last bit. Measured in
Ironforge at grom's login spot: 3,883 distinct probes standing still, 5,543 at a
far clip of 777, and his session saturated during the world load — after which
every further interior prop falls back to the room's flat day/night law.
The distinctness is the per-prop baked colour, not the prop's position: folding
the probe on a 16 yd grid instead of each prop's own centre removed only 4.5% of
the slots. Rounding the *key* onto a 1/32 step removes 35%. A shared slot keeps
the first claimer's exact rows, so the quantization decides who shares and never
reaches the light the shader reads.
Verified by render, not by argument: that Ironforge storeroom at the new default
is byte-identical to exact keying across 1.44M pixels, while starving the table
outright (the new `WOW_PROBE_CAP=0`) moves 109,830 of them by up to 14/255 — so
the lane does drive this view, and the sharing costs nothing measurable in it.
The table now also counts the distinct probes it refused, and all three overflow
warnings report it. That number is the whole question when the table saturates: a
small overflow is answered by sharing harder, a large one only by more slots, and
until now the log could not tell them apart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
A player sees something wrong for two seconds and quits. What the client said at
that moment lived only in the terminal it was launched from — which, for a player
who starts it from a desktop, is nowhere. So three things, all aimed at the next
report being answerable from the machine it happened on.
**The session log.** `benilla-config/Logs/benilla.log` now holds every line that
goes to stderr, with the previous two runs kept beside it as `benilla-1.log` and
`benilla-2.log`. The ring that feeds the crash report already formats these lines
([`log_ring`]), so the file is one more sink on it: the ring's backlog replays into
the file when it opens, so the boot lines are not lost to the attach being late,
and writes are unbuffered because the point of the file is to survive the crash
that made someone read it. 64 MiB cap, then one line saying so. A capture run
resolves no state folder and writes none. Player chat is logged at `debug` and so
is not in it; server notices are.
**The zone line.** One line per change naming the whole chain: the leaf area id
the terrain chunk under the player carries, the zone it rolls up to, and what the
world map selected from that. This is unanswerable otherwise — the ids live for a
frame, and the map only shows the end of the chain. It is already earning itself:
the player reported the map naming a far-away zone in a custom area, and the
terrain says why. `Azeroth_36_43.adt` (the server's own, out of `patch-A.mpq`)
tags 44 of its 256 chunks `5599 "Rugford's Mountain Rest"` — the custom area the
quest is in — and **53 of them `3 "Badlands"`**, in a block that starts a few
chunks south of it. Walk off the rest and the client reads Badlands off their
terrain and moves the map there, exactly as the reference would.
**The interior line.** One line per room change saying whether the camera is
inside a WMO group and which, because that claim decides whether the open world is
drawn at all: inside, the exterior is culled to what the portal flood reaches. "It
all went grey but I could see at some angles" is a description of that state, and
from the outside a wrong claim and a right one look identical.
`log_ring::attach_file` and `FILE_CAP_BYTES` are published engine API: the engine
owns the log, the app owns where this player's state lives. The wall caught both
the moment they crossed, which is what re-baselining it was for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
`scripts/check.sh` has been failing before it ran a single test, on 62 clippy
findings in code none of the recent rounds touched — 32 in `benilla-world`, 30 in
`benilla-app` — so the in-loop gate could not tell anyone anything. Every one is a
newer clippy noticing older code (`as_chunks` and `is_multiple_of` are both lints
younger than the lines they fire on), and they are fixed, not silenced, except
where the silence is the point:
- **Argued allows, the house idiom.** `clippy::type_complexity` on ten Bevy system
signatures and two inline caster queries, each with the one-line reason beside
it, as the other 67 in this workspace have. `clippy::approx_constant` on
`liquid/waves.rs`'s `TWO_PI`, whose own doc already says why it is spelled
`6.2831853`: it mirrors a shader line, and `f32::consts::TAU` would stop reading
against the file it copies.
- **Two runtime assertions became compile-time ones** (`EXT_SEL_EMPTY is index 255`,
the endless-stream frame count): a claim about constants should fail the build
that breaks it, not a test run after it.
- **Three real, if harmless, redundancies gone**: `len > MAX_TILT_TAN && len > 0.0`
(the constant is `tan(4°)`, so the second test never spoke), `u32::from(..) + 1 |
flags` now parenthesised where `+` binds tighter than `|`, and `palette.get(0)`.
- The rest is mechanical: `chunks_exact(3)` → `as_chunks::<3>()`, `map_or(true, ..)`
→ `is_none_or`, two `Default::default()`-then-assign test fixtures, a doc line
that began `1.` and so turned its own paragraph into a list item, and
`local_utc_offset_secs` moved above the test module it had drifted below.
`tests/repro_mail.rs` is deleted. Its first line says "TEMPORARY repro harness —
not for commit"; it reads a loose mirror of FrameXML out of `/tmp/opencode/
vanilla-ui`, which no longer exists, so it has been failing on a missing directory
rather than telling anyone about mail.
What is left of the gate's red is one test: `world_api_wall` counts 174 crossings
of the `benilla-world` boundary against a ceiling of 107 — 90 of them already
filed with a close plan, 84 unsorted. That is not cleanup; the wall's own message
says the remedies are to close the crossing or to publish it as engine API with the
record that says so, and 84 of those is a design pass on what the engine's public
surface is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
The SH-probe table is 13 tag bits wide and nothing can widen it — the probe
field is the last of the u32 — so a dense interior can ask for more slots than
exist. His session hit it (`live 8192, peak 8192`), and every prop that missed
out was then tagged as plain exterior: furniture lit by the sky, which in a Dun
Morogh snowstorm reads as the weather coming indoors.
A prop that misses a slot is still *indoors*, and saying so costs no bits.
`PropSeat` names the three states a spawner can be in — `Exterior`, `Probe(slot)`
and `Matte` — and `Matte` is the exterior payload with `MATTE_INDOOR_BIT` and a
saturated lane: the room's day/night law, which is exactly what
`interior::AppliedLaw::Matte` already gives an entity whose classification finds
the table full. A dim room instead of a sunlit one.
The seat also replaces the bare `Option<u16>` the assembler took, so a caller
now says which of the three it means rather than leaving `None` to stand for
both "sky-lit" and "wanted a probe and lost".
Still true: the table can fill, and the props past it lose their room's *baked*
light. Lowering that demand (sharing by a quantized fold, or scoping the
allocation) is the next lever and wants a measurement first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This checkout had no git history at all, so this one commit carries the whole
of the Everwood work as a single import on top of chairmanpow/benilla-octo's
`main` (706cd71). The two trees have genuinely diverged — each has files the
other does not — so this is a fork point, not a catch-up.
Today's work, which is the reason a baseline exists at all:
- Booth previews no longer cast world shadows (`NO_BOOTH_SHADOW`). Portrait
characters were entering the sun's shadow cascades, so every skinned-rig
shadow specialization compiled live mid-frame; the hitch detector logged
"main thread blocked 1316 ms" beside a `compiled LIVE` warning. This was
the Fashion/Tmog stutter.
- Per-pane dress-up: each `DressUpModel` pane assembles and stages its own
tried-on look instead of every pane mirroring the live player.
- Item template cache is append-only JSONL written on the IO pool, replacing
an 11.6 MB whole-map JSON rewrite on the main thread every 10 s.
- `WOW_SYSTEM_TIMING` rebuilt per-thread: it took 2 mutexes per system enter
and 4 per exit, costing 15 ms a frame (590 fps -> 60) and corrupting the
measurements it was taken to produce.
- The visibility-churn sweep moved to its own `WOW_FLASH_CHURN` switch, so a
frame-cost reading no longer pays 2.7 ms a frame for it.
Local state (`benilla-config/`, `logs/`, `target/`) is excluded by the
existing .gitignore; the account credential it holds is not in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
Same fork policy: upstream structure wins, local-unique re-instated.
Cargo.toml unions both measured dev-profile philosophies (codegen-units=1 +
lto=off from samwhosung, debug=line-tables-only + split-debuginfo from the
fork's disk measurements).
glue_booth.rs takes samwhosung's typed GlueScene (token/of_race/from_model_path)
and re-adds the fork's TurtleWoW races (Goblin=9, BloodElf=10) as variants;
their glue fog stays on Human's row, exactly as the fork's scene_fog fallback did.
ui script takes samwhosung's mouseover/hyperlink rework (world_mouseover,
minimap_blip, hyperlink.rs) over the fork's inline hover_link/span_at/pointer
path — the rework covers the same OnHyperlinkEnter/Leave behaviour.
game_plugins.rs keeps the fork's re-measured ceilings (466 / 5,509); mail.rs
keeps the explicit 13-tuple with a float daysLeft; MAP.md takes the superset.
Fork policy (docs/METHOD.md): upstream structure wins; local-unique work
re-applied on top. This commit is the upstream half of the reconciliation.
Upstream is the same program at a rewritten history (884dff77 == 4e0e89cb
tree), so the merge base was replaced to keep a real three-way diff. The
large conflicts are upstream moving first: the UI manifest (benilla.toc ->
the binary LAYER_MANIFEST/layer.toc split), the CVar table (cvars.rs ->
cvars/{mod,table}.rs, +24 video rows), the editbox flush model, the
host-side GetTime, the Options* -> BenillaOptions* rename, expanded
ui_unit.rs, and a rebuilt volumetric_light.rs.
Local-unique pieces re-instated on the upstream structure:
- OptionsFrame.xml: the 1.12 OptionsFrameSliders table restored under the
Benilla rename, with the TurtleWoW deviation note (patch-4 replaces
OptionsFrame.lua).
- layer.toc: CharacterFrameAdapters.xml (the sixth TurtleWoW character tab).
- ui script: wipe/sizeof, BenillaGetMerchantItemStats, and the attach/mount
teardown re-walk all survive.
- game_plugins.rs keeps UPDATE_ACTIONABLE_CEILING=5_509 (re-measure pending).
The LFT/Turtle_TransmogUI rows benilla.toc named off the player's install
now come from the chain's own FrameXML.toc via the new core loader.
A light whose whole candidacy sphere (the shader's own reach, the interior fill
dome included) sits off-screen leaves the packed table, so a city's torches
behind the player stop padding the per-vertex walk. The packer also reuses one
scratch buffer and tests the cheap camera distance before the portal walk.
1.12.1's MSG_MOVE_TELEPORT_ACK handler (0x603f90) reaches none of the
loading screen's raises: 0x406800 is entered only from SMSG_TRANSFER_PENDING
(0x401900), the glue EnterWorld (0x46b6f1) and 0x407de0. benilla raised the
cover on a same-map snap twice over: the "focus not resident" backstop on the
snap frame, and a >=100 yd relocation raise. The relocation raise goes, and
the backstop stays off while a snap's settle hold is up; the body is held over
the destination as before.
Verified live against the local realm (map 0, Gmagent): .tele ironforge from
Northshire (~3960 yd) and back covered 50 and 93 frames (0-916 ms, 0-1680 ms)
before, 0 and 0 after; cross-map .tele valley and back still raise the cover
("worldport"). New tests in loading_screen.rs fail on the old code at the snap
frame. benilla-app and benilla-world lib tests green, install-reading tests
green with BENILLA_REQUIRE_DATA=1 (the addon corpus is absent on this
machine). Built on macOS (arm64).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seam W: InstanceSurfaceParams (whole_wet, lower_wet, waterline_y, feather) per rig
slot, a 2048-row table in the light buffer, applied in wow_model.wgsl through the
existing wet_surface/wet_sheen (max with rain wetness, opaque/alpha-test + fade
twins only). Zero row = stock image. Self-contained: cherry-pickable to lighting.
Seam F: WaterFoamSpawn message; plugin foam records live in their own 96-record
pool so they never evict or throttle the stock unit wake/ring records.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner: planar is cheap enough (~1 ms CPU, GPU about even) to replace SSR outright. High water
always reflects with the planar mirror; the waterReflections cvar, its preset rows, the options
row (EN/RU) and the debug knob are removed. The screen-space march stays only as the mirror's
fallback for water at another height than the mirror plane; WOW_WATER_REFLECT=0 remains a dev
A/B switch.
SSR: the march accepted rays up to 2 + 0.1*dist yd behind the scene depth and returned the
bisected pixel unchecked, so rays passing BEHIND a thin dock deck converged on its front
silhouette - a vertical smear. After bisection a hit may now sit at most
0.25 + 1.5%*dist + the last interval behind; deeper = passing behind an occluder, march on.
Planar: liquid/mirror.rs (WaterMirrorPlugin) renders a half-res mirrored camera (oblique
near plane at the water) before the world camera into binding 107; no water, no sun
shadows, no post. The plane is voted by a 7x6 ray fan with hysteresis; water off that
plane fades back to SSR. Cost ~+1 ms CPU, GPU about even (SSR march skipped) -> its own
cvar waterReflections (0 Screen Space / 1 Planar, EN+RU option row), on for High/Ultra.
The High preset (= registered defaults) now carries High water (waterQuality 2); Low and
Medium keep Enhanced. Classic and Enhanced captures byte-identical.
The enhanced exterior WMO point-light term evaluated falloff per fragment but used the
flat lamp selection of the provoking vertex (snapped to its 33 yd MCNK cell), so a large
wall or paving triangle took one vertex's whole lamp pool and neighbours differed: long
straight-edged dark wedges on Stormwind's gate ramp and yard walls at dusk/night. Ranks
the closest exterior lamps at the fragment (static_gx + fallback wow_model), shared by
the shadowed and unshadowed evaluations. Classic (all light gains 0) keeps the vertex
pick and its packed frame bits; capture scenarios stormwind-training-yard-*/gate-ramp-*.
A doodad standing on its own baked terrain shadow (Lakeshire gryphon roosts, most forest
trees) was dimmed twice by day: by the MCSH 0.5 sun band AND by the realtime shadow map,
on ground that no longer shows the bake. wow_model.wgsl and static_gx.wgsl now use
terrain.wgsl's own gate (sh_c16.w, weighted by the sun lane so the bake returns at dusk).
Lane off = reference bits. Capture scenarios lakeshire-statue*/lakeshire-gryphons*,
WOW_CAPTURE_EYE/LOOK/MINUTE re-aim overrides.
foliage_wind_batch matched 'thorn' anywhere in the path, so Stranglethorn/ActiveDoodads/Cages
swayed. rigid_model basename rejection now also lists cage and prison.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit add0a8edb2b2524b05bf60f045e96a5a6f290112)
Placements.by_id was keyed by the raw 32-bit UID, so an M2 (MDDF) and a WMO (MODF) carrying
the same numeric UID merged: the second registration only bumped the first one's refcount and
never spawned its model (approach-order dependent; e.g. UID 54418 farm.wmo vs redridgefallentree01,
54705 humantwostory.wmo vs elwynnbush09, 55333 redridgedocks02.wmo vs elwynnbush09).
Key is now u64: M2 = uid, WMO = (1<<32)|uid, carried through tile refs, refcount/release, WMO
readiness, straddler bookkeeping, prop merge and collider weld. Spawn metadata keeps the 32-bit UID.
Regression: monkey_mddf_and_modf_with_the_same_uid_stream_independently.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a55cfa184377f87734a352c6b399adc8833b25eb)
Exclude interior sources from unshadowed lamp fog. Require a ready matching shadow map for exterior WMO spill and remove unoccluded room fill on that path. Verified the fog-picker regression in 4314 passing app tests, Windows play build and clean inn/exterior-night captures.
Co-Authored-By: GPT-6 Astra (Codex)
World and entity receivers now choose the nearest ready torch map within the existing tolerance. Previously an earlier neighbouring fixture won even when the light's own map was present at its exact position. Add an opt-in attached spell-light capture fixture and correlation trace.
Verified on Windows with the app library gate, play build and inn/blacksmith captures. The inn trace reproduces first slot 3 versus exact-position slot 11. Visual 60056 movement and doorway transitions still require an in-game retest.
Co-Authored-By: GPT-6 Astra (Codex)
The GPU-skinned character caster made realtime shadows render wrong, and the
Stormwind lag it was meant to fix does not come from the shadow lanes anyway
(character shadows are off there, so the rig is torn down). Restore the
CPU-skinned merged proxy exactly as it was.
The character shadow lane CPU-skinned every admitted unit into one merged
Mesh asset and re-uploaded it each rebuild — the documented expensive half
of the lane's ~3 ms, and the part that scales with players. Replace it with
one GPU-skinned caster per admitted creature part: the part's own skinned
mesh and MeshTag, drawn through a new SkinnedShadowCasterMaterial whose
prepass vertex skins from the shared rig palette (the same rows wow_model
reads). Pose changes are free; only membership is reconciled per frame.
Palette region offsets are pushed as shader defs, so the caster shader reads
the shared light buffer's tail as a flat array and needs no copy of the
WowLight layout.
Reject rock, stone, boulder, pebble, gravel and rubble model filenames from retained foliage. Give matching ground-detail models zero bend height so neither grass wind nor player parting moves them. Limit the name veto to the filename to preserve vegetation in stone-named zones.
Verified on Windows: two foliage classifier tests and one rigid-model test pass; benilla-app lib tests have 4313 passed, 0 failed, 10 ignored (no-install run); play build passes. Elwynn wind captures at 0 and 3 seconds preserve coloured foliage motion. The tester's specific small-rock assets still need an in-game retest.
Co-Authored-By: GPT-6 Astra (Codex)
Move the foliage wind marker from bit 14 to bit 15 in its Rust producer and WGSL consumer so distance-fade twins cannot trigger a downstream fragment effect. Keep bit 14 reserved for those extensions.
Verified on Windows: cargo build -p benilla --profile play; cargo test -p benilla-app --lib (4313 passed, 0 failed, 10 ignored; no-install run). Public-base and fixed captures at the reported Elwynn position are pixel-identical; downstream integration still needs its in-game retest.
Co-Authored-By: GPT-6 Astra (Codex)
Letter and punctuation keys were named by their US position, so on AZERTY the key labelled Z bound as W, and Ctrl+A sat on the key labelled Q.
1.12.1 on Windows names a key from the VK its layout produced. `VK_A`–`VK_Z` and digits are named as themselves, and other keys by `MapVirtualKeyA` (`0x42da39`); a key with no character is dropped (`0x42da49`). The EditBox's Ctrl chords switch on that same name (`0x77b1fb`). The Mac client names keys by `KeyTranslate`.
Now:
- Windows takes the same path through `MapVirtualKeyExW`.
- The Mac names keys through the current layout, or the ASCII-capable layout it pairs with.
- Linux follows one documented rule.
- Bindings, Key Bindings capture and Ctrl/Cmd+A/C/X/V all follow the layout.
- A character above U+00FF names its key, as a deviation.
Tests cover AZERTY, Hebrew, Greek, Lithuanian, Turkish, Bulgarian, Hungarian and Dvorak names, layout switches and chords.
Fixes#621Fixes#746🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A crowd of arriving players was composited in one frame, on the main thread, under the archive lock: 40 geared looks took 76–115 ms.
In 1.12.1 an unforced full composite never waits on its section loads (`0x44b430`), and a unit whose first composite is pending is not drawn: ShouldRender `0x607da0` feeds the draw flag `0x710c50`, and the flag goes to the mount model when mounted. It appears dressed once the composite is done.
Now an arriving body composites on the async pool, one task per look, reading outside the lock. The body, and its mount, are built once the atlas lands. Arriving now costs the main thread 0.26–0.45 ms for 40 looks. The appear fade runs from arrival, as `FadeTo` is armed at create.
A re-dress, a rig heal and the glue and dressing-room previews stay in the frame. Baked NPC skins are unchanged.
Fixes#609🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Volumetric light: the shadow rig's directional light (sun by day, moon at
night) is ray-marched through its cascades at half resolution (16 Medium /
32 High jittered samples from 0.35 yd, height-thinned air, HG g 0.6), then a
depth-aware blur and upsample composite it over the fog pass. Density and
colour follow the zone fog; no shafts in WMO interiors. The volumetric fog
pass retires its own midpoint shafts while this is on. Cvars volumetricLight
0/1/2 and volumetricLightStrength, env WOW_VOLLIGHT; Graphics Preset Medium 1,
High/Ultra 2.
Moonlight: a cool, dim directional term from the visible moon added to the
stock night on terrain, models, WMO exteriors and doodads, shadowed by the
moon's map (shadow_hook::realtime_shadow_moonlit .z), ramping with the moon's
elevation and handing back to the sun at dawn. MonkeyFrame grows one row
(per-frame blob 8800 B). Cvar moonLight 0..2 (0 = the reference night).
Advanced Graphics rows (EN + RU), capture scenarios vol-*, docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Volumetric light: the shadow rig's directional light (sun by day, moon at
night) is ray-marched through its cascades at half resolution (16 Medium /
32 High jittered samples from 0.35 yd, height-thinned air, HG g 0.6), then a
depth-aware blur and upsample composite it over the fog pass. Density and
colour follow the zone fog; no shafts in WMO interiors. The volumetric fog
pass retires its own midpoint shafts while this is on. Cvars volumetricLight
0/1/2 and volumetricLightStrength, env WOW_VOLLIGHT; Graphics Preset Medium 1,
High/Ultra 2.
Moonlight: a cool, dim directional term from the visible moon added to the
stock night on terrain, models, WMO exteriors and doodads, shadowed by the
moon's map (shadow_hook::realtime_shadow_moonlit .z), ramping with the moon's
elevation and handing back to the sun at dawn. MonkeyFrame grows one row
(per-frame blob 8800 B). Cvar moonLight 0..2 (0 = the reference night).
Advanced Graphics rows (EN + RU), capture scenarios vol-*, docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A meshed doodad counted as drawn whenever one of its parts had a `Visibility` other than `Hidden`. Bevy's frustum culling writes only `ViewVisibility`, so a doodad behind the camera kept an `Inherited` part and went on animating and ticking its particles.
The reference animates (`0x714260`) and ticks (`0x718960`) only the M2 scene's worklist, which `0x707680` builds. A doodad is admitted to that worklist only after the 6-plane frustum test `0x683700`.
`gate_doodad_anim` now also tests a meshed host's fade sphere against the world camera's frustum. That sphere is the doodad's world bounding sphere, fixed at spawn. A meshless host already had the same test, and both branches now share one closure. The `Visibility` term stays, because it carries the fade, exterior and room verdicts.
Test: `a_meshed_host_outside_the_frustum_is_parked`. Two meshed hosts with `Inherited` parts sit one behind the camera and one ahead of it. Only the host behind is parked.
Fixes#619🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A placement whose model load failed (a file missing from the chain, or a model the parser rejects) stayed unspawned for ever. The spawner treats `m2s.get(h) == None` as "still loading", the progress count holds it as pending, and `scene_ready` needs zero pending, so the loading screen never cleared.
`spawn_loaded_placements` now first retires any unspawned placement (M2 or WMO root) or WMO prop whose handle's `LoadState` is `Failed`. A retired entry spawns nothing, is flagged spawned, and leaves `pending_spawns`, with one warning naming the path. That is the same fallback the WDT load already takes. Handles that are still loading keep waiting. WMO groups read inline in the WMO loader, so the root's state covers them.
`a_failed_model_load_retires_the_placement` and `a_failed_prop_load_retires_the_prop` load unresolvable paths through a real `AssetServer`. Both fail with the failure check removed. Built and gated on macOS. What draws in place of a missing model stays #330.
Fixes#616🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- On Linux a key the keyboard layout remaps (X11's caps:escape) acts as the key it is remapped to, as in the real client
- A layout-moved named key also acts as that key on Windows, where the reference reads the layout's virtual key; macOS keeps its physical table
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
**#549: the stock video window's CVars.** Its check boxes, `ffx` and `gxRefresh` are registered at the reference's defaults. `pixelShaders` and `specular` ship as `overridden` "1" (`hwDetect`, `0x639a60`), and `gxFixLag` ships "1".
- `ffx`, `ffxGlow` and `ffxDeath` gate the full-screen effect passes every frame, as the reference's gate does (`0x6cd890`; `CFFXGlow` reads `ffxGlow` at `0x6cc5a8`, `CFFXDeath` reads `ffxDeath` at `0x6cdf10`).
- `gxMaximize` is latched. At the `RestartGx` commit it makes a windowed client borderless over the monitor, as the reference's window rebuild does (`0x58cf10`).
- The rest are registered with no effect, and each row says why: `useWeatherShaders` (snow's fallback leg is a separate CPU draw), `lod`, `M2UseShaders`, `M2UsePixelShaders`, `movieSubtitle`, `gxTripleBuffer`, `gxCursor`, `gxRefresh`, `DesktopGamma`.
- `cvars.rs` is split into the runtime and the table.
**#329: Spell Detail.** benilla's options window gains 1.12's Spell Detail Level slider on the Graphics page, over `spellEffectLevel` on the stock 0-2 grid.
**#583: `FrameXML.log`.** Load failures now go to `benilla-config/Logs/FrameXML.log`, as the reference's go to `Logs\FrameXML.log`, and no longer to chat.
- The log uses the reference's lines ("Couldn't open", "Error loading", "Couldn't parse XML in") and its banners.
- A UI load that reports something rewrites the file, and a runtime `LoadAddOn` appends to it (`0x490187`, `0x46aac0`).
Verified:
- New tests for each switch, the maximize latch, every box's stored value with no unknown-CVar warning, the Spell Detail row and the log file's rewrite and append, each failing on main.
- A region dump of the options window shows the new row, and the rows below it moved 35 px, and nothing else.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run unticked Full Screen Glow in the stock window, which turned the glow pass off and saved `ffxGlow = "0"`, and a missing addon file produced its four log lines.
Built on macOS; no platform seam touched.
Fixes#549Fixes#329Fixes#583🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stock video options window (`OptionsFrame`, off the player's chain) now opens, saves, restores defaults and cancels with no Lua error, as on 1.12.1.
- The CVars it reads are registered at the reference's defaults: `SmallCull` "0.04" (`0x68854a`), `shadowLevel` "1" (`0x6885bc`), `baseMip` "0" (`0x6887aa`), `DesktopGamma` "0" (`0x402d4d`), `spellEffectLevel` "2" (`0x688900`), plus `doodadAnim` and `texLodBias`, which the new getters read. `SetWorldDetail` keeps `SmallCull` in step (`0x488e56`), and the wrong comment that called the getter its only reader is gone.
- `spellEffectLevel`'s handler (`0x689510`): the value is clamped to 0-2, "Spell effect level set to %d." is printed, the particle emission scalar is set to 0.33/0.66/1.0, and the shard emitter's rate follows the level (`0x6eb95e`).
- The 21 functions #337 lists: the six video getter/setter pairs (`0x488ec0`-`0x489300`), `GetNumFrames`/`EnumerateFrames` in creation order (`0x705f00`, `0x705f60`), and the client queries, including `GetDebugStats` and `OpeningCinematic`'s empty `CMSG_OPENING_CINEMATIC` (0xf9, `0x48c8c9`).
Verified: a headless test walks the stock window's 18 boxes and 9 sliders, then Okay, Defaults and Cancel, with no error (it fails on main at `OptionsFrame.lua:146`), plus unit tests for each binding and handler; 12 mutations each fail a test. A live run opened the window, saved, restored defaults and cancelled with no Lua error. `scripts/gates.sh` green. Built on macOS; no platform seam touched.
Fixes#548Fixes#337
Refs #329 (benilla's own options window has no Spell Detail row yet)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A dev build finds its project folder, where its `WoW` link, `benilla-config/` and `.probe-identity` live, from its launcher, not from a benilla crate's own `CARGO_MANIFEST_DIR`. For a crate built on top of benilla, where benilla is a git dependency, that manifest dir is cargo's checkout of benilla.
- `benilla_buildstamp::emit()` stamps `BENILLA_PROJECT_DIR`, the launcher's workspace root (the nearest folder up holding `Cargo.lock`), only when the launcher's `dev` feature is on. A player build stamps it empty and carries no source path. The shims pass it in as `BuildId::project_dir`.
- `benilla_formats::project_folder` is the one home for the folder. Each entry point records the stamped folder once, before anything resolves from it. The install's dev rung, the state folder (which keeps its step from a linked worktree to the primary checkout) and the probe identity all read it. With nothing recorded (a test, an example, a tool) it is benilla's checkout, as before, and in a player build it is `None`.
- Live `assets/ui` and the addon corpus stay on benilla's own source.
- The example launcher's header now says a dev build of a crate on top keeps these in its own folder.
- The comment above the stamp's resource names the build banner, the stamp's reader beside the panel footer.
Fixes#494Fixes#493🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A crate built on top of benilla now adds its own Bevy plugins through `benilla_app::run_with(build, extend)`: `extend` gets the client fully built, after the game plugins, the asset loaders and the `ExtractSchedule` executor flip, and before the probe fleet, which sees what it adds. `run` is `run_with` with nothing added, so the stock client is unchanged.
- A client started through `run_with` carries `extended` in its build id (`BuildId::summary`), so the startup banner, the crash report and the debug panel's footer all say so, and a report from an extended client reads apart from stock. The entry point sets the flag; a launcher leaves it to `..Default::default()`.
- `crates/benilla-app/examples/extended_launcher.rs` is a launcher with a plugin of its own, compiled by the gates. Its header is the shape a crate of its own copies: the manifest (benilla-app without default features and `dev` forwarded, the workspace's bevy, and the root's `[patch]` and `[profile.dev]`, which cargo reads from the root manifest only), the build script's stamp, and a `Cargo.lock` started from benilla's.
- `tests/launcher_shape.rs` holds that documented manifest to the root `Cargo.toml` and the `benilla` launcher's, so a patch or profile added later cannot leave the documented shape building a client that is not benilla's.
- `docs/CONTRIBUTING.md`: a feature 1.12.1 does not have lives in its own crate on top of benilla, started through `run_with`.
Fixes#483🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>