Audit de l'UI Scaleform et MenuXml

VérifiéSûr

Audit approfondi du contrat hôte de l'UI de jeu : pistes Scaleform/SWF (Ruffle, AVM1/AVM2, wgpu hors écran) et MenuXml (eval, layout, raster), orchestré par agents.

Spar Skills Guide Bot
DeveloppementAvancé
02/11/2026
Claude Code
#scaleform#game-ui#code-audit#rust#bethesda

Recommandé pour

Synthèse automatique

Commande d'audit orchestrée qui inspecte le contrat hôte de l'interface de jeu sur deux pistes, Scaleform (Ruffle/AVM1/AVM2, navigateur d'archives, upload d'overlay wgpu) et MenuXml (éval, layout, raster), avec leurs pilotes de HUD.

Points forts

  • Découpage explicite en 7 dimensions avec un agent Task par dimension et un plafond de 3 en parallèle, plus un format de constat et une déduplication partagés via _audit-common.md.
  • Ancrage fort dans des sources de vérité nommées (docs/engine/ui.md, docs/smoke-tests/README.md) et dans des garde-fous de régression précis comme #3429 sur update_rgba same-extent.
  • Délimite clairement les responsabilités et les renvois vers d'autres audits (concurrency, safety, parsers, gameplay) au lieu d'empiéter dessus.
  • Fournit une liste explicite de ce qui n'est délibérément pas construit, avec instruction de re-vérifier plutôt que de faire confiance, ce qui évite de fausses conclusions.

