Commit Graph
227 Commits
Author SHA1 Message Date
gromandClaude Opus 5 33fc0c2023 Froxel volumetrics: the haze stops painting shadowed air the colour of sunlit air
`volumetricFog` gains tier 3, "Volume", with a "Shadowed Air" dial beside it; the Ultra
rung of Lighting Quality takes it. A view-space 160x90x64 froxel grid — a fixed BUDGET,
so every resolution costs the same — marched one compute thread per screen COLUMN, front
to back, with a reprojected eight-frame history. Inject and integrate in one pass, because
a froxel past the last shadow cascade has no evidence either way and the honest answer is
the mean of what the rest of its column could see; a thread that owns the column can carry
that and a froxel-per-invocation dispatch cannot.

It publishes two things:

- **the lit share** — how much of the air in front of each pixel the sun actually reaches,
  weighted by each slice's own contribution to what the eye sees through it.
  `volumetric_fog`'s haze multiplies its colour by it. The haze below this rung is the
  reference's: a zone colour mixed in by distance, the SAME colour whether the air it tints
  stands in open sun or a mountain's shadow, and no screen pass can tell it otherwise
  because a shaft march only ever ADDS light. On an Elwynn overlook at noon, 74% of the
  frame is air the sun does not fully reach.
- **the lamps' in-scatter**, free once a volume exists: the phase lobe evaluated at every
  point of the air rather than once per ray at its closest approach, attenuated by the
  medium it crosses and strengthened when the medium is. `lampFog`'s analytic halos retire
  into this solve at tier 3, and that row still chooses which lamps and how many. The gain
  is derived, `0.9 / density(clear night)`, so the two lanes match on a clear night. At the
  Goldshire inn, isolating each by rendering lampFog 0 against 2 on both (the bases are
  byte-identical, which is what makes it a comparison): the volume's are 1.30x and wider.

The sun's SHAFTS deliberately stay on `volumetric_light`'s per-pixel march, and that is a
measurement. A volume bands in hard horizontal steps at 160x90, 320x180 and 534x300 alike,
because one froxel column hands the same value to about a hundred pixels and a canopy's
shadow is finer than any grid worth paying for. Spreading each column's samples across its
own screen footprint (R2 down the column) turns the steps into a gradient and is worth
knowing for any volume, but the march still wins on detail. docs/GRAPHICS.md has the sweep.

Safety is one number: fully lit air multiplies by EXACTLY 1, so at Shadowed Air 0 the lane
renders byte-identical to High over all 1,440,000 pixels with every pass still running.
Getting that exact needed `1 - (1-floor)*(1-lit)` and not the `mix(floor, 1, lit)` it
equals — `mix` is a fused multiply-add and lands a last ulp below 1.0, which moved five of
nine tier 0-2 control pairs with the rung switched OFF. The shipped floor, 0.85, is
deliberately half the depth of the first one tried, and the dial reaches twice it.

Control: nine tier 0-2 pairs against the previous build, five byte-identical and four
differing by one or two PIXELS out of 1,440,000 at 1-2 of 255 — every tier-0 pair identical,
and tier 0 is the tier that switches the fog pass off, so the remainder is the fog shader's
instruction schedule changing because the shader did. Cost is under the noise of both
instruments: the frame probe puts Volume inside High's own run-to-run spread, and the GPU
pass timer puts `gpu_other` at 0.75 -> 0.76 ms.

Also recorded: `water-lake` leaves the deterministic row once shadows are forced on — it
differs from a build of ITSELF by up to 88/255 on 0.03% of pixels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-28 06:40:48 -06:00
gromandClaude Opus 5 11dda4f9cf Two cheap lanes: RCAS sharpening, and a field of view a player can move
Both were named in docs/GRAPHICS.md as nearly free and neither had been built.
They are unrelated to each other; they land together because each is small.

## Sharpening (`sharpening`, 0 Off / 1 Low / 2 Medium / 3 High)

RCAS, FSR 1.0's sharpening half taken on its own. It does not resample, so it
needs none of the upscaler's motion vectors, jitter or history — which is the
whole reason it can land while §7 cannot. One 5-tap cross; the lobe is not a
constant but the widest one that cannot push the result outside the ring's own
range, solved per channel against a peak of 1.0 and taken as the most
conservative of the three. Flat regions get nothing and nothing rings.

The seat is most of the design. `Core3d`, after `FfxGlowLabel` and before
`Node3d::Bloom`:

- After the glow, not before. `FfxGlowNode` builds the blur chain the glow and
  the underwater haze read, and a sharpened bloom source is how an unsharp mask
  turns a lit window into a ringed halo.
- The world view only. The world's last reader is the FFXGlow combine drawn
  inside the UI camera's main pass, which binds `main_texture_view()` at
  node-run time per frame, precisely so a `post_process_write` upstream lands.
  The interface is drawn afterwards into a different target — and sharpening
  text is the one thing this must never do.
- Every tap saturates, as the combine's own scene read does one pass later: the
  limiter is solved against a peak of 1.0, so a super-white tap would invert
  the bright side's sign. Nothing is lost, the blur chain already took its copy.
- No denoise term. The deband dither it would exist to suppress is added
  downstream, in the combine's exit.

Measured, 600 frames: Redridge overlook 2.95 -> 3.02 (Medium) -> 3.01 (High);
Goldshire inn 3.46 -> 3.51 -> 3.29. Inside the noise, the inn reading *faster*
at High being how you know. CPU flat. What it buys: 3.8% / 19.9% / 48.6% of the
frame moved by more than 2/255.

**The first run hit the trap this repo already has a note about.**
`max(4.0 * mx, 1e-5)` where `mx` is a `vec3` is `error: inconsistent type
passed as argument #2 to max`. naga refused the shader, the pass drew nothing,
and the capture came out looking perfectly normal. One `grep -ciE 'error:'` on
the capture log answered it before a single image was diffed.

## Field of view (`fieldOfView`, vertical degrees, 40..70, default 45)

Field of View on the Graphics page under Render Scale, live as you drag.

It is the BASE lens, not a fourth factor. `player::scoped_view` already
multiplied two things in and neither is a field of view: SuperWoW's `FoV` is
the reference's `[camera+0x40]` as a ratio, the spyglass is a zoom off normal.
Both answer "how much wider than normal"; nothing a player could move answered
what normal IS, and the answer was a `const` in `benilla-world`. So the term
order is `base × FoV × zoom`, and an addon writing `FoV` still means the same
fraction of the lens the player chose.

Vertical degrees, not horizontal, which keeps the reference's Hor+ framing: a
fixed vertical lens widens horizontally as the window does, so an ultrawide
player gains picture at the sides instead of losing it off the top.

The default is `CAM_FOVY` to the bit — `45f32.to_radians()` and `FRAC_PI_4` are
both `0x3f490fdb` — which the defaults-mirror test now asserts on `to_bits()`
rather than against a tolerance. An ulp of projection is a pixel in a capture
diff, and a row that reframes the world for a player who never opened Options
is not a row, it is a regression.

## Both lanes off: byte-identical

Against a `git stash`ed build of the same tree: `overlook-noon` and
`water-lake` identical, and `fieldOfView=45` written explicitly identical to
the row never touched (40 and 65 move 97.6% and 99.3% of the frame).

`inn-noon` is excluded WITH PROOF rather than quietly: two runs of the *same*
binary differ on 37.8% of their pixels by up to 199, because that scene's flame
emitters do not reproduce. Its own before/after delta is 0.00% of pixels at a
maximum of 1. A scenario that cannot reproduce itself cannot be a control, and
the number to check first is always the baseline against itself.

