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.
À 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.
À é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ûrThe 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.
- •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.
Exemples
/audit-ui --depth deep — audit all seven dimensions, dedup against the latest docs/audits/AUDIT_UI_*.md, and write the new report./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)./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.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-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
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; their DDS census pins measured per-set counts (Obliviontextures\menus*1,975/1,712/1,789; FO3 / FNVtextures\interface\1,157 / 1,767; 065c9d31d, #5669). 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's own length and the absoluteMAX_SWF_BODY_BYTESceiling, 128 MiB, ~2x the largest shipped movie (FO76fonts_zhhant.swf, 65,902,267 B): a header claiming up to0xFFFFFFFFover 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) 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. The prune and the empty-URL skip are pinned (b7760069a, #5732:host/tests.rs::invoking_an_unregistered_callback_prunes_and_returns_noneover the real AVM2 fixture,navigator.rs::an_empty_import_assets_url_is_skipped_silently).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). 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.rswarns 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 byMAX_INCLUDE_SPLICES256,MAX_INCLUDE_BYTES256 KiB,MAX_INCLUDE_FRAGMENT_BYTES64 KiB,MAX_INCLUDE_FETCHED_BYTESandMAX_DOCUMENT_TILES16,384; every fetch chargesmax(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 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).
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 annexesCe 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.mdDepuis 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.
Bonnes pratiques React Hook Form
Developpement
Maîtrisez React Hook Form : évitez les anti-patterns, gérez les abonnements, et traitez correctement reset, dirty state et entrées contrôlées.
Migration Emotion vers Mantine
Developpement
Migrer les styled-components Emotion vers des composants Mantine avec style props et modules CSS. Pour convertir .styled.tsx ou supprimer les imports @emotion.
Détecteur de dérive de périmètre
Developpement
Analyse les diffs git par rapport à une intention pour détecter la dérive de périmètre, fichiers non liés et PR trop larges, puis suggère de garder, diviser ou justifier.