From a9ed091ebe48e3bc78de629fc1992d9364ba5a8f Mon Sep 17 00:00:00 2001 From: grom Date: Sat, 31 Oct 2026 17:48:09 -0600 Subject: [PATCH] `texture miss: ''` was three setters storing an empty path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Texture:SetTexture("")` already cleared, and so did a button's slot setters — the reference treats nil and `""` alike on the path arm (`0x770200`). But `SetStatusBarTexture`, `SetThumbTexture` and the two `ColorSelect` setters stored the empty string, so the region reached the extractor as `path: Some("")`, and the renderer asked the patch chain for a nameless file. What that cost is less than the warning suggested, and the comments say so rather than the opposite: both the decode and the warning are cached per key, so it was one chain lock and two candidate reads, once. The damage was the line itself — a defect-shaped warning with nothing under it, in a client where a real `texture miss` is worth reading. pfUI blanks textures this way in a dozen places, which is why it showed up at all. Measured both ways: the new test reports `[""]` with any one guard removed, and three UI captures (`ui-char`, `ui-options`, `ui-actionbar`) now log zero `texture miss` of any kind. His own 2026-10-28 log has the line once, at 20.7 s. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-ui/src/script/colorselect.rs | 8 +++ crates/benilla-ui/src/script/slider.rs | 8 +++ crates/benilla-ui/src/script/statusbar.rs | 11 ++++ .../benilla-ui/src/script/tests/statusbar.rs | 59 +++++++++++++++++++ 4 files changed, 86 insertions(+) diff --git a/crates/benilla-ui/src/script/colorselect.rs b/crates/benilla-ui/src/script/colorselect.rs index e92e8b60..375da367 100644 --- a/crates/benilla-ui/src/script/colorselect.rs +++ b/crates/benilla-ui/src/script/colorselect.rs @@ -144,6 +144,14 @@ fn install_slot_texture( let mut model = lua.app_data_mut::().expect("model app_data"); let data = model.region_data.entry(rh).or_default(); match &a1 { + // `""` clears, as `SetTexture`'s own path arm does (`0x770200`). See the + // note on `SetStatusBarTexture`: stored, an empty path reaches the extractor + // as `path: Some("")` and the renderer logs a `texture miss: ''` for a file + // nobody named. + Value::String(s) if s.to_str()?.is_empty() => { + data.texture = None; + data.fill = None; + } Value::String(s) => { data.texture = Some(s.to_str()?.to_string()); data.fill = None; diff --git a/crates/benilla-ui/src/script/slider.rs b/crates/benilla-ui/src/script/slider.rs index ac2a4299..bb0dbede 100644 --- a/crates/benilla-ui/src/script/slider.rs +++ b/crates/benilla-ui/src/script/slider.rs @@ -169,6 +169,14 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let mut model = lua.app_data_mut::().expect("model app_data"); let data = model.region_data.entry(rh).or_default(); match &a1 { + // `""` clears, as `SetTexture`'s own path arm does (`0x770200`). See the + // note on `SetStatusBarTexture`: stored, an empty path reaches the extractor + // as `path: Some("")` and the renderer logs a `texture miss: ''` for a file + // nobody named. + Value::String(s) if s.to_str()?.is_empty() => { + data.texture = None; + data.fill = None; + } Value::String(s) => { data.texture = Some(s.to_str()?.to_string()); data.fill = None; diff --git a/crates/benilla-ui/src/script/statusbar.rs b/crates/benilla-ui/src/script/statusbar.rs index 3f18e329..8369576c 100644 --- a/crates/benilla-ui/src/script/statusbar.rs +++ b/crates/benilla-ui/src/script/statusbar.rs @@ -160,6 +160,17 @@ pub(super) fn install(lua: &Lua) -> mlua::Result<()> { let mut model = lua.app_data_mut::().expect("model app_data"); let data = model.region_data.entry(rh).or_default(); match &a1 { + // `""` clears, as `SetTexture`'s path arm does (`0x770200`: "nil or `\"\"` + // clear it") and as the button slot setter does. Stored, it reached the + // extractor as `path: Some("")`, so the renderer asked the patch chain for a + // nameless file and logged `texture miss: ''`. Both the decode and the warning + // are cached per key, so the cost was one lock and two candidate reads, once — + // the damage was the warning itself, a real-looking signal with no defect + // under it. pfUI blanks textures this way in a dozen places. + Value::String(s) if s.to_str()?.is_empty() => { + data.texture = None; + data.fill = None; + } Value::String(s) => { data.texture = Some(s.to_str()?.to_string()); data.fill = None; diff --git a/crates/benilla-ui/src/script/tests/statusbar.rs b/crates/benilla-ui/src/script/tests/statusbar.rs index 62eb5925..dc0abf4d 100644 --- a/crates/benilla-ui/src/script/tests/statusbar.rs +++ b/crates/benilla-ui/src/script/tests/statusbar.rs @@ -195,3 +195,62 @@ fn statusbar_get_texture_returns_the_bar_region() { ) .unwrap(); } + +/// `""` clears, on every setter that takes a texture path — not just `SetTexture`. +/// +/// The reference's path arm treats nil and `""` alike (`0x770200`), and ours did for +/// `Texture:SetTexture` and for a button's slot textures while `SetStatusBarTexture`, +/// `SetThumbTexture` and the two `ColorSelect` setters stored the empty string. Stored, it reached +/// the extractor as `path: Some("")`, and the renderer then asked the patch chain for a nameless +/// file and logged `texture miss: ''`. The decode and the warning are both cached per key, so the +/// cost was one lock and two candidate reads once — what it really produced was a defect-shaped +/// warning with no defect under it, in a client where a real `texture miss` is worth reading. +/// pfUI blanks textures this way in a dozen places. +#[test] +fn an_empty_path_clears_every_texture_setter_not_only_set_texture() { + let mut s = script(); + s.set_screen_size(1024.0, 768.0); + s.run( + r#" + Blanked = {} + local b = CreateFrame("StatusBar", "SbBlank") + b:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 0) + b:SetWidth(200); b:SetHeight(20) + b:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") + b:SetMinMaxValues(0, 100) b:SetValue(100) + table.insert(Blanked, function() b:SetStatusBarTexture("") end) + + local sl = CreateFrame("Slider", "SlBlank") + sl:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 40) + sl:SetWidth(100); sl:SetHeight(16) + sl:SetThumbTexture("Interface\\Buttons\\UI-SliderBar-Button-Horizontal") + table.insert(Blanked, function() sl:SetThumbTexture("") end) + + local cs = CreateFrame("ColorSelect", "CsBlank") + cs:SetPoint("BOTTOMLEFT", nil, "BOTTOMLEFT", 0, 80) + cs:SetWidth(128); cs:SetHeight(128) + cs:SetColorWheelTexture("Interface\\Buttons\\UI-ColorPicker-Buttons") + table.insert(Blanked, function() cs:SetColorWheelTexture("") end) + "#, + ) + .unwrap(); + s.resolve(); + let paths = |s: &UiScript| -> Vec { + s.extract() + .iter() + .filter_map(|q| match &q.content { + QuadContent::Texture { path: Some(p), .. } => Some(p.clone()), + _ => None, + }) + .collect() + }; + assert_eq!(paths(&s).len(), 3, "three textured regions to begin with"); + + s.run("for _, f in ipairs(Blanked) do f() end").unwrap(); + s.resolve(); + assert!( + paths(&s).is_empty(), + "an empty path must leave no textured quad at all, let alone a nameless one: {:?}", + paths(&s) + ); +}