Limites

  • Fortement couplé à ce dépôt : chemins de crates, numéros d'issue, noms de tests et documents internes, donc peu réutilisable ailleurs.
  • Plusieurs tests de protocole et corpus sont data-gated ou #[ignore] et passent silencieusement sans données de jeu, ce qui peut donner une fausse impression de couverture.
  • Le fichier est tronqué en pleine Phase 1 (fin de l'étape 2 sur le census DDS), donc la procédure complète n'est pas intégralement lisible en l'état.
Quand l'utiliser

À utiliser pour un audit en profondeur du chemin interface/host contract Scaleform ou MenuXml dans ce dépôt, notamment après une modification du rendu d'overlay ou du pont AVM.

Quand l'éviter

À éviter sur un autre projet, ou pour une simple revue de code ciblée ne nécessitant ni orchestration multi-agents ni comparaison à un rapport d'audit antérieur.

Analyse de sécurité

Sûr
Score qualité88/100

The content is an orchestration document for auditing a game engine's UI layer; it performs read-only analysis plus ordinary dev commands (git log, cargo test, writes under /tmp and docs/audits). There is no obfuscation, no network exfiltration, no credential or secret access, no destructive system-level deletes, and no attempt to override the reviewer. The only shell deletions target a fixed temp scratch directory. Its treatment of untrusted assets is conservative and bounded, so listing it poses no meaningful execution risk.

Points d'attention
  • •No declared allowed-tools, though the workflow requires shell (mkdir, rm -rf on a scoped /tmp path), git, and cargo; the missing declaration is a minor governance/metadata gap rather than an execution risk.
  • •Step 1 (`mkdir -p /tmp/audit/ui`) and Phase 3 (`rm -rf /tmp/audit/ui`) are scoped to a fixed temp directory, not user or repo data.
  • •The skill intentionally processes untrusted third-party binaries (SWF/ABC/MenuXml archives) and explicitly constrains them: path-escape rejection, bounded inflate/decode budgets, byte ceilings, fail-closed behavior. This is defensive design, not a hazard.
Comment cette analyse est faite

Exemples

Full deep audit
/audit-ui --depth deep — audit all seven dimensions, dedup against the latest docs/audits/AUDIT_UI_*.md, and write the new report.
Overlay regression check
/audit-ui --focus "Render & Overlay Upload" --depth deep — verify that a same-extent update_rgba never recreates the image or fence-waits on the overlay path (guard #3429).
MenuXml and HUD drivers only
/audit-ui --focus "MenuXml & HUD Drivers" --depth shallow, and run the vanilla/fnv/fo3 menuxml corpora with --ignored to record their status.

description: "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" argument-hint: "--focus <dimensions> --depth shallow|deep"

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.rs UiManager, deliberately not an ECS Resource; profile.rs; prepare.rs; host.rs; avm1_host.rs Skyrim scanner; avm2_host.rs FO4 BGSCodeObj ABC injection; catalog.rs; navigator.rs; player.rs offscreen wgpu; pacing.rs render pacer; input.rs); protocol pins crates/ui/tests/{hudmenu_protocol,fallout4_hudmenu_protocol}.rs.
  • MenuXml: crates/menuxml/src/ (eval, layout, raster, menu, profile audited here; parse, tex, font are 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/--hud launch), renderer crates/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/wgpu unsafe /audit-safety; MenuXml XML/DDS/.fnt parse 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-15 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

  1. mkdir -p /tmp/audit/ui; dedup per _audit-common.md; read the latest docs/audits/AUDIT_UI_*.md (date D = delta baseline).
  2. 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_DATA via test_paths::SKYRIM_SE_ENV, BYROREDUX_FO4_DATA), and avm1_host/tests.rs::installed_skyrim_host_calls_are_all_cataloged is #[ignore]d. The menuxml corpora (crates/menuxml/tests/{vanilla,fnv,fo3}_corpus.rs) are #[ignore]d since #4660 — run them with --ignored; their DDS census pins measured per-set counts (Oblivion textures\menus* 1,975/1,712/1,789; FO3 / FNV textures\interface\ 1,157 / 1,767; 065c9d31d, #5669). A "verified" claim resting on one of these is unverified unless you ran it with data.
  3. Count, do not trust, catalog sizes. Re-derive from crates/ui/src/catalog.rs: Skyrim array = 142, all Measured since 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 (138 Measured + 131 HeuristicNamePrefix, #2966; HeuristicNamePrefix is now FO4-only). A stray 74 claiming the whole catalog, or any Skyrim HeuristicNamePrefix count (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 is SwfMovieData since the b91e82160 nightly-2026-10-10 bump), never from --game or archive provenance. A forced new_with_profile that contradicts the movie fails loudly or is test-only. The external_interface_id used at registration equals the id the bridge filters on (mismatch = every host call vanishes silently). Malformed SWF returns Err, not a half-built player.
  • Queue: MAX_QUEUED_CALLS (1024) evicts oldest with pop_front, counts each eviction, warns once. A drained batch may be non-contiguous; the engine (app_frame.rs) latches dropped_host_calls per menu and host_call_gap_for_menu resets it on a menu swap. Regression = comparing against zero or dropping the latch.
  • Every bounded set (callbacks, known_methods, unknown_methods, unanswered_methods via insert_bounded; the player's error/load lists) caps at MAX_DISTINCT_HOST_METHOD_NAMES and logs once at the trip. The adapter's lifecycle callbacks are exempt from the cap by exact membership in RESERVED_ENGINE_CALLBACKS (3 names, built from the avm2_host constants; 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 (the scaleform_hud.rs push no longer has a skip at all, Dim 7). The guard sits in insert_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_dialect runs before every prepare_movie stage (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.rs tests). 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's own length and the absolute MAX_SWF_BODY_BYTES ceiling, 128 MiB, ~2x the largest shipped movie (FO76 fonts_zhhant.swf, 65,902,267 B): a header claiming up to 0xFFFFFFFF over a ~4 MB zlib bomb is declined before any inflate, d601b45ab, #5719; cws_header_claiming_u32_max_over_a_bomb_is_declined, swf_body_ceiling_clears_the_corpus_and_stops_short_of_u32, cws_within_the_ceiling_inflates), and own the prepare path's single decode (15b65b1b9, #5479): on CWS it inflates once and ALWAYS returns the uncompressed file (patched or not) so swf::decompress_swf takes the uncompressed copy path; on FWS it returns Some only when a record changed; it returns None for 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::decompresses is 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; --menu passes profile: None and reads UiManager::menu_profile() (no pre-extract-and-detect).
  • SwfPlayer::invoke_callback consults ruffle's own external-interface registry (not only the bridge's recorded name set) before invoking, and prunes a stale name: AVM2 addCallback(name, null) unregisters a callback behind the bridge, and invoking it would otherwise reach ruffle's unknown-callback warn and return a Null indistinguishable from a live callback's answer (86f5a1a0a). None = unknown/unregistered, Some(Null) = registered callback returning null. The prune and the empty-URL skip are pinned (b7760069a, #5732: host/tests.rs::invoking_an_unregistered_callback_prunes_and_returns_none over the real AVM2 fixture, navigator.rs::an_empty_import_assets_url_is_skipped_silently).
  • ScaleformValue round-trips in both directions with unrepresentable values becoming explicit Null; ScaleformHostDispatch::MissingResponse (Request with no response) stays distinct from Unknown.
  • The bridge is Rc/RefCell (Ruffle is single-threaded): nothing hands a clone to another thread; UiManager stays out of the ECS resource set (ScaleformHudDiag is 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).
  • ScaleformHostObjectState has four variants (NotRequired, NotPresent, AdapterInjected, AdapterInjectedWithoutDestroyHook). The re-injection probe scans for DESTROYED_EVENT, not its strict prefix DESTROY_CALLBACK; the four destroy strings are emitted together or not at all. Both scene.rs menu-load log sites call UiManager::host_object_state() (else NotPresent logs like a healthy menu).
  • The destroy callback is registered only when the movie's class declares onCodeObjDestruction; AdapterInjectedWithoutDestroyHook does not increment code_object_destruction_count().
  • One shared helper normalises BGSCodeObj.Method → Method while keeping the transport name in ScaleformHostCall (not 269 per-method copies).
  • Degradation: an unparseable ABC tag or no lifecycle-class match returns the original bytes with NotPresent + log::warn!; the hard Err is kept only for patch_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 CallMethod arity 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 ⇔ Request for every entry it sees (floor 16 four-arg methods so a quiet scanner fails loudly; synthetic pin a_four_argument_site_records_the_callback_bearing_count). A new Skyrim entry must arrive Measured; a name-prefix guess there is the regression.
  • Only FO4 sweep entries may be HeuristicNamePrefix (its #2966 sweep was name-only, no arity); a Command/Request mis-type on a heuristic entry is lower confidence than on a Measured one. kind only selects a diagnostic bucket (record_call queues every call and answers from the configured response regardless), so a misclassification moves a name between unanswered_methods() and Queued; it cannot drop a call. Spot-check high-traffic methods against the installed-ABC/SkyUI evidence in docs/engine/ui.md.
  • AVM1 scanner: GameDelegate.call operands 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::unresolved counts 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_url rejects .. escapes; resolve_url joins against the movie URL). Verify absolute paths, non-archive schemes and network/filesystem escapes are refused (escape = HIGH); one backslash/case normalisation point matching byroredux/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_loads dedups by archive_path with a hit counter and caps at MAX_RECORDED_RESOURCE_LOADS; import_asset_paths caps at MAX_IMPORT_ASSET_PATHS (512). An ImportAssets record 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 unresolvable ImportAssets URL is a recorded non-fatal error on both the root scan (PreparedMovie::root_import_errors) and nested scans; a Result-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 + TextureTarget against docs/engine/memory-budget.md and 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() returns UiFrame::{Fresh, Unchanged, Hidden}: Unchanged reuses the previous handle, Hidden stops the UI quad (a hidden overlay must not keep compositing its last frame). Pixel format and row stride from capture_frame match the upload (a mismatch is a sheared overlay cargo test cannot see). UiManager size, TextureTarget size and register_ui_quad extent 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 PresentationPipeline and rebuilt only in its recreate.
  • MAX_INSTANCES overflow: ui_instance_idx is captured immediately after the push and becomes None when it lands past the cap, at capture time, not after UiOverlayDraw is built (else firstInstance reads out of range; robust_buffer_access is off).
  • In-frame dynamic RGBA upload (both HUD paths): update_rgba (Scaleform, same extent) and write_rgba_inplace (MenuXml; extent/format mismatch is an error) both queue into DynamicRgbaUploads — latest pixels coalesce per handle, record_pending_rgba_uploads (called from begin_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 by submitted(slot), and a re-queue after recording resets recorded_slot so newer bytes are never marked done. MenuXmlHud overwrites one texture in place (cfaccff57, #4892 retired the three-handle rotation; the registry barriers order earlier frames' sampling before the copy) and overwrite_rgba_pixels is deleted (its guard now pins Texture::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): GrowthBackoff retries after RETRY_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 failing vkAllocateMemory per 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-recorded record_pending_rgba_uploads, never in submitted (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 an Err into draw_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_upload rasterizes into a persistent upload_buffer (no per-change to_vec).
  • Render pacing (#4717): RenderPacer (pacing.rs, pure, clock = summed dt) gates SwfPlayer::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 skips invalidate leaves 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_input fires once; apply_mouse_look needs 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_key is checked before the UI swallow. egui's "consumed" flag is honoured only through honour_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 bare if 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 an InputAction edge into combat_input_system / interaction_system is 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.rs stays winit-free; set_mouse_in_stage updates on every motion event including leaving the window.
  • HUD route is not a modal menu: scaleform_hud::launch keeps world input (set_input_focus(false)), sets the stage transparent, and yields to --menu when a UiManager already exists — both --hud routes (Scaleform and MenuXml) refuse through the one shared hud::menu_owned_overlay_skip (a7d3222a8, #4723; a_menu_owned_overlay_blocks_both_hud_routes), so --menu plus a MenuXml --hud can no longer strand a focused, never-ticked UiManager; the Scaleform probe runs first and a won Scaleform route suppresses the MenuXml launch (mutually exclusive per run). hud.off must really hide it (UiFrame::Hidden for Scaleform, early return for MenuXml).
  • Native egui pages (crates/debug-ui) are modal too: every path leaving the dialogue page (Close, Escape via toggle_game_menu, the Inventory key via open_inventory_menu) runs App::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 with MAX_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, FO3 hp_meter/ap_meter) share one override, so verify grafted tiles get unique names. ScreenTraits cropx/cropy implement 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 (the Menus\ / Menus80\ / textures\menus\ spellings of one file read and decode once) and charged against MAX_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). Refusals are memoised per resolved archive path and consulted before the fetch, so one over-budget file is extracted for a budget decision at most once however many spellings name it; a refused candidate continues to its siblings (a smaller resolution-set variant can still serve) and the warning no longer claims later textures are refused, the check being per texture (4c979297b, #5693). When a lower-priority resolution set serves a tile because the preferred copy is undecodable, menu.rs warns once per authored spelling naming the undecodable paths (64bca63af, #5670; font atlases take the first present atlas and have no fallback to signal). An uncached, per-spelling or post-fetch-only decode path is the regression; a new decode site must be count-bounded or charged. Include splicing (parse.rs): bounded by MAX_INCLUDE_SPLICES 256, MAX_INCLUDE_BYTES 256 KiB, MAX_INCLUDE_FRAGMENT_BYTES 64 KiB, MAX_INCLUDE_FETCHED_BYTES and MAX_DOCUMENT_TILES 16,384; every fetch charges max(len, 1 KiB) against the fetch budget so the per-document extract count stays bounded, and an exhausted-budget cause (budget/bytes/fetched/tiles) warns once per document naming the first offending path while per-include causes (cycle/oversize/missing) keep per-spelling warnings (547d3c385, #5668). The cycle check runs only on the resolved include after the per-document cache lookup, so menus\x.xml including x.xml no longer drops the distinct menus\prefabs\x.xml as a false cycle (ba252c482, #5329; a_distinct_higher_priority_include_is_not_a_cycle, self_including_fragment_costs_one_fetch).
  • Layout: locus chain for x/y, stable depth sort (document order tiebreak), visible/alpha 0 skip, clipwindow intersection. Negative, NaN or huge authored extents must not panic or explode work.
  • Raster: blit/blit_sub clamp each loop axis to the framebuffer via axis_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 without axis_span is the regression. Layout depth sort uses f32::total_cmp with the sign of zero folded (#4716, nan_depth_siblings_do_not_panic_the_depth_sort). Zoom contract: zoom<0 stretches, 0/100 draws texels 1:1 clipped to the tile rect (stretch-as-default squeezes padded ribbon art into a constant-width bar), >0 scales, crop applies after zoom; tiled repeats 1:1 with cropx as a wrapping scroll (compass strip); text wrap uses the font's metrics.
  • HUD drivers (hud.rs): fraction() reads PlayerEntity's ActorValues only (#4675; hud_bars_resolve_through_player_vitals_and_read_the_player_only), ceiling = ActorValue::undamaged() (base + permanent + temporary + the set_override layer) rather than a hand re-sum, else a routed player SetBase clamps the bar full (627149b62, #5508; fraction_max_includes_the_set_override_layer); bar tables carry canonical AVIF editor ids resolved per load through PlayerVitals — 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 by fallout_profile from the load order (--master/--esm, then the base ESM beside the corpus), not the --esm name (#4718; a_new_vegas_dlc_launch_resolves_the_new_vegas_profile, smoke m48-5-fnv-hud.sh). Unchanged-skip: change signature (heading quantised to 0.1°) plus a 33 ms HUD_REFRESH_INTERVAL cadence 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 in HudGameProfile/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 Skyrim hudmenu calls exactly GetButtonFromUserEvent/PlaySound/RegisterHUDComponents/myLog and registers only the GameDelegate call/respond pair; FO4's makes zero host calls at idle. ScaleformHudDriver::tick must copy HudControl out and drop the read guard in the same statement before its live-bars write (same-thread deadlock, 2b5bdebd8; tick_publishes_live_bars_without_deadlocking_on_hud_control). The driver's updateStats/RequestPlayerInfo poll 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 guard the_push_table_never_targets_a_reserved_engine_callback checks only that those four appear in push and are disjoint from RESERVED_ENGINE_CALLBACKS. It does not fail on a new arm naming a reserved hook (despite its doc), so read the arm list; a __byro prefix 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.heading reject NaN/±inf before clamp/rem_euclid (cb7874760, #5280); a pin path without is_finite blanks the MenuXml bar (raster non-finite reject) or pushes Number(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).

Du fichier au premier essai

Le téléchargement contient exactement le contenu affiché sur cette fiche. Cette copie peut différer de la dernière version du dépôt source.

Consulter la source et ses fichiers annexes

Ce téléchargement contient uniquement SKILL.md. Les scripts, références, modèles et dépendances éventuels ne sont pas inclus. Récupérez les fichiers nécessaires depuis la source en conservant leur arborescence avant de poursuivre.

1. Télécharger le fichier

Télécharger SKILL.md

Depuis la racine d’un projet de test, enregistrez le fichier sous .skill-downloads/scaleform-and-menuxml-ui-audit-mvg6ty07/SKILL.md, ou utilisez la commande suivante. Ce dossier sert à examiner la skill avant son installation.

Commande pour macOS/Linux ou WSL : un shell POSIX et curl sont nécessaires. Sous Windows sans WSL, utilisez le bouton de téléchargement et créez le dossier manuellement.

mkdir -p '.skill-downloads/scaleform-and-menuxml-ui-audit-mvg6ty07' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/scaleform-and-menuxml-ui-audit-mvg6ty07/download' --output '.skill-downloads/scaleform-and-menuxml-ui-audit-mvg6ty07/SKILL.md.part' && mv '.skill-downloads/scaleform-and-menuxml-ui-audit-mvg6ty07/SKILL.md.part' '.skill-downloads/scaleform-and-menuxml-ui-audit-mvg6ty07/SKILL.md'

2. Vérifier les prérequis

Lisez SKILL.md avant de l’utiliser. Si le fichier cite des scripts ou services externes, examinez-les et préparez les accès nécessaires dans votre environnement de test.

Aucune section de prérequis reconnue dans ce fichier. Cela ne signifie pas qu’il est autonome : les bibliothèques, outils et accès peuvent être cités ailleurs dans le contenu ou dans le dépôt source.

Les plateformes de la fiche sont des indications du catalogue. Les chemins ci-dessous suivent la documentation des outils ; ils ne prouvent pas que cette skill y a été exécutée. Aucune version minimale de ces outils n’est attestée pour cette fiche.

3. Charger la skill

Le fichier ne fournit pas un en-tête name/description exploitable pour cette procédure native. Le téléchargement reste disponible pour lecture ; utilisez le mode manuel ci-dessous.

Mode manuel ou autre outil : joignez SKILL.md à une conversation, ou ouvrez-le dans un agent capable de lire les fichiers. Ajoutez les ressources annexes nécessaires. Il n’existe pas de dossier universel reconnu par tous les outils.

4. Faire un premier essai

Utilisez une entrée courte et fictive : un endpoint d’API, un petit document ou un brief d’interface. Dans une nouvelle conversation, joignez le fichier ou donnez son chemin à l’agent, puis adaptez cette demande :

Lis .skill-downloads/scaleform-and-menuxml-ui-audit-mvg6ty07/SKILL.md et applique ses instructions à cet exemple : [décrire une entrée de test]. Indique les fichiers, dépendances ou accès qui manquent. Produis le résultat demandé par la skill sans inventer de données ni exécuter de script non examiné.

Résultat attendu : une sortie correspondant aux instructions de la skill, ou une liste précise des prérequis manquants. Une simple confirmation de lecture ne valide pas le premier usage. Si une ressource externe est indisponible, ce parcours reste à tester.

Skills similaires