## Seats

Six Options edits each, per the recipe, including the silent one
(`OPTIONS_PAGE_ROWS`). Sharpening is a dropdown on Advanced Graphics with the
other screen effects and reuses the existing `qOff/qLow/qMedium/qHigh` labels;
Field of View is a `raw`-format slider on Graphics, whose label is literal
because that page is not localised. Preset ladder: `sharpening` takes
`ambientOcclusion`'s shape (`0 0 1 2 2`) because a look change starts at Medium
in this file's convention; `fieldOfView` is on no rung at all, because it
changes how the game feels to play and that is the player's call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-27 16:56:54 -06:00
gromandClaude Opus 5 280c2e886f Planar water: the mirror works, and ladders in at waterQuality 3 (Ultra)
The starburst was three defects, and none of them was the oblique
projection I blamed it on in the last commit. What found them was looking
at the mirror's own render target instead of reasoning about the
composited frame — the water reads it through a fresnel weight, a rippled
UV, a plane-match band and an SSR march that paints over it, so from the
outside a broken mirror and a broken water shader look identical.
WOW_MIRROR_DUMP=1 now writes that target beside the capture. (Bevy's own
save_to_disk cannot: try_into_dynamic knows four 8-bit formats and answers
UnsupportedFormat for Rgba16Float, logging and writing nothing.)

- The oblique row's scale. Replacing the near plane with the water plane
  costs the FAR plane: `z >= 0` stops meaning "nearer than CAM_FAR" and
  becomes `depth >= a*d`, a plane through the eye tilted by a. Lengyel's
  own scale puts that plane exactly through the frustum's far corner, and
  the sky dome is pinned just inside the far plane, so it straddled the
  boundary and was sliced along its own tessellation. Scale by the
  supremum of d/depth over the frustum instead, with 2% of margin.
- The water surface lay ON its own clip plane. This was the starburst and
  it was fatal rather than untidy: depth now means "distance above the
  plane", so a fragment at d=0 reads z/w=1, the nearest value reverse-Z
  has, and beats every other surface in the frame. The mesh is
  wave-displaced, so its triangles straddled the plane instead of lying in
  it, and each was clipped to a sliver that kept the winning depth.
  SURFACE_CLEARANCE lifts the plane a yard: the swell sums to 0.34 and the
  probed height answers for the surface under the eye, not this fragment.
- The mirror was stored upside down. A true reflection is what a camera on
  the far side sees with its up vector reflected too — an improper basis
  no Transform can hold. The honest right-handed camera sees the same
  surfaces through the same frustum and stores the image flipped, so the
  shader samples 1 - v; that costs no winding flip, so backface culling
  stays correct. It read as a lake reflecting the ridge the RIGHT way up.

Measured: Lake Everstill 2.87 -> 3.36 ms mean over 600 frames (+17%), the
Stormwind canal inside the noise, CPU +0.2 to +1.1 ms. It is the first
lane in docs/GRAPHICS.md that spends main thread, so it is a rung of its
own above High and the default stays 2.

Seated as a setting, per the standing rule that no graphics lane hides
behind an env var: waterQuality gains tier 3, the Ultra preset takes it,
and the Advanced Graphics dropdown gains a fourth entry in both locales.

With the lane off, both scenarios are byte-identical to the build before
it — which took two findings of its own:

- The system could not live in Update at all. `&mut Transform` on a camera
  is shared with everything that moves anything; declaring it put 59
  undeclared orders into Update and tripped the schedule ratchet, which is
  what kept this behind an env var. It belongs in PostUpdate with the
  skybox and the billboards, after propagation and before visibility,
  where it also reads the eye's settled pose instead of a frame-old one.
  (The comment blaming ResMut<Assets<Image>> for those 59 was wrong —
  dropping the asset handle changed the count by zero.)
- A system that runs and returns on its first line is still not free.
  Holding &mut Camera and &mut GlobalTransform for a frame is enough to
  change which undeclared orders the executor picks: the canal moved 0.75%
  of its pixels by up to 96/255 with the lane switched off. So the whole
  system sits behind a run condition rather than an early return — and the
  same effect killed a .before(UpdateDirectionalLightCascades) order that
  was correct in itself.

PostUpdate's ambiguity ceiling goes 466 -> 495 with the reason (the pairs
are structurally impossible: nothing else in the schedule names the mirror
camera); Update's ratchets down 4973 -> 4952.

Also restores scripts/gates.sh's `where` example, which had gone missing.
Its absence was silent — cargo's error went to /dev/null and `|| true`
made it read as "no install" — so the memo key and the player-tests gate
saw none even when WOW_DATA was set.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-27 09:35:02 -06:00
gromandClaude Opus 5 77fce9210e The water already reflected the scenery; it was switched off
`docs/GRAPHICS.md` said we had refraction and foam but no mirror. That was
wrong. `waterQuality` 2 has carried a depth-marched screen-space
reflection of the scenery all along (`enhanced_water.wgsl`, "HIGH"), with
bisection refinement against the real scene depth rather than the fixed
probe its WarcraftXL source took. What was missing was the switch: the
registered default was Enhanced (sky only), and of the five lighting rungs
only Ultra reached High.

The line holding it back said exactly what it was waiting for —
"Mirror reflections stay opt-in until their cost is measured." So I
measured it, `WOW_FPS_PROBE=300`:

                  Classic   Enhanced   High
    Lake Everstill  1.91 ms   1.84 ms   1.90 ms
    open ocean      1.89 ms   1.88 ms   1.89 ms

Every tier is inside the run-to-run noise — Enhanced lands FASTER than
Classic on the lake, which is how you know you are reading noise rather
than a cost. What it buys is not noise: the far bank and the mountains
appear in the water, 18.5% of the lake frame and 35.1% of the ocean frame.

So High becomes the default and the High rung. That is a weld across four
places the tests hold together, and each one bit in turn and was fixed as
it asked: `REGISTERED`, `VideoConfig`'s literal, `WaterQuality::default`,
and the rung itself — the High rung IS the registered defaults, which
`the_high_preset_is_the_registered_defaults` states and enforces.

Low and Medium keep Enhanced deliberately, and that needed saying in the
one test that assumed every rung between Off and Ultra sits AT its
default: free on the machine it was measured on is an argument for the
default, not for the rung whose whole job is hardware that cannot afford
what this one can. `water_and_lava_observers_clamp_and_water_high_is_opt_in`
asserted the old decision in its name and its body, so it is renamed and
now pins the pairing the other way: High reflects, Medium does not.

The honest remaining gap is written into the doc rather than claimed as
done: SSR gives up when the ray leaves the screen and fades toward the top
edge, which is where a shallow-angle lake wants its far bank. A planar
pass would fill that — and it is the first item on the list that draws the
scene twice, so it costs main thread and gets priced against item 1's
view-distance table before anyone builds it.

