"Deep audit of the game UI tracks — Scaleform/SWF (Ruffle host bridge, AVM1/AVM2 profiles, ABC adapter injection, archive navigator, offscreen wgpu readback, overlay upload, input routing) and the Oblivion/FO3/FNV MenuXml track (eval, layout, raster) with both HUD drivers"
Install
npx skillscat add matiaszanolli/byroredux/claude-commands-audit-ui Install via the SkillsCat registry.
UI Audit: Scaleform (R4 + M48) and MenuXml (M48.4-M48.7)
Read .claude/commands/_audit-common.md (delta-first scoping, dedup, finding format) and _audit-severity.md for shared protocol.
Audits the host contract that decides whether a Bethesda menu works, on two tracks that share one overlay compositing path. Orchestrator; one Task agent per dimension (max 3 concurrent).
Scope
- Scaleform:
crates/ui/src/(lib.rsUiManager, deliberately not an ECSResource;profile.rs;prepare.rs;host.rs;avm1_host.rsSkyrim scanner;avm2_host.rsFO4BGSCodeObjABC injection;catalog.rs;navigator.rs;player.rsoffscreen wgpu;pacing.rsrender pacer;input.rs); protocol pinscrates/ui/tests/{hudmenu_protocol,fallout4_hudmenu_protocol}.rs. - MenuXml:
crates/menuxml/src/(eval,layout,raster,menu,profileaudited here;parse,tex,fontare the parse side, owned by/audit-parsers). - Engine side:
byroredux/src/{ui_input,hud,scaleform_hud}.rs,byroredux/src/commands/hud.rs,byroredux/src/app_frame.rs(tick_ui_overlay,tick_hud_overlay),app_events.rs,scene.rs(--menu/--hudlaunch), renderercrates/renderer/src/vulkan/{presentation.rs,texture.rs,context/post_passes.rs},crates/renderer/src/texture_registry/dynamic_rgba.rs+crates/renderer/shaders/ui.{vert,frag}. - Handoffs: GPU teardown order
/audit-concurrency; Ruffle/wgpuunsafe/audit-safety; MenuXml XML/DDS/.fntparse robustness/audit-parsers; what a HUD shows (actor values, inventory)/audit-gameplay.
Ground truth: docs/engine/ui.md (host contract, MenuXml section, pending list), docs/smoke-tests/README.md.
Deliberately unbuilt (verified 2026-10-12 against docs/engine/ui.md; re-verify, do not trust this line): engine handlers for host methods (menus receive Null), Papyrus↔UI bridge, menu stack/focus policy, font fidelity, full menu pack, Starfield HUD bring-up (#4470 is closed, but only its parser blocker shipped: 9813af435 normalize_scaleform_dialect; no ScaleformProfile::Starfield, Starfield catalog, driver bars, protocol pin or m48-8 smoke exists). Audit the mechanism that will carry them.
Regression guards: #3429 (closed; mechanism e2f99ad55): a same-extent update_rgba queues into dynamic_rgba.rs and is recorded in the next frame's command buffer (persistent image, per-slot staging, no fence wait); only an extent/format change recreates the image. A per-frame image create or fence wait on the overlay path is the regression (see Dim 5).
Parameters
--focus <dims> (default all 7) · --depth shallow|deep (deep traces archive → SWF/XML → VM/eval → host call → pixels → GPU).
Extra fields: Dimension: Profile & Bridge | AVM2 Adapter | Catalog & AVM1 Scanner | Resource Navigator | Render & Overlay Upload | Engine Wiring & Input | MenuXml & HUD Drivers. Profile: SkyrimAvm1 | Fallout4Avm2 | MenuXml | both | n/a.
Phase 1: Setup
mkdir -p /tmp/audit/ui; dedup per_audit-common.md; read the latestdocs/audits/AUDIT_UI_*.md(date D = delta baseline).cargo test -p byroredux-ui -p byroredux-menuxml; record pass/ignored. Data-gated tests return early (silent pass) without game data:crates/ui/tests/*_protocol.rs(BYROREDUX_SKYRIMSE_DATAviatest_paths::SKYRIM_SE_ENV,BYROREDUX_FO4_DATA), andavm1_host/tests.rs::installed_skyrim_host_calls_are_all_catalogedis#[ignore]d. The menuxml corpora (crates/menuxml/tests/{vanilla,fnv,fo3}_corpus.rs) are#[ignore]d since #4660 — run them with--ignored. A "verified" claim resting on one of these is unverified unless you ran it with data.- Count, do not trust, catalog sizes. Re-derive from
crates/ui/src/catalog.rs: Skyrim array = 142, allMeasuredsince 3df616c12 (#5274): 74 SkyUI-sourced + 68 #3103 corpus-sweep entries typed by their measured call-site arity; 126 commands + 16 requests (12 off the SkyUI sources, 4 by four-argument corpus sites:LoadDLC/RequestLoadingText#4720,GetMouseButtonForSetDestination/ShouldShowMod#5274). FO4 = 269 (138Measured+ 131HeuristicNamePrefix, #2966;HeuristicNamePrefixis now FO4-only). A stray 74 claiming the whole catalog, or any SkyrimHeuristicNamePrefixcount (the old 76/66 split), is doc rot.
Phase 2: Dimensions
Dim 1: Profile, Prepare & Host Bridge Transport
Paths: crates/ui/src/{profile,prepare,host,lib}.rs, crates/ui/src/host/tests.rs
First step: git log --since=D -- crates/ui/src/host.rs crates/ui/src/prepare.rs crates/ui/src/profile.rs crates/ui/src/lib.rs
Guards: host/tests.rs::a_response_handler_may_re_enter_its_own_bridge (handler is cloned out of the map before invocation; a refactor holding one borrow across the call re-introduces the panic); prepare.rs tests (an_archive_menu_open_decompresses_and_parses_once, a_loose_avm1_movie_is_decompressed_once_and_never_parsed, a_profile_mismatch_is_rejected_before_any_further_decode) via SwfDecodeCounts.
- Profile comes from the SWF header's AS3 bit (
ScaleformProfile::from_header,is_action_script_3()on the decompressed header; Ruffle's movie type isSwfMovieDatasince the b91e82160 nightly-2026-10-10 bump), never from--gameor archive provenance. A forcednew_with_profilethat contradicts the movie fails loudly or is test-only. Theexternal_interface_idused at registration equals the id the bridge filters on (mismatch = every host call vanishes silently). Malformed SWF returnsErr, not a half-built player. - Queue:
MAX_QUEUED_CALLS(1024) evicts oldest withpop_front, counts each eviction, warns once. A drained batch may be non-contiguous; the engine (app_frame.rs) latchesdropped_host_callsper menu andhost_call_gap_for_menuresets it on a menu swap. Regression = comparing against zero or dropping the latch. - Every bounded set (
callbacks,known_methods,unknown_methods,unanswered_methodsviainsert_bounded; the player's error/load lists) caps atMAX_DISTINCT_HOST_METHOD_NAMESand logs once at the trip. The adapter's lifecycle callbacks are exempt from the cap by exact membership inRESERVED_ENGINE_CALLBACKS(3 names, built from theavm2_hostconstants; worst case per set = cap + 3), so movie-registered__byro*spoofs cannot lock out__byroBGSCodeObjReady/Loaded/Destroy(8974dec12, #4719 retired the prefix-keyed 32-wide band). A prefix test reappearing anywhere is the regression (thescaleform_hud.rspush no longer has a skip at all, Dim 7). The guard sits ininsert_bounded, the single choke point for all sets; check a new set goes through it. - Starfield PlaceObject3 dialect shim (#4470, 9813af435):
prepare.rs::normalize_scaleform_dialectruns before everyprepare_moviestage (and on the navigator's dependency path) and sets MOVE only on PlaceObject3 records with neither MOVE nor HAS_CHARACTER, recursing into DefineSprite (depth-capped at 1: nested sprites are illegal SWF, and an uncapped descent overflowed the stack on a ~10 B/level crafted movie, 8872f3725, #5456;deeply_nested_sprites_do_not_overflow_the_stack,one_level_of_sprite_nesting_is_still_patched) and re-emitting FWS at a fixed length (prepare.rstests). It must stay length-preserving, fail closed on a malformed body (a CWS body overrunning its declared length is declined,cws_body_longer_than_its_declared_length_is_declined; the inflate is bounded by the header length), and own the prepare path's single decode (15b65b1b9, #5479): on CWS it inflates once and ALWAYS returns the uncompressed file (patched or not) soswf::decompress_swftakes the uncompressed copy path; on FWS it returnsSomeonly when a record changed; it returnsNonefor ZWS, conforming FWS and foreign containers (conforming_cws_returns_its_decode_with_an_untouched_body). A shim that inflates the raw bytes separately from the prepare decode is the #5479 regression (SwfDecodeCounts::decompressesis the real count). - One SWF decode per open:
prepare_movie→PreparedMovie; floor is two inflates + one tag walk. A stage taking raw bytes again is the regression;--menupassesprofile: Noneand readsUiManager::menu_profile()(no pre-extract-and-detect). SwfPlayer::invoke_callbackconsults ruffle's own external-interface registry (not only the bridge's recorded name set) before invoking, and prunes a stale name: AVM2addCallback(name, null)unregisters a callback behind the bridge, and invoking it would otherwise reach ruffle's unknown-callback warn and return aNullindistinguishable from a live callback's answer (86f5a1a0a).None= unknown/unregistered,Some(Null)= registered callback returning null.ScaleformValueround-trips in both directions with unrepresentable values becoming explicitNull;ScaleformHostDispatch::MissingResponse(Request with no response) stays distinct fromUnknown.- The bridge is
Rc/RefCell(Ruffle is single-threaded): nothing hands a clone to another thread;UiManagerstays out of the ECS resource set (ScaleformHudDiagis the console mirror).
Output:/tmp/audit/ui/dim_1.md
Dim 2: AVM2 Adapter Injection (FO4 BGSCodeObj), highest risk
Paths: crates/ui/src/avm2_host.rs
First step: git log --since=D -- crates/ui/src/avm2_host.rs
Guards: avm2_host.rs::no_other_injected_name_is_a_prefix_of_another, generated_adapter_pool_carries_no_abandoned_loader_strategy, crates/ui/tests/fallout4_hudmenu_protocol.rs (data-gated). Bytecode surgery on a third-party binary: a wrong constant-pool index yields a movie that loads and misbehaves, and no test fails.
- Every index written is one the rewriter added or verified (append-then-reference ordering). Injection is idempotent (menu reload / resize rebuild must not duplicate helpers or traits).
ScaleformHostObjectStatehas four variants (NotRequired,NotPresent,AdapterInjected,AdapterInjectedWithoutDestroyHook). The re-injection probe scans forDESTROYED_EVENT, not its strict prefixDESTROY_CALLBACK; the four destroy strings are emitted together or not at all. Bothscene.rsmenu-load log sites callUiManager::host_object_state()(elseNotPresentlogs like a healthy menu).- The destroy callback is registered only when the movie's class declares
onCodeObjDestruction;AdapterInjectedWithoutDestroyHookdoes not incrementcode_object_destruction_count(). - One shared helper normalises
BGSCodeObj.Method→Methodwhile keeping the transport name inScaleformHostCall(not 269 per-method copies). - Degradation: an unparseable ABC tag or no lifecycle-class match returns the original bytes with
NotPresent+log::warn!; the hardErris kept only forpatch_root_constructor(a partial rewrite would corrupt the SWF). No third branch may silently hard-fail.
Output:/tmp/audit/ui/dim_2.md
Dim 3: Catalog Fidelity & the AVM1 Scanner
Paths: crates/ui/src/{catalog,avm1_host}.rs, crates/ui/src/avm1_host/tests.rs
First step: git log --since=D -- crates/ui/src/catalog.rs crates/ui/src/avm1_host.rs
Guards (default lane): catalog.rs::{skyrim_catalog_is_sorted_and_unique, fallout4_catalog_is_sorted_and_unique, fallout4_catalog_provenance_split_matches_the_2966_sweep, skyrim_catalog_is_fully_measured, the_skyrim_measured_kinds_match_their_call_site_arities} (find is a case-sensitive binary_search_by, so sortedness under str::cmp is its prerequisite). Data-gated: installed_skyrim_host_calls_are_all_cataloged (Phase 1).
- Diff the re-derived counts (Phase 1 step 3) against every number in
docs/engine/ui.md, ROADMAP and prior reports; mismatch = doc rot. - Fourth-argument rule (#4720, 588085f20; #5274, 3df616c12): the AVM1 scanner records each resolved site's
CallMethodarity as a(min, max)pair (Avm1HostCallInventory::arg_counts), so a mixed-arity name fails the corpus sweep instead of collapsing onto the max; the sweep pins four-arg ⇔Requestfor every entry it sees (floor 16 four-arg methods so a quiet scanner fails loudly; synthetic pina_four_argument_site_records_the_callback_bearing_count). A new Skyrim entry must arriveMeasured; a name-prefix guess there is the regression. - Only FO4 sweep entries may be
HeuristicNamePrefix(its #2966 sweep was name-only, no arity); aCommand/Requestmis-type on a heuristic entry is lower confidence than on aMeasuredone.kindonly selects a diagnostic bucket (record_callqueues every call and answers from the configured response regardless), so a misclassification moves a name betweenunanswered_methods()andQueued; it cannot drop a call. Spot-check high-traffic methods against the installed-ABC/SkyUI evidence indocs/engine/ui.md. - AVM1 scanner:
GameDelegate.calloperands are read right-to-left; function bodies are nested byte slices and the constant pool descends with the walk; the stack is cleared on unmodelled actions (can lose a site, never invent one);Avm1HostCallInventory::unresolvedcounts recognised-but-unnamed sites. Runtime-named calls (this.callbackName) are invisible to a static walk; the catalog is a union, not a proof of completeness. unknown_methods()is live, not test-only: the frame driver warns once per(menu, method)(ui_reported_host_methods, bounded by the same cap) and the set is not cleared per frame or per menu load.
Output:/tmp/audit/ui/dim_3.md
Dim 4: Resource Navigator (archive-backed loads)
Paths: crates/ui/src/navigator.rs, crates/ui/src/player.rs (load side)
First step: git log --since=D -- crates/ui/src/navigator.rs
Guards: archive_menu_route_tests in byroredux/src/scene.rs (CLI-argument parser only); docs/smoke-tests/m48-menu-load.sh (Vulkan + game data) is the only end-to-end gate.
- Movies are untrusted content; URL→archive resolution is confined to the game archives (
archive_movie_urlrejects..escapes;resolve_urljoins against the movie URL). Verify absolute paths, non-archive schemes and network/filesystem escapes are refused (escape = HIGH); one backslash/case normalisation point matchingbyroredux/src/asset_provider/. - The local-executor pump runs from the same place the player ticks; a load future that is never polled hangs the menu silently unless surfaced via
resource_error.resource_loads()records misses as well as hits. - Bounds:
resource_loadsdedups byarchive_pathwith a hit counter and caps atMAX_RECORDED_RESOURCE_LOADS;import_asset_pathscaps atMAX_IMPORT_ASSET_PATHS(512). AnImportAssetsrecord with an empty URL is skipped (upstream Flash Player does not load it; joining an empty relative URL resolves to the movie itself and emitted a bogus archive path, 86f5a1a0a). One unresolvableImportAssetsURL is a recorded non-fatal error on both the root scan (PreparedMovie::root_import_errors) and nested scans; aResult-collecting.collect()reappearing turns one bad import into a whole-menu failure.
Output:/tmp/audit/ui/dim_4.md
Dim 5: Render Path, Overlay Upload & Device Lifecycle
Paths: crates/ui/src/{player,pacing}.rs, byroredux/src/app_frame.rs, byroredux/src/hud.rs (upload_frame, render_and_upload), crates/renderer/src/vulkan/{texture.rs,presentation.rs}, crates/renderer/src/texture_registry/{mod,dynamic_rgba}.rs, crates/renderer/src/vulkan/context/{post_passes,build_and_upload_instances}.rs
First step: git log --since=D -- crates/ui/src/player.rs crates/renderer/src/vulkan/presentation.rs crates/renderer/src/vulkan/texture.rs byroredux/src/hud.rs
Guards (source-shape, default lane): presentation.rs::ui_overlay_composites_after_the_tone_map_draw; lib.rs test on UiFrame::Hidden => {} in app_frame.rs; dynamic_rgba.rs::rgba_updates_use_the_frame_command_buffer_and_keep_descriptors. None can see pixel stride, image hazards or device lifetime; say so instead of claiming verification.
- Second GPU device: the UI creates its own wgpu device on Vulkan beside
VulkanContext. Quantify device + allocator +TextureTargetagainstdocs/engine/memory-budget.mdand feedback_vram_baseline; a per-menu device never reused is a leak class. Creation failure leaves the engine running with UI off, and a transient failure is not cached as permanent. UiManager::render()returnsUiFrame::{Fresh, Unchanged, Hidden}:Unchangedreuses the previous handle,Hiddenstops the UI quad (a hidden overlay must not keep compositing its last frame). Pixel format and row stride fromcapture_framematch the upload (a mismatch is a sheared overlaycargo testcannot see).UiManagersize,TextureTargetsize andregister_ui_quadextent move together on resize with no orphaned target.- Overlay composites after tone-mapping, inside the presentation pass at output resolution: the quad in the geometry pass (fog, bloom, TAA, FSR, ACES) is the regression. Both descriptor sets are rebound before the overlay draw (the tone-map draw binds an incompatible set 0); the UI pipeline is owned by
PresentationPipelineand rebuilt only in its recreate. MAX_INSTANCESoverflow:ui_instance_idxis captured immediately after the push and becomesNonewhen it lands past the cap, at capture time, not afterUiOverlayDrawis built (elsefirstInstancereads out of range;robust_buffer_accessis off).- In-frame dynamic RGBA upload (both HUD paths):
update_rgba(Scaleform, same extent) andwrite_rgba_inplace(MenuXml; extent/format mismatch is an error) both queue intoDynamicRgbaUploads— latest pixels coalesce per handle,record_pending_rgba_uploads(called frombegin_frame_recording.rs) copies them into the frame command buffer from a per-slot staging arena (MAX_FRAMES_IN_FLIGHT= 2,crates/renderer/src/vulkan/sync.rs) and requires the slot be fence-idle; an update is acknowledged only bysubmitted(slot), and a re-queue after recording resetsrecorded_slotso newer bytes are never marked done.MenuXmlHudoverwrites one texture in place (cfaccff57, #4892 retired the three-handle rotation; the registry barriers order earlier frames' sampling before the copy) andoverwrite_rgba_pixelsis deleted (its guard now pinsTexture::can_update_rgba). Staging failures (arena growth, mapped write, flush) degrade to a skipped overlay frame, warn once per episode and keep the dirty pixels queued (792c56de3, #4889;staging_failures_skip_the_overlay_frame_instead_of_failing_it). The growth attempt itself backs off during an episode (e588e5e67, #5528):GrowthBackoffretries afterRETRY_INTERVAL_FRAMES(120) frames that would have attempted growth, or at once for a smaller request than the one that failed, and any success clears it, so a persistent BAR shortage no longer pays one failingvkAllocateMemoryper frame (growth_retries_back_off_during_a_failure_episode,growth_gate_sits_before_the_allocation). The warning episode re-arms only at the end of a fully-recordedrecord_pending_rgba_uploads, never insubmitted(the skip frame itself submits Ok; 7e43a1550, #5275;a_submitted_skip_frame_does_not_rearm_the_episode_warn). Only the fence-idle sequencing check may propagate anErrintodraw_frame, so a new?in the recording path is the regression. A barrier finding here needs validation-layer or RenderDoc evidence (feedback_speculative_vulkan_fixes), not a reading. #4608:render_and_uploadrasterizes into a persistentupload_buffer(no per-changeto_vec). - Render pacing (#4717):
RenderPacer(pacing.rs, pure, clock = summeddt) gatesSwfPlayer::render's Ruffle pass + blocking readback: active cadence while the last readback changed or a host write (handle_input,set_mouse_in_stage,invoke_callback,set_stage_transparent→invalidate) woke it, one idle probe per interval after N identical passes. Opt-in (set_render_pacing): only the Scaleform HUD installs it (HUD_RENDER_PACING); modal menus stay unpaced. A new host-write path that skipsinvalidateleaves input visually stale for up to the idle interval. SwfPlayer(and its wgpu device) drops before the Vulkan allocator tears down; report the ordering here, the teardown finding in/audit-concurrency.
Output:/tmp/audit/ui/dim_5.md
Dim 6: Engine Wiring & Input Routing
Paths: byroredux/src/{ui_input,app_frame,app_events,scene,scaleform_hud}.rs, crates/ui/src/{input,lib}.rs, byroredux/src/main.rs (route_scaleform_window_event)
First step: git log --since=D -- byroredux/src/ui_input.rs byroredux/src/app_events.rs byroredux/src/scaleform_hud.rs
Guards: ui_input.rs tests (focus_release_does_not_recapture_or_rotate_on_return, mouse_look_requires_both_window_focus_and_capture).
- Focus is a two-state contract: a focused menu stops world input; releasing focus leaves no key stuck and does not recapture the cursor or rotate the camera on return (
release_world_inputfires once;apply_mouse_lookneeds window focus and capture). A menu open/close mid-strafe leaves the camera still. - Dispatch order: egui debug overlay → focused Scaleform menu → world.
is_debug_overlay_keyis checked before the UI swallow. egui's "consumed" flag is honoured only throughhonour_egui_consumed(app_events.rs), i.e. while an interactive egui surface shows (DebugUiState::captures_gameplay_input: F3 overlay or a player menu page): egui-winit consumes Tab unconditionally, so a bareif egui_consumed { return; }swallowed the Tab→Inventory binding every frame from boot (10cddd22e;consumed_is_only_honoured_while_an_interactive_surface_shows,window_event_gates_consumed_through_the_predicate), while event forwarding to egui stays unconditional (#2831). A menu-open frame leaking anInputActionedge intocombat_input_system/interaction_systemis a finding here. - winit →
UiInputEvent: physical vs logical key, mouse buttons, wheel units (line vs pixel) and IME each translated once;crates/ui/src/input.rsstays winit-free;set_mouse_in_stageupdates on every motion event including leaving the window. - HUD route is not a modal menu:
scaleform_hud::launchkeeps world input (set_input_focus(false)), sets the stage transparent, and yields to--menuwhen aUiManageralready exists — both--hudroutes (Scaleform and MenuXml) refuse through the one sharedhud::menu_owned_overlay_skip(a7d3222a8, #4723;a_menu_owned_overlay_blocks_both_hud_routes), so--menuplus a MenuXml--hudcan no longer strand a focused, never-tickedUiManager; the Scaleform probe runs first and a won Scaleform route suppresses the MenuXml launch (mutually exclusive per run).hud.offmust really hide it (UiFrame::Hiddenfor Scaleform, early return for MenuXml). - Native egui pages (
crates/debug-ui) are modal too: every path leaving the dialogue page (Close, Escape viatoggle_game_menu, the Inventory key viaopen_inventory_menu) runsApp::leave_dialogue_page, sampled before its visibility flip (14b0d33f2, #5409;every_native_page_leave_path_routes_through_the_shared_teardown). A new leave path that only flips visibility skips the OnEnd fragment (what OnEnd does is/audit-gameplay). - Per-frame cost is timed into
bench_ui_ns; tick+render happen once per frame and are skipped when no menu is loaded.
Output:/tmp/audit/ui/dim_6.md
Dim 7: MenuXml Track & the HUD Drivers
Paths: crates/menuxml/src/{eval,layout,raster,menu,profile}.rs, byroredux/src/{hud,scaleform_hud}.rs, byroredux/src/commands/hud.rs, docs/smoke-tests/m48-{4,5,6,7}-*-hud.sh
First step: git log --since=D -- crates/menuxml/src byroredux/src/hud.rs byroredux/src/scaleform_hud.rs docs/smoke-tests/
Guards: crates/menuxml/src/tests.rs (synthetic eval/layout/raster/graft, default lane); data-gated corpus tests and protocol pins (Phase 1 step 2); smoke gates m48-4-oblivion-hud.sh (health-fill run 158→53 px after a pin), m48-5-fo3-hud.sh (tick columns 226→68), m48-6-skyrim-hud.sh / m48-7-fo4-hud.sh (chrome on/off pixel diff; bars are not gated, vanilla feeds them by GFx object-path calls Ruffle cannot make). Smokes need a Vulkan device + game data; m48-menu-load.sh and every m48-4..7 script, m48-5-fnv-hud.sh included (c47b2d72d, #5276), exit 77 (SKIP) on missing data and are enforced by scripts/check-playable-smoke-contracts.sh (161ab5ae8, #4724). A new m48-* smoke missing from that checker's list is the regression.
- Eval: a trait's operator chain is a fold (each op sees the previous working value; booleans are 2/0;
onlyif); reads are on-demand and memoised per frame withMAX_READ_DEPTH(32) cutting cycles to 0, so a self-referential mod menu cannot hang. Engine overrides are keyed by lower-cased tile name: duplicate names (grafted prefabs, FO3hp_meter/ap_meter) share one override, so verify grafted tiles get unique names.ScreenTraitscropx/cropyimplement the 4:3-safe insets. Menu-texture lookup (menu.rs) keeps trying resolution-set candidates past an undecodable one and warns "present but undecodable" apart from "not found" (5c53ad9c7, #5400; the decoder itself is/audit-parsers). Decoded textures are shared by resolved archive path (theMenus\/Menus80\/textures\menus\spellings of one file read and decode once) and charged againstMAX_DECODED_TEXTURE_BYTES(1 GiB) from the DDS header alone (Rgba8::decoded_len) before allocating; over budget the filename resolves to nothing (cached, one warning per renderer) while already-decoded art keeps drawing (5dce486b6, #5658; the budget exceeds every game's whole menu-texture set, 979.9 MiB Oblivion). An uncached or per-spelling decode path is the regression; a new decode site must be count-bounded or charged. - Layout: locus chain for x/y, stable depth sort (document order tiebreak),
visible/alpha 0skip,clipwindowintersection. Negative, NaN or huge authored extents must not panic or explode work. - Raster:
blit/blit_subclamp each loop axis to the framebuffer viaaxis_span(in f32, before the int cast) and draw nothing on non-finite geometry/crop/zoom/clip; tiled texel math uses wrapping arithmetic (#4715;a_huge_finite_extent_visits_at_most_the_framebuffer,non_finite_geometry_draws_nothing,the_glyph_path_is_bounded_and_rejects_non_finite_geometry_too). A new draw path that loops on authored extents withoutaxis_spanis the regression. Layout depth sort usesf32::total_cmpwith the sign of zero folded (#4716,nan_depth_siblings_do_not_panic_the_depth_sort). Zoom contract:zoom<0stretches,0/100draws texels 1:1 clipped to the tile rect (stretch-as-default squeezes padded ribbon art into a constant-width bar),>0scales, crop applies after zoom;tiledrepeats 1:1 withcropxas a wrapping scroll (compass strip); text wrap uses the font's metrics. - HUD drivers (
hud.rs):fraction()readsPlayerEntity'sActorValuesonly (#4675;hud_bars_resolve_through_player_vitals_and_read_the_player_only), ceiling =ActorValue::undamaged()(base + permanent + temporary + theset_overridelayer) rather than a hand re-sum, else a routed playerSetBaseclamps the bar full (627149b62, #5508;fraction_max_includes_the_set_override_layer); bar tables carry canonical AVIF editor ids resolved per load throughPlayerVitals— a literal AVIF FormID in a bar table is the regression (the old 0x2C9/0x2D0 were test-fixture ids; FO4 0x2C9 is Experience). FO3 vs FNV is decided byfallout_profilefrom the load order (--master/--esm, then the base ESM beside the corpus), not the--esmname (#4718;a_new_vegas_dlc_launch_resolves_the_new_vegas_profile, smokem48-5-fnv-hud.sh). Unchanged-skip: change signature (heading quantised to 0.1°) plus a 33 msHUD_REFRESH_INTERVALcadence cap; the rate limiter must leave the signature unset so a throttled change is not lost. Compass heading =atan2(f.x, -f.z). Per-game facts live inHudGameProfile/MenuProfile(font table, strings source, AVIF editor ids; Skyrim's stamped 0x3E8-0x3EA): a per-game branch outside the profile is a doctrine violation (feedback_format_translation). - Scaleform HUD (
scaleform_hud.rs): vanilla Skyrimhudmenucalls exactlyGetButtonFromUserEvent/PlaySound/RegisterHUDComponents/myLogand registers only theGameDelegatecall/respondpair; FO4's makes zero host calls at idle.ScaleformHudDriver::tickmust copyHudControlout and drop the read guard in the same statement before itslive-bars write (same-thread deadlock, 2b5bdebd8;tick_publishes_live_bars_without_deadlocking_on_hud_control). The driver'supdateStats/RequestPlayerInfopoll handlers are per-game table data (ScaleformGame::poll_handlers+PollPayload, #4725; Skyrim two entries, FO4 none;poll_handlers_are_per_game_table_data) and with the push table answer SkyUI-class menus, not vanilla; they must stay catalog-consistent. The push carries no reserved-name skip since 431f74ce7 (#5278): its four gameplay arms (UpdateStats/UpdateCompass, both casings) end in_ => continue, which declines the adapter hooks. Its guardthe_push_table_never_targets_a_reserved_engine_callbackchecks only that those four appear inpushand are disjoint fromRESERVED_ENGINE_CALLBACKS. It does not fail on a new arm naming a reserved hook (despite its doc), so read the arm list; a__byroprefix literal anywhere is the regression. Response shapes are working hypotheses: unverified until a menu polls them. - Console pins (
byroredux/src/commands/hud.rs):hud.values/hud.headingreject NaN/±inf before clamp/rem_euclid(cb7874760, #5280); a pin path withoutis_finiteblanks the MenuXml bar (raster non-finite reject) or pushesNumber(NaN).
Output:/tmp/audit/ui/dim_7.md
Phase 3: Merge
Combine /tmp/audit/ui/dim_*.md into docs/audits/AUDIT_UI_<TODAY>.md (header per _audit-common.md Report finalization): Executive Summary (findings by severity; which profiles were traced end to end; re-derived catalog counts vs documented), Host Contract Matrix (profile × {detection, host object, transport, catalog size, live consumer, HUD route} verified/drifted), Findings (deduplicated), Pending-Row Readiness (invariants pinned for the unbuilt handlers / Papyrus bridge / menu stack; never listed as findings). Then rm -rf /tmp/audit/ui; suggest /audit-publish docs/audits/AUDIT_UI_<TODAY>.md (labels ui; add legacy-compat for menu-fidelity findings and game:* when one title's menus are the cause).