From 1c786b7ecb80af76a8ff4033c3d3dd8f9848d619 Mon Sep 17 00:00:00 2001 From: grom Date: Mon, 26 Oct 2026 14:39:25 -0600 Subject: [PATCH] Anisotropic Filtering takes its seat on the Graphics page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/assets/ui/OptionsFrame.xml | 74 +++++++++++++++++- .../src/ui_script/options_tests.rs | 75 ++++++++++++++++++- 2 files changed, 143 insertions(+), 6 deletions(-) diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index 2bea1780..2bb70146 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -420,6 +420,19 @@ .. "Below 100% trades sharpness for frame rate; above 100% is supersampling, which " .. "smooths edges at the cost of frame rate. Applies when you click Apply." + -- 1.12's own OPTION_TOOLTIP_ANISOTROPIC describes a four-stop slider that takes + -- effect on restart. Ours is neither: the groove runs the CVar's own 1-16 (the + -- reference's callback parses that range, `0x689110`; only its *slider* stopped at 4), + -- and it commits in place on Apply. So the text is ours. + BENILLA_TOOLTIP_ANISOTROPIC = + "Sharpens ground and wall texture seen at a grazing angle - the road running away " + .. "from you, a floor stretching into a room.\n\n" + .. "Off is what the original client shipped, which smears that texture to mush a few " + .. "yards out. 16x costs about a millisecond of graphics time and nothing at all on " + .. "the processor, so it is close to free on any modern card.\n\n" + .. "Each texture is sampled the way it was when it loaded, so this takes effect the " + .. "next time you start the game." + -- 1.12 has no such checkbox. BENILLA_TOOLTIP_BACKGROUND_SOUND = "Keeps the game's sound playing while you are working in another window.\n\n" @@ -1051,7 +1064,8 @@ }, Graphics = { "RowUiScale", "RowFarclip", "RowWorldDetail", "RowBrightness", - "RowWeatherDensity", + -- GFX (anisotropic): index 7 of OptionsFrameSliders, in that table's own order. + "RowAnisotropic", "RowWeatherDensity", "RowDisplayMode", "RowResolution", "RowVerticalSync", "RowRenderScale", }, @@ -1257,7 +1271,15 @@ { text = WEATHER_DETAIL, func = "weatherDensity", minValue = 0, maxValue = 3, valueStep = 1, tooltipText = OPTION_TOOLTIP_WEATHER_DETAIL }, } - -- Holes at 4, 5, 7 and 8, so the OnShow loop uses the pairs form: ipairs would stop at 3. + -- The rows whose bounds ARE the stock table's, re-applied on show so an addon's write to + -- OptionsFrameSliders lands. Holes at 4, 5, 7 and 8, so the OnShow loop uses the pairs + -- form: ipairs would stop at 3. + -- + -- 7 (ANISOTROPIC) is a seated row but deliberately not listed: the stock entry caps at 4, + -- which was the reference *slider's* limit and not the CVar's (its callback parses 1-16, + -- `0x689110`), and High and Ultra both ask for 16. So that row carries its own literal + -- bounds, as `RowRenderScale` does for a CVar the table never had — and the table above + -- stays verbatim for any addon that reads it. BENILLA_GRAPHICS_SLIDER_ROWS = { "RowUiScale", "RowFarclip", "RowWorldDetail", [6] = "RowBrightness", [9] = "RowWeatherDensity", @@ -1430,6 +1452,14 @@ if row.fmt == "yd" then return string.format(BENILLA_ADVGFX.yards, math.floor(value + 0.5)) end + -- The anisotropy groove. 1 is not "1x", it is the feature OFF (`tex_filter.rs`: + -- the reference writes filter mode 5 only at 2 and above), and a player reading "1x" + -- next to "2x" would take it for the weakest setting rather than for none. + if row.fmt == "aniso" then + local n = math.floor(value + 0.5) + if n <= 1 then return "Off" end + return string.format("%dx", n) + end return string.format("%d%%", math.floor(value * 100 + 0.5)) end @@ -2792,12 +2822,48 @@ OptionsFrameSliders[6].valueStep, "travel") + + + + + + + -- Index 7 of OptionsFrameSliders, seated where the + -- reference's own slider order puts it: after + -- Brightness, before Weather Intensity. Before this + -- the row was a hole, so the only way to anisotropy + -- was `/console anisotropic 16` — and a player on a + -- Custom graphics preset never met it at all, since + -- the preset ladder is what seeds the value. + OptionsRow_OnLoad(self, "anisotropic", "Anisotropic Filtering", + "BENILLA_TOOLTIP_ANISOTROPIC") + -- 1 to 16, the CVar's own range (`ANISO_RANGE`), + -- not the stock table's 1-4 — see + -- BENILLA_GRAPHICS_SLIDER_ROWS. Step 1 rather than + -- a power-of-two ladder because the value is a + -- MAXIMUM the driver is free to round down: an + -- off-ladder stop is a weaker setting, never a + -- wrong one, which is the test `RowShadowResolution` + -- fails and so takes a dropdown instead. + OptionsSliderRow_Setup(self, 1, 16, 1, "aniso") + -- DEFERRED, as the two rows below it are, because + -- the write does not reach a texture that is + -- already resident: a sampler is baked into each + -- `Image` at load (`tex_filter.rs`), which is why + -- the reference's own UI says "enabled upon + -- restart". Apply is therefore the honest commit + -- point, and a drag does not write a CVar on every + -- step of the groove that only the next launch + -- reads. + self.deferred = 1 + + - - + + OptionsRow_OnLoad(self, "weatherDensity", "Weather Intensity", diff --git a/crates/benilla-app/src/ui_script/options_tests.rs b/crates/benilla-app/src/ui_script/options_tests.rs index ca47d517..d751ae65 100644 --- a/crates/benilla-app/src/ui_script/options_tests.rs +++ b/crates/benilla-app/src/ui_script/options_tests.rs @@ -1704,6 +1704,71 @@ fn the_render_scale_row_shows_a_percentage_and_defers_to_apply() { assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); } +/// GFX (anisotropic): the row that was a hole. Three things it has to get right — the groove +/// reaches the CVar's own 16 and not the stock table's 4, `1` reads as "Off" and not as "1x", and +/// the write waits for Apply (committing re-samples every resident texture, which a drag would +/// pay on every step). +#[test] +fn the_anisotropic_row_reaches_sixteen_reads_off_at_one_and_defers_to_apply() { + benilla_formats::wow_data_or_skip!(); + let mut s = audio_harness(); + s.set_cvar_host("anisotropic", "1"); + let mut s = harness_on(s); + s.run("ShowUIPanel(BenillaOptionsFrame)").unwrap(); + s.run("BenillaOptionsFrameCategoryListRowGraphics:Click()") + .unwrap(); + let row = "BenillaOptionsFrameContainerBodyGraphicsRowAnisotropic"; + + assert_eq!( + s.eval::(&format!("return {row}Label:GetText()")) + .unwrap(), + "Anisotropic Filtering" + ); + assert_eq!( + s.eval::(&format!("return {row}ControlValue:GetText()")) + .unwrap(), + "Off", + "1 is the feature off, not its weakest stop" + ); + // The CVar's own `ANISO_RANGE`, not `OptionsFrameSliders[7]`'s 1-4, which was the reference + // slider's ceiling and is below both the High and the Ultra rung. + assert!(s + .eval::(&format!( + "local lo, hi = {row}ControlSlider:GetMinMaxValues() \ + return lo == 1 and hi == 16" + )) + .unwrap()); + assert!(s + .eval::("return BENILLA_TOOLTIP_ANISOTROPIC ~= nil") + .unwrap()); + // The stock table stays verbatim for any addon that reads it. + assert!(s + .eval::("return OptionsFrameSliders[7].maxValue == 4") + .unwrap()); + let _ = s.take_cvar_changes(); + + s.run(&format!("{row}ControlSlider:SetValue(16)")).unwrap(); + assert_eq!( + s.eval::(&format!("return {row}ControlValue:GetText()")) + .unwrap(), + "16x" + ); + assert!( + s.take_cvar_changes().is_empty(), + "a deferred row must not write the CVar on the move" + ); + assert!(s + .eval::("return BenillaOptionsFrameApplyButton:IsVisible()") + .unwrap()); + + s.run("BenillaOptionsFrameApplyButton:Click()").unwrap(); + assert_eq!( + s.take_cvar_changes(), + vec![("anisotropic".to_string(), "16".to_string())] + ); + assert!(s.errors().is_empty(), "script errors: {:?}", s.errors()); +} + /// Pending edits are panel-wide, as the era's modified table: a category switch keeps them, and /// hiding the window discards them. #[test] @@ -3125,6 +3190,9 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() { ), ("BENILLA_TOOLTIP_LAVA_GLOW", "AdvancedGraphicsRowLavaGlow"), ("BENILLA_TOOLTIP_RENDER_SCALE", "GraphicsRowRenderScale"), + // GFX (anisotropic): ours, because 1.12's own text describes a four-stop slider + // that needs a restart and this row is neither. + ("BENILLA_TOOLTIP_ANISOTROPIC", "GraphicsRowAnisotropic"), ("BENILLA_TOOLTIP_DISPLAY_MODE", "GraphicsRowDisplayMode"), // 1.12 shipped the resolution dropdown with no tooltip at all, so there is no // `OPTION_TOOLTIP_*` for it to borrow; benilla's own text explains the restart. @@ -3260,7 +3328,9 @@ fn every_row_tooltip_key_resolves_in_the_real_global_strings() { // lava, fog, daylight, Graphics Preset, Render Distance, ...) add 20. 78 -> 98. // MONKEY (post / dither / fog / wet / wind / volumetric light): their rows add 17, and the // Graphics page's resolution dropdown one more. 98 -> 116. - assert_eq!(checked, 116, "every tipped row carries a live key"); + // GFX (anisotropic): the Graphics page's own anisotropy slider, seated where the reference's + // slider table already declared it. 116 -> 117. + assert_eq!(checked, 117, "every tipped row carries a live key"); assert_eq!( untipped, vec![ @@ -3364,7 +3434,8 @@ fn every_flavor_of_row_raises_its_plate_from_the_page_it_lives_on() { // per-row; the total is here so a row losing its key is a failure rather than a smaller pass. // MONKEY (graphics programme): the Advanced Graphics page's described rows add 19. 78 -> 97. // MONKEY (post / dither / fog / wet / wind / volumetric light / resolution): 97 -> 116. - assert_eq!(raised, 116, "every row but Auto Loot raises a description"); + // GFX (anisotropic): the Graphics page's anisotropy slider. 116 -> 117. + assert_eq!(raised, 117, "every row but Auto Loot raises a description"); } /// 1.12's AdvancedOptionsCombatText box as saved-global rows: a click writes the global, never a