Verified: ALL GATES GREEN (492 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-26 19:26:46 -06:00
gromandClaude Opus 5 88b6998453 Auto exposure: dark rooms read at their own level
The programme's item 1 was terrain horizon occlusion, ranked first on the
reasoning that it would make the frame cheaper on the thread that is
actually full. **I measured it before building it, and the premise does
not hold for this renderer**, so this is item 2 instead. The numbers are
in `docs/GRAPHICS.md`: cutting the view distance from 777 yd to 177 —
removing four fifths of the visible world — saves 0.44 ms of a 3.6 ms
frame, p95 is flat at 6.4 ms at every distance, no main-world system in a
steady vista exceeds 0.12 ms/run, and the model cull already takes 6,016
submeshes to 29 drawn. Horizon occlusion can only remove the fraction of
that which sits behind a hill. The reference needed it on a 2004 CPU with
no GPU-side culling; we do not.

What the same harness said about item 2 was the opposite. Before anything
was written: the Elwynn vista renders at a gamma median of 87.6/255 with
1.4% of its pixels under 32, the Goldshire inn at noon at 31.9 with 50.1%
under 32, and the inn's kitchen at **12.8 with 94.5% under 32**. That is
not a dim room, it is a black screen — and the reference has the same
problem, because it has no metering either.

A histogram-and-reduce over the HDR view target, smoothed in the log
domain, applied ahead of the shafts, bloom and glow chain so FFXGlow's
blur — which is built from the scene — sees the same exposed image
everything else does. Two departures from the textbook version, both
forced by what this renderer's pixels mean (the world is drawn in the
reference's gamma convention, which the combine clamps to 1.0):

- The gain has a **floor of 1.0**: it only ever brightens. Aiming at the
  exterior's own measured level is what lands it at 1.0 outdoors with no
  special case — the vista's median moves 87.6 -> 87.5, and 0.12% of its
  pixels with it. Daylight is the one look this client is measured
  against and nothing here was worth moving it.
- It has a **ceiling**, which is the tier. The kitchen meters to roughly
  24x; uncapped it comes out as daylight, which erases the room instead of
  making it readable. Subtle and Full are 2x and 4x: "a dark room you can
  see in" and "one you can read in", neither "a lit room".

Measured after: inn noon goes 50.1% -> 4.6% of pixels under 32, the
kitchen 94.5% -> 40.3%, and the vista does not move. **+0.08 ms of GPU**,
nothing on the main thread.

The calibration is the one thing to get right if this is ever re-derived,
and it is written down because I got it wrong first: the aim level is the
level of the scene THIS PASS SEES, not of the screenshot. The glow adds
`w·blur²` downstream, so the finished vista log-averages to 0.0931 linear
and the pass's own input to 0.060. Calibrating against the former aimed
the lane 1.5x high and brightened the daylight exterior it exists to leave
alone — which the vista capture caught immediately.

Two rows on Advanced Graphics, both locales, the blend greyed while there
is no gain to blend; on the Graphics Preset ladder from Medium, in the
shape the other post lanes take. Classic keeps it off, as the reference
has no metering at all.

Verified: ALL GATES GREEN (550 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-26 18:05:43 -06:00
gromandClaude Opus 5 50fd1f158e The interior lighting rig gets its ten rows, and the audit closes
The last ten CVars `video::on_cvar` answers that a player could not reach.
With these the sweep is done: every graphics and video row the observer
takes now has a seat, bar the three that are not settings — `frillDensity`
(World Detail already drives it, and two dials on one knob is worse than
none), `nearclip` (the projection near plane: changing it is a bug, not a
preference) and `interiorDebug` (a visualiser).

Seven shape the room — the light budget, the ambient floor under every
indoor surface, the indoor daylight floor, the bounce gain, the reach past
each fixture's authored window, the share of painted-in lighting kept
where nothing reaches, and the room gate — and three are the indoor shadow
budgets.

Named for what they do to a room, not for the quantity they scale, because
the quantity is the renderer's word and the room is the player's: "Unlit
Room Floor", not "interior bake floor"; "Lights Stay in Their Room", not
"room gate". Each description says what the setting is FOR rather than
what it multiplies, and three of them carry the thing a player would
otherwise misread:

- `interiorAttenScale` at 0 is not darkness, it is the authored window
  switched OFF, so every fixture reaches the whole room. A left end that
  looks like "no light" and means "all light" needs saying.
- `interiorRoomGate` is the one setting whose job is diagnosis: a room the
  gate leaves on ambient alone looks exactly like a bug, and one click
  tells the two apart. That belongs in the options window and not only in
  `/console`.
- `interiorShadowEntityRate` is the only row in the indoor shadow block
  that actually costs frame time — neither count dial above it moved it,
  which is why it exists as its own row — so its description says to lower
  it first.

Two grooves deliberately offer less than their CVar's clamp, as
`RowRenderScale` already does (50-200% inside a 0.25-4 clamp): ambient and
indoor daylight run 0..0.1 of a 0..1 clamp, because the shipped value is
0.015 and past about 0.1 a room is not lit, it is fogged. `/console`
still reaches the rest. One offers less than its *registration* says:
`interiorShadowDynamic`'s help line reads 0..16, but the observer raises 0
to 1 and clamps to `MAX_TORCH_DYNAMIC` (8), so a groove to 16 would be
seven stops that all mean 8 and a zero that means 1.

The gating is two-level: every row needs the live-fixture lane, and the
three shadow budgets need the shadows on top of it. A budget row left live
while its shadows are off is a dial that does nothing, which is how
settings earn the reputation these ones had.

Russian for all ten rows and their descriptions is written to match the
register of the column already there; a native reader would still improve
it.

Verified: ALL GATES GREEN (346 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-26 16:37:01 -06:00
gromandClaude Opus 5 2d1dbd5639 Seven more graphics settings get a seat: the filter pair, MSAA, the shadow rig
The sweep behind this: every CVar `video::on_cvar` answers, against every
CVar the Options window names. Twenty of fifty-five had no seat at all, so
the only way to them was `/console` — which is not a setting. Seven of
them here; the ten interior-lighting rows follow.

**Graphics page.** Trilinear Filtering under Anisotropic, because they are
one policy: aniso at 2 or more writes filter mode 5 and trilinear writes
mode 4 only while that bit is clear (`0x688d0d`), so the row is GREYED
while anisotropy is on — not hidden, and not rewritten. It keeps the value
a player set, and turning anisotropy off hands it straight back. The
gating reads the STAGED anisotropy where there is one, since both rows
defer: otherwise a player who moves one and has not pressed Apply sees the
row it is about to gate stay live for the rest of the visit.

Multisampling beside Vertical Sync and Resolution, the latched `gx*` rows
it commits with. 1.12 had this row and we did not. Backed by the index
verbs its own dropdown used (`0x48c580` / `0x48c640`) rather than by
`gxMultisample`, because picking a format writes the colour and depth rows
with it as one record — and it rebuilds its list on open, like Resolution,
since at OnLoad the engine has published no formats yet. Only the sample
count is named: the colour and depth halves of each triple describe our
swapchain and steer nothing, so offering them would offer a choice that
does not exist. Its description says plainly that multisampling does less
here than in a modern game, because it misses the alpha-tested cutouts
most of this game's foliage and fencing is made of.

**Advanced Graphics.** The shadow rig's four unreachable rows — Soft
Shadow Edges (the 9-tap gaussian against the card's own 2x2; Bevy's third
mode is deliberately not offered, since it only resolves under TAA this
camera does not run), Shadow Caster Range, and the two update-rate caps —
plus Torch Shadow Darkness beside the softness dial it shares a shadow
with.

The rate caps needed their own readout. `0` there is **every frame**, the
opposite of what it looks like, so a bare "0 per second" would read as the
shadows being switched off — which is what the two toggles above them are
for. They now say "Every frame", in both locales.

Gating is per-lane where the lane is what matters: each rate cap is dead
only while its own lane is, while the filter and the reach ride the
cascade and need just one sun lane left. Russian for all five rows and
their descriptions is written to match the register of the column already
there; it would still benefit from a native reader.

One thing worth recording for the next row: `OPTIONS_PAGE_ROWS` has
positional contracts in its tests, and the list's own comments say
"appended so the other rows keep their indices". It drives refresh, search
and Restore Defaults — never layout, which is the frames' own anchors — so
appending costs nothing and seats each row where it belongs.

Verified: ALL GATES GREEN (322 s); `benilla-app --lib` against the install
at the exact 269-failure baseline, set-identical by `comm`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-26 16:22:20 -06:00
gromandClaude Opus 5 1c786b7ecb Anisotropic Filtering takes its seat on the Graphics page
The CVar has been registered and read since the filter policy landed, and
last commit put it on the Graphics Preset ladder — but a player had no way
to reach it. The reference's own `OptionsFrameSliders` declares the row at
index 7; ours left that index a hole, so the only route to anisotropy was
`/console anisotropic 16`, and a player whose preset reads Custom (the
seeder never overwrites one) never met the setting at all.

Seated where the reference's own slider order puts it: after Brightness,
before Weather Intensity. Three things it does differently, each stated
beside it:

- The groove runs 1 to 16, the CVar's own `ANISO_RANGE`, not the stock
  table's 1 to 4. That ceiling was the reference *slider's*, not the
  CVar's — its callback parses 1-16 (`0x689110`) — and both the High and
  the Ultra rung ask for 16. So the row carries literal bounds and stays
  out of `BENILLA_GRAPHICS_SLIDER_ROWS`, exactly as `RowRenderScale` does
  for a CVar the stock table never had, which leaves `OptionsFrameSliders`
  verbatim for any addon that reads it.
- `1` reads out as "Off", not "1x": the reference writes filter mode 5
  only at 2 and above, so 1 is the feature off and not its weakest stop.
- The description is ours. 1.12's own OPTION_TOOLTIP_ANISOTROPIC describes
  a four-stop slider, and this one is sixteen.

Deferred to Apply, as Vertical Sync and Render Scale are: a sampler is
baked into each `Image` at load, so the write cannot reach a texture that
is already resident. The tooltip says so in the row rather than leaving a
player to wonder why the ground did not change — the same thing the
reference's UI means by "enabled upon restart".

Verified: `benilla-app --lib` at the exact 269-failure data-gated baseline,
set-identical by `comm`, with the new row's own test and the two census
counts (tooltip keys, raised plates) moved 116 -> 117 as the row asks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-26 14:39:25 -06:00
andi79h-claude 73f4bf808d The target-of-target rows only write their globals, as 1.12's options do (#2150)
1.12's options declare Show Target of Target as a bare uvar
(UIOptionsFrame.lua:65) and the mode is read on save
(UIOptionsFrame.lua:365); neither calls TargetofTarget_Update. The frame
follows from TargetFrame_OnUpdate and TargetofTarget_OnUpdate on the next
frame (TargetFrame.lua:492-501). Our rows called it as their applyFunc,
with the row as `this`; the function refreshes `this`'s debuffs
(RefreshBuffs(this, 0, "targettarget"), TargetFrame.lua:538), so with a
target's target up the switch raised "BuffFrame.lua:281: attempt to index
local 'debuffBorder' (a nil value)" (seen on a Linux client).

Verified: the_target_of_target_switch_leaves_the_frame_to_its_own_updates
(with a target's target up: no error, the frame shows after one tick on
and hides after one tick off) fails with the applyFunc put back, with
exactly that error, and passes without it; the options and tot_frame
tests pass on a deDE install; cargo test -p benilla-app --lib 5145,
clippy -D warnings, fmt. Built on macOS only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4d6f9ecc8a0d99e6d3ecf7fa4a8da229ebad92c2)
2026-10-25 23:26:20 -06:00
gromandClaude Opus 5 0e02401325 superwow: CastSpellByName at a unit, FoV, NameplateMotion, the raw log
The DLL publishes its own **usage strings**, in one block beside the
names it registers, and reading them turned up four things this file had
wrong — three of them bugs rather than gaps.

`CastSpellByName(name [, "unit"])`. A unit ID, which is what every real
call site passes and what `Model::unit` already resolves. It used to fall
through to the ordinary queue, which casts at the player's OWN selection
— a silent wrong target at six sites in grom's corpus: pfUI's /castfocus
(a GUID) and /castmouseover (a token), Decursive's cure-at-guid, BigWigs'
spell requests and two encounter scripts. Each asked for a unit and got
whatever was targeted. The stock `onSelf` meaning keeps its behaviour:
only a string that resolves to a live unit takes the new path.

`CombatLogAdd`'s second argument names a FILE, not the event. There is a
third log literal in the same pool — `Logs\WoWRawCombatLog.txt` — and it
is the GUID-form log, the same lines RAW_COMBATLOG carries as arg2.
`ui_chat/logging.rs` keeps it beside the ordinary one now, opened by the
same `LoggingCombat`, which also makes the arg2 composition worth paying
for when nothing is registered but the log is on (`wants_raw_form`). My
first reading, "also fire the event", was an inference from the name
alone, and the second literal settles it.

`FoV`'s hold-back is resolved, and the answer was already in the tree.
The row was held because 1.57 looked irreconcilable with CAM_FOVY's
0.785. It is the same number: 1.57 is π/2, the reference's own
`[camera+0x40]` default of 90° in radians, and that field is not a
vertical FOV at all — `scoped_view.rs` already divides by it as a
DENOMINATOR, because what it expresses is a ratio. So upstream's own
default is this client's existing lens by construction, which is what the
test asserts; the DLL even registers the literal `1.57079`. Live, where
upstream's panel says "Requires UI Reload".

`NameplateMotion` asked for an investigation that was also already
answered: benilla spreads plates with the reference's own SmartScreenRect
solver. 0 (Overlap) bypasses it, 1 (Default spread) is it; 2 and 3 are
the mod's own algorithms and fall back to 1, a documented silent-equal
rather than a refused row.

Also: `GetTradeSkillItemLink(index[, spellLink])`, the only way to link a
recipe that makes no item — an enchant, whose product_item is 0 and which
answers nothing at all on the stock surface. And four stored-only rows
(SelectionCircleStyle, UncapSounds, HealingText, InterfaceVolume), each
with its own reason, so SuperAPI's options panel round-trips.

Three ghosts retired: outline/shadow text, force-show-auras and XP text
are in no part of the DLL's string pool, nor is NameplateVerticalSpacing.
They came from the early wiki reading, not from the binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-25 13:53:40 -06:00
gromandClaude Opus 5 85cd750a4e vplates: no plate for a critter, by the client's own no-TAB flag
grom asked for the name and level over every rabbit to be gone. pfUI cannot do it: its option is
"Hide Healthbar On Critters", and the branch it takes is `level:Show() name:Show() health:Hide()`
— bar hidden, name and level kept, a label floating in empty space.

The reason it shows up here and not on his real client is our own compat. That pfUI branch needs
`UnitCreatureTypeID` on a plate GUID, which needs the `NAME_PLATE_UNIT_ADDED` → `C_NamePlate` →
`UnitGUID` chain; stock 1.12 has none of it, so the check returns nil there and a critter gets an
ordinary plate nobody notices. We answer all three, so the feature wakes up and strips the bar.

So the gate goes where the plate is decided. It keys on `CreatureType.dbc`'s **no-TAB flag**
rather than on the id 8: that bit is the client's own mark for a creature the player does not
mean to interact with, it is set on Critter alone in the 1.12 data, and `target::scan`'s TAB
gate already reads it — one notion of "critter", from the data, with two consumers. An
unresolved type keeps its plate, as the scan treats an out-of-range index as targetable.

A deviation, and `nameplateShowCritters` says so: the reference's ladder (`0x60f600`) has no
creature-type test at all. Off by default, because the floating name is the thing nobody wants;
`SetCVar("nameplateShowCritters", "1")` restores vanilla. Removing the plate leaves nothing
behind — the overhead-name path gates a creature on `UnitNameNPC`, which is "0".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
2026-10-16 19:49:41 -06:00
gromandClaude Opus 5 81d96af35d Everwood: the HD/OctoWoW line of benilla, imported onto upstream
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
2026-10-15 15:01:14 -06:00
chairmanpow 706cd717a3 merge: absorb samwhosung/main on top of the upstream merge
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.
2026-10-12 17:24:31 +01:00
chairmanpow 26d714fd1d merge: absorb upstream/main into the Turtle fork
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.
2026-10-12 17:20:45 +01:00
Sam SungandClaude Opus 5.5 7158c58575 Hidden frames defer their layout; GetWidth answers the set size (#973)
A hidden frame resolved and fired `OnSizeChanged` at once, a getter's resize fired at the drain, and `GetWidth`/`GetHeight` answered the rect over scale.

In 1.12.1 hiding sets the deferral flag `0x2` (`0x76ad50`); while set, an invalidate marks the frame pending and passes it to its dependents without a resolve (`0x768178`). Show resolves it first (`0x76ae58`), a getter resolves a pending one and fires `OnSizeChanged` inside the read (`0x7a1980`, `0x76b580`), and an anchor read pulls it for good (`0x7a2fc0`). `GetWidth` answers the set size when non-zero, else the rect span, unscaled (`0x7a1e93`, `0x7a1ee6`).

The deferral is modelled for frames and regions, getters fire inside the read, layout ids mint in handle order (deterministic), and the options window measures its scroll by its edges.

Tests bite on the parent; the verifier is green; stock quads match.

Fixes #948
Fixes #971

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:06:04 +07:00
Sam SungandClaude Opus 5.5 55729baff9 Message fades and scroll ranges run in the walk; OnUpdateModel gates (#913)
Hidden chat frames kept fading their lines, a scroll frame recomputed its range on every read and fired `OnScrollRangeChanged` on every `UpdateScrollChildRect`, hidden or not, and `OnUpdateModel` fired for panes with no size or off screen.

In the reference, each of these runs in the frame's own update slot, only while shown: message frames trim and fade there; a scroll frame recomputes a range its writers marked, stores it and notifies only on a real move. The model paint fires only for a resolved rect whose screen-clamped viewport has size.

Message frames and scroll frames are now walk kinds, the range is stored and marked, and the paint applies the rect and viewport gates. benilla's options scroll measures its body's own overhang.

Tests pin the hidden fades, the stored range and its notify, and the paint gates.

Fixes #182
Fixes #175
Fixes #892

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 00:25:14 +07:00
Sam SungandClaude Opus 5.5 4e0011bdd7 Backdrop pieces are nine Texture regions on the frame, as in 1.12.1 (#905)
A backdrop was data drawn in the frame's own slot, so `GetRegions` and `GetNumRegions` never saw its pieces, and addons that skin by walking a frame's regions missed them.

In the reference, installing a backdrop creates nine Texture regions on the frame: the background on BACKGROUND, then eight border pieces on BORDER, linked into its region list in that order. A resize re-runs their texcoords and colours, and the colour setters tint the background or the border alone.

The pieces are now real regions, drawn through the region path, so edges draw over another frame's BACKGROUND at the same level. `<Layers>` and `<Backdrop>` load in document order. A region leaving its parent empties any Button, CheckButton, StatusBar, Slider or ColorSelect slot holding it, and a dead layout node now seeds only its dependents.

Tests pin the order, colours, resize, teardown and stock quads.

Fixes #883

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:10:33 +07:00
pkuzic f545081e9f MONKEY (planar water): planar replaces screen-space reflections - the Water Reflections option is gone
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.
2026-10-09 19:06:59 +01:00
pkuzic 52bc8e2f94 Merge branch 'gfx/1009-water' into gfx/1009-upstream 2026-10-09 17:34:52 +01:00
pkuzic 8279d3b92d MONKEY (planar water): SSR stretch fixed; planar water reflections (Water Reflections: Screen Space / Planar)
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.
2026-10-09 17:34:52 +01:00
pkuzic ee6c76b988 Merge upstream main (samwhosung/benilla c2fc3cf3) into the graphics line
Conflicts: capture fixtures/scenarios (both sides appended: kept both), options_tests
row counts (upstream 80->81 rows: ours 100->101 and 99->100), docs/MAP.md regenerated
with scripts/genmap.sh.
2026-10-09 12:50:37 +01:00
198b6cd894 fix: overhead names carry the player's PvP title, as 1.12.1 does (#792)
- capture: a name-rank scenario shoots a ranked player's overhead name line
- Overhead names carry the player's PvP title under UnitNamePlayerPVPTitle, as 1.12.1 does (#550)
- review: bundle the rank-title inputs; doc the snapshot and rank-19 limits
- nameplates: read rank strings per VM; test the composed name line
- pvp: correct the rank-title docs; convert the credit team without a cast
- Fix the titles row comment; name-rank reuses WOLF_POS, cites :900
- rank strings: the honor feed reads them off the VM; nameplates only reads the value

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-06 16:31:56 +01:00
622e1c3e7a fix: overhead names carry the player's PvP title, as 1.12.1 does (#792)
- capture: a name-rank scenario shoots a ranked player's overhead name line
- Overhead names carry the player's PvP title under UnitNamePlayerPVPTitle, as 1.12.1 does (#550)
- review: bundle the rank-title inputs; doc the snapshot and rank-19 limits
- nameplates: read rank strings per VM; test the composed name line
- pvp: correct the rank-title docs; convert the credit team without a cast
- Fix the titles row comment; name-rank reuses WOLF_POS, cites :900
- rank strings: the honor feed reads them off the VM; nameplates only reads the value

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-04 11:04:32 +07:00
pkuzicandClaude Opus 5.5 494d232c11 Merge upstream benilla main (b396bbf6) into everwood-graphics
105 upstream commits (v0.2.0 ba7fe6e2 -> b396bbf6) onto the graphics programme.

Conflicts resolved, keeping both sides:
- OptionsFrame.xml: upstream renamed every layer global (Options* -> BenillaOptions*,
  OPTIONS_* -> BENILLA_OPTIONS_*, KeyBindings* -> BenillaKeyBindings*, ...) and moved handlers
  to `this`; the Advanced Graphics page (rows, strings, presets) is carried over onto the new
  names, and upstream's Click-to-Move / Spell Detail rows and nameplate store are kept.
- cvars.rs -> cvars/{mod,table}.rs split: our REGISTERED rows go into table.rs, the preset
  ladder + tests into mod.rs beside upstream's ffx/gxMaximize tests.
- video.rs: lighting arms + upstream gxMaximize/ffx arms; VideoConfig keeps both field sets.
- ffx_glow.rs: SkyDither and FfxSwitches both registered/extracted; dither lane on
  upstream's switched player state.
- dest_fx.rs: our AreaLightPlan radius + upstream's spellEffectLevel shard scale.
- fixtures.rs: seed_perf_crowd + upstream's set_enemy_plates.
- options_tests.rs: tipped/described counts 100/99 (upstream 80 + ours 20/19); our Advanced
  Graphics tests use the renamed globals and `this`.
- game_plugins.rs: Update actionable ceiling 4,999 = upstream 4,980 + 19 ours (measured).
- docs/MAP.md regenerated with scripts/genmap.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:35:23 +01:00
chairmanpow 0b71937624 Options: the era AddOns page, one checkbox per installed addon, then Load out of date AddOns
The era's tenth category was a button in the tree that went nowhere. It now opens onto the
era's own per-addon enable list, rebuilt on show from the AddOn API: one checkbox per
installed addon carrying its `GetAddOnInfo` enabled state, then the Load out of date AddOns
row under it (`checkAddonVersion`, inverted, so checked means "0"). A toggle calls
`EnableAddOn`/`DisableAddOn`, which the host persists to `AddOns.txt` at logout — the
reference's own contract for this screen, where the change lands on the next world entry.

`the_defaults_button_is_armed_by_rows_not_by_a_category` is the invariant that every
category leads somewhere and that Defaults is live on every page with a CVar behind it, and
AddOns is the second category to run its own page after Keybindings. It is exempted from the
`OPTIONS_PAGE_ROWS` half for that reason, and from the Defaults half because an addon's
enabled state is not a CVar: there is nothing there for Defaults to write back.
2026-10-02 13:36:51 +01:00
2ce6a3e881 feat: click to move walks to an out-of-reach target, then interacts (#752)
- Click to Move: with AutoInteract on, a right-click on an NPC, a body to loot or skin, or a GameObject out of reach walks there and runs the verb on arrival
- Click to Move: the arrival scales by the mover's current speed, and a Use still out of range walks again
- Click to Move: a Loot walk stops at the square root of the melee reach
- Put the meeting-stone test's doc comment back on its test
- A GameObject click out of range raises ERR_USE_TOO_FAR unless the walk starts
- Wrap the finish's doc lines at the column limit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
2026-10-02 05:57:47 +07:00
chairmanpow 031acdd8bb Fix TurtleWoW baked NPC atlases (append .blp) plus in-progress GFX work: moonlight, torch depth, water/scene-depth, skybox 2026-10-01 21:40:02 +01:00
pkuzicandClaude Opus 5.5 34d35881c2 GFX: volumetric light (sun/moon shafts) and moonlight
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>
2026-09-29 17:31:51 +01:00
pkuzicandClaude Opus 5.5 72647d4ab0 GFX: volumetric light (sun/moon shafts) and moonlight
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>
2026-09-29 17:31:51 +01:00
Sam SungandClaude Opus 5.5 459e0e90c1 The stock video window's boxes are registered and ffx, ffxGlow, ffxDeath and gxMaximize work; benilla's options gain Spell Detail; load failures go to FrameXML.log (#585)
**#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 #549
Fixes #329
Fixes #583

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:09:02 +07:00
Sam SungandClaude Opus 5.5 0a1f69466c The nameplate verbs write two runtime bits, cleared at world entry, and the setting lives in FrameXML's saved variables, not two CVars (#571)
The nameplate verbs now answer as the reference's (`0x489450`-`0x489480`): they flip bits `0x1` (enemy) and `0x8` (friend) of runtime state, write no CVar, and world entry clears both (`0x401639` → `0x6033c0`, after `VARIABLES_LOADED`). The setting persists only through FrameXML's `NAMEPLATES_ON`/`FRIENDNAMEPLATES_ON`, as on 1.12.1.

- The `nameplateShowEnemies`/`nameplateShowFriends` CVar rows and the host's plate push are gone. A player's saved plate setting in `config.toml` moves once into the saved variables at the first world entry.
- benilla's layer keeps what players see today. Its `UpdateNameplates` fix drops the stock show-then-hide (`UIOptionsFrame.lua:775-776`), and it registers `FRIENDNAMEPLATES_ON` for saving, which the stock file never does (`:180-183`). So plates turned on come back after login.
- With the layer off, benilla behaves as 1.12.1: friendly plates are off after entry.
- The V keys flip the verb and the saved global together. V and Shift-V keep their recorded deviation; #570 tracks moving the binding table onto the stock `Bindings.xml`.

Verified:
- New tests for the verbs, the entry clear, the stock-only and layered persistence across entry and reload, the V keys and the one-time migration, each failing with its change undone.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run with `WOW_GM=off` turned both kinds on, relogged and read both back, from the file too.

Built on macOS; no platform seam touched.

Fixes #552

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 01:21:17 +07:00
Sam SungandClaude Opus 5.5 05e204de10 benilla's layer speaks the 1.12 API alone: handlers take this and no arguments, GetBinding answers the 1.12 list, the Key Bindings page captures through its window, /errors chains the error handler, and the host hooks leave the VM (#569)
benilla's layer now uses only the 1.12 API and the 1.12 Lua forms, and the host hooks it used are gone from the VM.

- **The VM calls handlers as 1.12.1 does.** A handler body is compiled as the chunk itself, so `self` in it is an ordinary global (`0x704c70`). A handler is called with no arguments (`0x705199`). The loader reads no `function=` attribute.
- **`GetBinding`/`GetNumBindings` answer the 1.12 list** (`0x4b7f60`): `HEADER_*` section rows, plus every key bound to a command, with no category column. The stock `KeyBindingFrame` now shows its section headers and both key columns.
- **The Key Bindings page captures keys through its own window**, as `Blizzard_BindingUI` does: the frame's `OnKeyDown` for keys, clicks for mouse buttons, and the wheel. It takes the `Benilla` prefix, so loading the stock binding addon no longer breaks it. The UI pointer feed now hands frames mouse buttons 4 and 5.
- **`/errors` keeps its log by chaining the Lua error handler**, and the stock ScriptErrors dialog is unchanged. Load failures that never raise now print one chat line each at world entry.
- **Slash commands:** `/reload` and `/errors` are `SlashCmdList` rows in the layer. `/convertraid` is gone; 1.12.1 has no such command. The dev commands are host rows in dev builds only, so a player build answers them with the unknown-command line once.
- **Naming:** every global the layer defines is `Benilla`-prefixed. `BenillaBindCapture`, `KeyBindings_OnHostKey`, the three script-error globals and `SubmitChatInput` are removed.

Verified:
- New tests for each rule and each removed surface; 16 mutations each fail a test.
- Region dumps of the Options window (general page, Key Bindings, armed) are identical before and after.
- `scripts/gates.sh` and `scripts/smoke.sh` are green.
- A live run bound a key, a Shift chord and the middle button through the page, and opened `/errors`.

Built on macOS; no platform seam touched.

Fixes #539
Fixes #544
Fixes #407
Part of #171

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 00:12:27 +07:00
Sam SungandClaude Opus 5.5 0f36374445 The core loads the player's own FrameXML.toc in its order, and the boot VM loads no Fonts.xml (#568)
The core now loads the player's own `Interface\FrameXML\FrameXML.toc` off their chain, row by row in its order (`0x6edb90`, from `UI_Init` at `0x48ffed`), instead of benilla's copy of the list, and `benilla.toc` is gone. Order is observable in 1.12.1: event listeners run in registration order, children are kept in creation order, and hit-test ties go to the earlier frame.

- The world VM starts bare: `GlobalStrings.lua` and `Fonts.xml` load as the toc's first rows. The boot VM keeps `GlobalStrings.lua` and the emote tokens, and no longer loads `Fonts.xml`, which nothing reads there (#307).
- A chain without `FrameXML.toc` builds no stock UI and still loads the layer and addons, as the reference does (`"Couldn't open %s"`, severity 2, return unread); benilla reports it as an error.
- Every test kit now loads in the production order, `FrameXML.toc`'s rows then `layer.toc`'s, and a guard fails any kit that loads out of it. Two tests whose premises the stock order disproves are gone.
- `scripts/smoke.sh` counts VMs by the VM marker, since `Fonts.xml` now loads once per world VM.

Verified: new tests for the load list against the chain toc and a reordered fixture toc, the missing-toc case, and two stock frames on one event running in the toc's order (ExhaustionTick before PlayerFrame on `PLAYER_UPDATE_RESTING`), each failing on main's order. A before/after dump of globals, frames, draw order, event listeners and saved variables differs only in stock-order effects: the reordered listeners and children, draw-order ties between non-overlapping frames, and `UnitPopup`'s quality colours now sharing the live tables. `scripts/gates.sh` and `scripts/smoke.sh` are green, and live before/after runs show 0 errors and identical warnings. Built on macOS; no platform seam touched.

Fixes #555
Fixes #307

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 23:52:14 +07:00
Sam SungandClaude Opus 5.5 6e869eea4e The core loads the stock GameMenuFrame.xml, and the layer reshapes it in place into benilla's menu, keeping every stock rung in the chain anchored TOP (#564)
The core now loads the stock `GameMenuFrame.xml` at its `FrameXML.toc` row, right after `UIPanelTemplates.xml`, and benilla's ESC menu look moves into the layer as `GameMenuAdapters.xml`, which reshapes the stock frame in place: a later file cannot take an occupied frame name (`0x701bd0`).

- Every stock rung stays in the chain with its name and a `TOP` anchor. The three the menu does not show are hidden and laid over the rung above, since a zero height leaves a rung's bottom edge unresolved (`combineEdge 0x767440`). So an addon that inserts under Interface Options and re-anchors Key Bindings (ShaguTweaks), or under Macros and re-anchors Logout (MCP, MoveAnything), lays out as on 1.12.1, with no error.
- The era rungs keep their look and behaviour: Options opens benilla's window; the `BenillaOptionsFrame`-aware wrappers of `ToggleGameMenu`, `IsOptionFrameOpen` and `UpdateMicroButtons` move into the layer; the non-stock names take the `Benilla` prefix. The core toc now names no file of ours.
- With the layer off (`WOW_STOCK_UI=1`), the menu is the stock menu and every rung opens its stock window.

Verified: a region dump of the open menu on main and here differs only in our own frames' names, their insertion order within one level (the rects do not overlap) and sub-1e-4 px float noise; new tests for ShaguTweaks' insert, MCP from the corpus, a pfUI-style skin and the stock-only menu, each failing on main; `scripts/gates.sh` and `scripts/smoke.sh` green; a live run opened the menu, opened Options and returned to the game, and MoveAnything's own insert landed under Macros with Logout below it. Built on macOS; no platform seam touched.

Fixes #553
Fixes #540

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 22:01:24 +07:00
Sam SungandClaude Opus 5.5 44fa1813ac SetCVar and RegisterCVar read their arguments as 1.12.1 does: a number as %.14g text, "0" for a value with no string form, the reference usage text, realmList/realmName/scriptMemory read-only in the world, and CVAR_UPDATE fired inside the call for every token (#557)
`SetCVar` and `RegisterCVar` now read their arguments as the reference's bindings do (`0x488c10`, `0x488b00`):

- a number is stored as Lua's `%.14g` text (`luaV_tostring` `0x6f7c80`), so the stock Sound slider's 0.4 stores `"0.40000000596046"`;
- a value with no string form (nil, absent, boolean, table) stores `"0"` (`0x488c98`, `0x488b6f`), so unticking a stock Sound options box no longer raises and music can be switched off there;
- the reference's usage texts (`0x84235c`, `0x8422e8`);
- `realmList`, `realmName` and `scriptMemory` are read-only to Lua `SetCVar` in the world (`0x48f566`-`0x48f584`), and host writes still land;
- `CVAR_UPDATE` fires inside the call for every string token, changed or not (`0x488cad`-`0x488cd5`, `SignalEvent2` `0x703f50`).

Verified: 7 new VM tests, a stock Sound window test off the chain and a world-entry read-only test, each failing with the change undone; `scripts/gates.sh` green. Built on macOS; no platform seam touched.

Fixes #167
Fixes #139
Fixes #547

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 20:19:05 +07:00
pkuzicandClaude Opus 5.5 89ad90a710 Merge upstream/main (samwhosung/benilla b17be271) into the graphics programme
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:28:31 +01:00
pkuzicandClaude Opus 5.5 853c09f44d Merge upstream/main (samwhosung/benilla b17be271) into the graphics programme
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 21:28:31 +01:00
pkuzicandClaude Opus 5.5 ddc001c849 MONKEY (reviewfix-a): cloud-review fixes — cvars, capture fixture, wind pin, post perf
- farclip High seed 777 declared in SEEDED_DEVIATIONS (owner: High is the
  default); test walks the seeded column against every reference row (#6)
- WOW_CAPTURE_CVARS=<file> replaces BENILLA_HOME as the capture cvar fixture (#13)
- label derivation skips session-owned rows, so an env lever no longer
  persists graphicsQuality/lightingQuality = Custom (#14)
- /console help strings: restored line continuations + a test (#15)
- WOW_FOLIAGE_WIND read only in a dev build under WOW_CAPTURE (#16)
- fog node skipped at volumetricFog 0 with no lamp; fog and SSAO uniforms
  persistent per view, written in prepare (#17)
- tests: every governed graphics row through its observer; OptionsFrame
  farclip slider tied to view::FARCLIP_RANGE

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 00:42:27 +01:00
pkuzicandClaude Opus 5.5 b2b5ef7539 MONKEY (reviewfix-a): cloud-review fixes — cvars, capture fixture, wind pin, post perf
- farclip High seed 777 declared in SEEDED_DEVIATIONS (owner: High is the
  default); test walks the seeded column against every reference row (#6)
- WOW_CAPTURE_CVARS=<file> replaces BENILLA_HOME as the capture cvar fixture (#13)
- label derivation skips session-owned rows, so an env lever no longer
  persists graphicsQuality/lightingQuality = Custom (#14)
- /console help strings: restored line continuations + a test (#15)
- WOW_FOLIAGE_WIND read only in a dev build under WOW_CAPTURE (#16)
- fog node skipped at volumetricFog 0 with no lamp; fog and SSAO uniforms
  persistent per view, written in prepare (#17)
- tests: every governed graphics row through its observer; OptionsFrame
  farclip slider tied to view::FARCLIP_RANGE

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 00:42:27 +01:00
pkuzicandClaude Opus 5.5 6bf7c409a2 MONKEY (integration): merge graphics-programme-presets
Graphics Preset ladder + Render Distance (farclip ceiling 1497). Conflict in
options_tests: the Advanced page now has 33 rows (preset + render distance lead,
then the 31 programme rows). The three round-3 rows join the ladder:
ambientOcclusion 0/0/1/2/2, zoneSkyboxes 0/0/1/1/1, lampFog 0/0/1/2/2
(Classic/Low/Medium/High/Ultra); the preset tooltip names them (EN+RU).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:52:52 +01:00
pkuzicandClaude Opus 5.5 66a6de2264 MONKEY (integration): merge graphics-programme-presets
Graphics Preset ladder + Render Distance (farclip ceiling 1497). Conflict in
options_tests: the Advanced page now has 33 rows (preset + render distance lead,
then the 31 programme rows). The three round-3 rows join the ladder:
ambientOcclusion 0/0/1/2/2, zoneSkyboxes 0/0/1/1/1, lampFog 0/0/1/2/2
(Classic/Low/Medium/High/Ultra); the preset tooltip names them (EN+RU).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 19:52:52 +01:00
Sam SungandClaude Opus 5.5 4f8350c9ed String literals and test names in benilla-app carry no record numbers (#368)
The strings pass for `benilla-app`, the last crate: string literals and test names lose the private record numbers and the workspace's words that a comment pass cannot touch.

Fixes #92. Fixes #36.

- **Messages.** About 230 assertion, panic, `expect` and log messages, printed tool labels, and comments inside embedded Lua fixtures drop their record numbers; the eight that named "the director" keep their fact in plain words. Stale claims in a few messages are corrected on the way (a cast-bar step, a font line, the every-shot recompute).
- **Names.** Seven test and helper names that carried a record number or "director" now say what they check. `git grep` found no other use of any of them.
- **Player files.** The key-bindings file's header and `config.toml`'s header each lose a record citation. Neither header is compared when the file is read back: the bindings reader skips every `#` line, and `config.toml`'s header is a TOML comment.
- **Fixtures.** Test fixtures use neutral character, account and frame names.
- **#36.** The quest-material test labels GameObject type 10 GOOBER (in 1.12, 3 is CHEST, 9 TEXT, 10 GOOBER); the code under test was already right, and the comments were fixed with the comment pass.
- **Verification.** With comments stripped and every literal's contents masked, each file is identical to main and keeps its literals' line counts, apart from the renamed functions and one rustfmt reflow. `cargo fmt --check` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 01:06:28 +07:00
Sam SungandClaude Opus 5.5 b50efc0a53 String literals and test names in benilla-app carry no record numbers (#368)
The strings pass for `benilla-app`, the last crate: string literals and test names lose the private record numbers and the workspace's words that a comment pass cannot touch.

Fixes #92. Fixes #36.

- **Messages.** About 230 assertion, panic, `expect` and log messages, printed tool labels, and comments inside embedded Lua fixtures drop their record numbers; the eight that named "the director" keep their fact in plain words. Stale claims in a few messages are corrected on the way (a cast-bar step, a font line, the every-shot recompute).
- **Names.** Seven test and helper names that carried a record number or "director" now say what they check. `git grep` found no other use of any of them.
- **Player files.** The key-bindings file's header and `config.toml`'s header each lose a record citation. Neither header is compared when the file is read back: the bindings reader skips every `#` line, and `config.toml`'s header is a TOML comment.
- **Fixtures.** Test fixtures use neutral character, account and frame names.
- **#36.** The quest-material test labels GameObject type 10 GOOBER (in 1.12, 3 is CHEST, 9 TEXT, 10 GOOBER); the code under test was already right, and the comments were fixed with the comment pass.
- **Verification.** With comments stripped and every literal's contents masked, each file is identical to main and keeps its literals' line counts, apart from the renamed functions and one rustfmt reflow. `cargo fmt --check` is clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 01:06:28 +07:00
Claude c7f91ef62c MONKEY (presets): Graphics Preset ladder and Render Distance row
Advanced Graphics gains a Graphics Preset row (cvar graphicsQuality:
Classic / Low / Medium / High / Ultra / Custom) and a Render Distance slider
on farclip (177..1497, same cvar as Graphics' Terrain Distance). The ladder is
GRAPHICS_PRESETS in cvars.rs, one line per governed row with a column per
rung; lightingQuality is itself a row, so the lighting ladder's members follow
it. The lighting ladder gains an Ultra rung (4096 sun map, 16/8 torch maps,
High water, High volumetric fog). The label is re-derived every frame beside
lightingQuality, so editing a governed row shows Custom.

A player whose config.toml names no graphicsQuality is seeded to High at
startup over the rows the file does not carry; captures, tests and malformed
files never seed, and the rows keep their per-lane registered defaults. The
page's Defaults lands on High too. ambientOcclusion, zoneSkyboxes and lampFog
are commented one-line TODO rows until their cvars land.

Verified: cargo test -p benilla-app --lib -- options cvars preset (136
passed, incl. 7 new preset tests); full benilla-app lib: 3722 passed, 5
pre-existing failures unrelated to this change (spell envelope float, schedule
ambiguity ceiling, pipe_warm fog/shaft/shadow lanes, dev-plane cfg). The
options UI tests need WoW data and were skipped; no captures (no install in
the cloud). Built on Linux only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Toc8dZYuTKW1JWBFbMb5S1
2026-09-25 18:00:24 +00:00
Claude 905fd9516e MONKEY (presets): Graphics Preset ladder and Render Distance row
Advanced Graphics gains a Graphics Preset row (cvar graphicsQuality:
Classic / Low / Medium / High / Ultra / Custom) and a Render Distance slider
on farclip (177..1497, same cvar as Graphics' Terrain Distance). The ladder is
GRAPHICS_PRESETS in cvars.rs, one line per governed row with a column per
rung; lightingQuality is itself a row, so the lighting ladder's members follow
it. The lighting ladder gains an Ultra rung (4096 sun map, 16/8 torch maps,
High water, High volumetric fog). The label is re-derived every frame beside
lightingQuality, so editing a governed row shows Custom.

A player whose config.toml names no graphicsQuality is seeded to High at
startup over the rows the file does not carry; captures, tests and malformed
files never seed, and the rows keep their per-lane registered defaults. The
page's Defaults lands on High too. ambientOcclusion, zoneSkyboxes and lampFog
are commented one-line TODO rows until their cvars land.

Verified: cargo test -p benilla-app --lib -- options cvars preset (136
passed, incl. 7 new preset tests); full benilla-app lib: 3722 passed, 5
pre-existing failures unrelated to this change (spell envelope float, schedule
ambiguity ceiling, pipe_warm fog/shaft/shadow lanes, dev-plane cfg). The
options UI tests need WoW data and were skipped; no captures (no install in
the cloud). Built on Linux only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Toc8dZYuTKW1JWBFbMb5S1
2026-09-25 18:00:24 +00:00
pkuzicandClaude Opus 5.5 2f68d4c4e6 MONKEY (integration): merge gfx/skybox
Zone Skyboxes row moved to the end of the Advanced Graphics page and added to
OPTIONS_PAGE_ROWS (the lane had left it out of the list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:08:23 +01:00
pkuzicandClaude Opus 5.5 89b68ed26b MONKEY (integration): merge gfx/skybox
Zone Skyboxes row moved to the end of the Advanced Graphics page and added to
OPTIONS_PAGE_ROWS (the lane had left it out of the list).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:08:23 +01:00
pkuzicandClaude Opus 5.5 3816d5c47a MONKEY (integration): Advanced Graphics row for daylightWindowSplit (EN+RU)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:06:45 +01:00
pkuzicandClaude Opus 5.5 f0a27ae72b MONKEY (integration): Advanced Graphics row for daylightWindowSplit (EN+RU)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 18:06:45 +01:00
pkuzicandClaude Opus 5.5 d554ffe939 MONKEY (integration): merge gfx/lampfog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:36:40 +01:00