From 1a1d7e50dcc78cb6cb745defe1f4cc1ca0debdd1 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 27 Aug 2026 15:16:01 +0700 Subject: [PATCH] the world redraws on resize, and terrain takes its DXT blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the world froze on every resize or graphics change: the backdrop was resized by writing a fresh image through the existing handle, and a mutated `Image` is a whole new GPU texture while a material's bind group is captured once and never re-captured — so the camera drew into a texture the UI never sampled, and the last frame stretched over the window forever; a resized world target is now a NEW asset (publish, retire the old one, and the UI pass forgets materials for removed images so a window drag does not leak a 46 MB target per frame), and a resized capture now matches the native render bit for bit - terrain tile layer arrays take the DXT blocks when the tile's whole layer set allows it — one array has one format, so eligibility is the tile's, not a texture's — and the install's own census says narrow is enough: 87 % of Azeroth's tiles and 92 % of Kalimdor's pass, layer bytes 1.57 GB → 0.63 and 2.23 → 0.76; the decode-tax comment is corrected on the way past — decoding costs 8x DXT1's bytes and 4x DXT3/DXT5's, not 4x and 2x - Environment Detail is the reference's slider, not a dropdown — row 27 of 1.12's own slider table, with the readout keeping the stop names because a bare "1" means nothing to a player — and the shipped ground cover comes off High to Medium: the x3 clutter density had been matched to a configured install, and Medium is the nearest stop no sparser than what a fresh install draws on capable hardware - Display Mode is modern Classic's two-entry dropdown — Windowed (Fullscreen) / Windowed, a boolean at Blizzard's own source, where the three-mode list is pre-8.0.1 folklore — riding `gxWindow` so saved configs keep meaning, with the label-against-value polarity pinned by the row's first test; the Multisampling row comes off the Graphics page for now (the CVar, the device clamp and the bindings all stay): with alpha-to-coverage set nowhere, MSAA touches polygon silhouettes only, and render scale on the same page measures four times the visual change Co-Authored-By: Claude Fable 5 --- crates/benilla-app/assets/ui/OptionsFrame.xml | 184 ++++---- crates/benilla-app/src/capture/fixtures.rs | 15 +- crates/benilla-app/src/capture/scenarios.rs | 22 +- crates/benilla-app/src/cvars.rs | 30 +- crates/benilla-app/src/ui_pass.rs | 117 ++++- .../src/ui_script/options_tests.rs | 276 +++++------- crates/benilla-app/src/video.rs | 10 +- crates/benilla-app/src/world_backdrop.rs | 200 +++++++-- crates/benilla-assets/src/adt.rs | 66 +-- crates/benilla-assets/src/gpu_blp.rs | 4 +- crates/benilla-assets/src/terrain.rs | 421 +++++++++++++++++- crates/benilla-world/src/clutter.rs | 12 +- 12 files changed, 982 insertions(+), 375 deletions(-) diff --git a/crates/benilla-app/assets/ui/OptionsFrame.xml b/crates/benilla-app/assets/ui/OptionsFrame.xml index d9497d0c..70627e74 100644 --- a/crates/benilla-app/assets/ui/OptionsFrame.xml +++ b/crates/benilla-app/assets/ui/OptionsFrame.xml @@ -609,7 +609,7 @@ -- CENTER-80, child rows +15 indent in the small font). Era rows with no 1.12 CVar + -- benilla knob behind them (Enable Sound=Sound_EnableAllSound, Dialog, output device, -- channels…) do not ship — the 0950 honest-tree rule applied to settings. - -- The one options string that is NOT the reference's (decision 1639). Every other + -- The options strings that are NOT the reference's (1639, 1650). Every other -- tooltip on this window resolves an OPTION_TOOLTIP_* out of 1.12's GlobalStrings, and a -- benilla row with no counterpart there has so far simply gone mute (the nameplate pair). -- Render Scale cannot afford to: the director's own reaction to the dial was "not really @@ -622,6 +622,17 @@ .. "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." + -- Display Mode (1650). 1.12's own OPTION_TOOLTIP_WINDOWED_MODE cannot ride this row: it + -- is written for the checkbox it belonged to ("Check to play in a non-fullscreen + -- window..."), and its second half describes a desktop-gamma consequence of an exclusive + -- mode-set we do not do. The row also has to answer the question a player arrives with — + -- "where is Fullscreen?" — which no reference string was ever written to answer. + BENILLA_TOOLTIP_DISPLAY_MODE = + "Windowed (Fullscreen) fills the screen with a borderless window; Windowed runs in " + .. "an ordinary one.\n\n" + .. "There is no exclusive fullscreen mode. WoW itself removed one in 8.0.1, and " + .. "modern Classic offers these same two." + OPTIONS_PAGE_ROWS = { Controls = { "RowStickyTarget", "RowAutoLoot", "RowInvertMouse", "RowMouseSpeed", @@ -633,7 +644,7 @@ }, Graphics = { "RowUiScale", "RowFarclip", "RowWorldDetail", - "RowWindowedMode", "RowVerticalSync", "RowRenderScale", "RowMultisample", + "RowDisplayMode", "RowVerticalSync", "RowRenderScale", }, Nameplates = { "RowEnemyPlates", "RowFriendlyPlates", @@ -855,9 +866,15 @@ -- refreshing guard: SetMinMaxValues re-clamps the held value and fires OnValueChanged, -- and that clamp is wiring, not a player gesture — without the guard every load would -- SetCVar the template's 0 clamped into the new range. - function OptionsSliderRow_Setup(row, min, max, step, fmt) + function OptionsSliderRow_Setup(row, min, max, step, fmt, stops) local slider = getglobal(row:GetName() .. "ControlSlider") row.fmt = fmt + -- A grid that IS a short named ladder passes its names here (Environment Detail's + -- Low/Medium/High over 0/1/2): the groove is still the reference's slider, and the + -- readout says the stop rather than its index. 1.12's own slider printed no value at + -- all — it labelled the two ENDS — but every slider row in this kit carries a readout, + -- and "1" is the one thing that seat could say that means nothing to a player. + row.stops = stops row.refreshing = 1 slider:SetMinMaxValues(min, max) slider:SetValueStep(step) @@ -958,6 +975,15 @@ if row.fmt == "raw" then return string.format("%d", math.floor(value + 0.5)) end + -- A named ladder reads out its stop's name (see OptionsSliderRow_Setup). The index is + -- clamped, not wrapped: the slider itself has already clamped a config value from + -- outside the range, so this only ever guards a stops table shorter than the grid. + if row.stops then + local i = math.floor(value + 0.5) + 1 + if i < 1 then i = 1 end + if i > table.getn(row.stops) then i = table.getn(row.stops) end + return row.stops[i] + end -- A row whose stored value is a FACTOR but whose meaning is a distance: show the -- distance (decision 1140 — cameraDistanceMaxFactor over the 15 yd base). The number -- the player is choosing is "how far out the wheel stops", not "times 1.4". @@ -2235,10 +2261,11 @@ retired by the director in 0961 and back by their call in 1513, now that the one number moves the far-clip wall AND how much terrain is resident, as it does in the reference. - Environment Detail (0992) is the same table's WorldDetail row + Environment Detail is the same table's WorldDetail row (0..2 step 1, label ENVIRONMENT_DETAIL) over the clutter-density - knob, worn as the dropdown row — the shape call is the - template's comment. --> + knob. 0992 wore it as a dropdown; 1649 puts it back on the + SLIDER the reference actually uses, keeping 0992's stop names + in the readout seat. --> @@ -2272,7 +2299,7 @@ OptionsSliderRow_Setup(self, 177, 777, 60, "raw") - + @@ -2283,42 +2310,54 @@ -- The stop names are the era preset ladder's (its -- quality dropdowns say Low/Medium/High); 1.12's -- slider had no stop labels to quote. - OptionsDropdownRow_Setup(self, { - { text = "Low", value = "0" }, - { text = "Medium", value = "1" }, - { text = "High", value = "2" }, - }) + OptionsSliderRow_Setup(self, 0, 2, 1, nil, + { "Low", "Medium", "High" }) - + - -- Windowed Mode: 1.12's own Video Options checkbox - -- over gxWindow, label and tooltip key both the - -- reference's GlobalStrings (WINDOWED_MODE / - -- OPTION_TOOLTIP_WINDOWED_MODE). + -- Display Mode (1650): MODERN CLASSIC's row, not + -- 1.12's, because 1627 already gave us modern + -- Classic's two states and a checkbox could only + -- name one of them. -- - -- The row is the reference's; UNCHECKED is not. - -- There it mode-sets the display at gxResolution; - -- here it raises a BORDERLESS fullscreen window, - -- and no exclusive mode exists to reach (decision - -- 1627 -- Wayland cannot do one at all, and WoW - -- itself dropped exclusive fullscreen in 8.0.1). + -- Blizzard_SettingsDefinitions_Shared/Graphics.lua + -- (live, classic AND classic_era, all identical): + -- a BOOLEAN proxy rendered as a dropdown, with + -- exactly two entries -- container:Add(true, + -- VIDEO_OPTIONS_WINDOWED_FULLSCREEN) and + -- container:Add(false, VIDEO_OPTIONS_WINDOWED). + -- There is no Fullscreen entry anywhere in it; + -- 8.0.1 removed the exclusive mode those clients + -- had, which is the same reason 1627 ships none. + -- + -- It rides gxWindow, OUR persisted CVar, not the + -- gxMaximize the modern client uses -- that client + -- deleted gxWindow entirely and had to move; we + -- did not, and repointing would orphan every + -- config.toml already holding the row for nothing. + -- Polarity is gxWindow's own: "0" is not-windowed, + -- which 1627 redefined as the borderless window. -- -- Not deferred, for the same reason as Vertical -- Sync below: the reference row carries -- gxRestart = 1 and ours applies on the click. - OptionsRow_OnLoad(self, "gxWindow", "Windowed Mode", - "OPTION_TOOLTIP_WINDOWED_MODE") + OptionsRow_OnLoad(self, "gxWindow", "Display Mode", + "BENILLA_TOOLTIP_DISPLAY_MODE") + OptionsDropdownRow_Setup(self, { + { text = "Windowed (Fullscreen)", value = "0" }, + { text = "Windowed", value = "1" }, + }) - - + + -- Vertical Sync: 1.12's own Video Options checkbox @@ -2340,12 +2379,15 @@ -- Render Scale (decision 1639) -- benilla's own -- row, no 1.12 counterpart: the reference renders -- straight to the backbuffer and has no second - -- image to size. Seated directly above - -- Multisampling because the two are the page's - -- antialiasing pair and a player weighing one - -- against the other should see them together -- - -- and because above 100% this row IS antialiasing, - -- on a client whose gxMultisample ships off (1629). + -- image to size. It CLOSES the page, and it is + -- the only antialiasing control on it: the + -- reference's own Multisampling row was pulled + -- 2026-08-27 (decision 1648, the director's call). + -- gxMultisample survives as a CVar and $WOW_MSAA + -- -- what went is the row, because multisampling + -- cannot touch the alpha-tested cutouts that carry + -- most of this game's visible aliasing, and above + -- 100% this row can. -- -- 50-200%, narrower than the CVar's own clamp -- (RENDER_SCALE_RANGE, 0.25-4.0). That clamp stops @@ -2367,82 +2409,6 @@ self.deferred = 1 - - - - - - - -- Multisampling: 1.12's own Video Options dropdown - -- (OptionsFrameMultiSampleDropDown, id 4), over - -- gxMultisample. Label and tooltip key are the - -- reference's GlobalStrings (MULTISAMPLE = - -- "Multisampling" / OPTION_TOOLTIP_MULTISAMPLING). - -- - -- The STOPS ARE THE DEVICE'S, not a fixed ladder: - -- built here from GetMultisampleFormats(), which - -- is what this GPU actually accepts (decision - -- 1631's enumeration, the same one that clamps a - -- typed gxMultisample). That is the reference's - -- design too -- its list comes from a D3D - -- CheckDeviceMultiSampleType sweep or a GL - -- wglGetPixelFormatAttribivARB sweep -- and it is - -- why the row cannot offer a mode that would kill - -- the client. An Apple GPU shows three stops - -- (1x/2x/4x); a machine with 8x shows four. - -- - -- NOT marked deferred. That flag stages a value - -- until Apply (uiScale's mid-drag rescale is what - -- it exists for); this row writes live like every - -- other. What it cannot do is take effect live: - -- gxMultisample is LATCHED (decision 1629 -- the - -- camera reads its sample count once, at spawn), - -- exactly as the reference's own row is, whose - -- callback echoes "set pending gxRestart". The - -- CVar moves and persists on the click; the - -- picture changes at the next launch. - OptionsRow_OnLoad(self, "gxMultisample", "Multisampling", - "OPTION_TOOLTIP_MULTISAMPLING") - local stops = {} - local fmt = { GetMultisampleFormats() } - -- Three varargs per entry, the walk OptionsFrame.lua - -- l.354 does (`for i=1, arg.n, 3`) -- and then we - -- use ONE of the three. - -- - -- 1.12 formats all three (MULTISAMPLING_FORMAT_ - -- STRING = "%d-bit color %d-bit depth %dx - -- multisample") because in 2004 they VARIED: its - -- D3D CheckDeviceMultiSampleType sweep really did - -- enumerate 16-bit and 32-bit colour modes, so the - -- colour and depth figures were part of the - -- choice. Ours cannot vary -- every entry carries - -- the one pair `view::dropdown_bit_depths` returns, - -- the swapchain's format and Depth32Float, fixed - -- in our code and not by luck of hardware. Quoting - -- them on every row would reproduce the FORMAT of - -- a fact we no longer have, and it overflowed the - -- capsule doing it (the director's 08-27 shot). - -- The full triple is on the record in 1632; the - -- row shows what a player is actually choosing. - -- - -- "Off", not "1x": 1 is the encoding of NO - -- multisampling, not one sample of it -- VERIFIED - -- at 0x59989b (D3D leaves MultiSampleType - -- D3DMULTISAMPLE_NONE) and 0x59de35 (GL never - -- emits the SAMPLE_BUFFERS/SAMPLES pair). Short - -- values are this page's own grammar too: the - -- label carries the noun and the capsule carries - -- the value ("Environment Detail [ High ]"). - for i = 1, table.getn(fmt), 3 do - local n = fmt[i + 2] - table.insert(stops, { - text = (n > 1) and (n .. "x") or "Off", - value = tostring(n), - }) - end - OptionsDropdownRow_Setup(self, stops) - -