#pragma once // Shared state of the native scene renderer, used by both of its translation // units: skate3_native_scene.cpp (game-thread capture + frame build) and // skate3_native_scene_gpu.cpp (render-thread RHI drawing). Everything here is // written on one thread and read on another as documented per member; inline // variables keep a single instance across the TUs. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "skate3_native_scene.h" REXCVAR_DECLARE(bool, skate3_native_render_scene); REXCVAR_DECLARE(bool, skate3_native_render_scene_ring); namespace skate3::native_scene { inline std::mutex g_scene_mutex; // Published scene: immutable once published (BuildFrameScene builds a fresh // one per guest frame). shared_ptr so the render thread grabs a reference // under the mutex instead of deep-copying ~800 items (with per-item heap // vectors) every frame. inline std::shared_ptr g_scene; inline uint64_t g_generation = 0; // Steady-clock stamp of the last scene publish (BuildFrameScene only // publishes when a perspective view submitted this frame). YieldForMenus // uses its freshness to tell the in-game pause menu (the world keeps // resubmitting behind the menu) apart from loading screens and the boot // frontend (publishes stop), for skate3_native_render_scene_pause_native. inline std::atomic g_last_publish_ns{-1}; // Debug-dialog cache flushes: consumed at the top of RenderScene so texture/ // mesh-affecting toggles (mip chains, 565 fixes, ...) take effect immediately // instead of only for newly streamed content. inline std::atomic g_flush_textures{false}; inline std::atomic g_flush_meshes{false}; // Off-screen retention clear request (see g_retained_items further down): // set on the menus/loading flip (render thread) and the F5 re-enable, // consumed at the top of BuildFrameScene's retention block (guest thread). inline std::atomic g_retained_clear{false}; // Camera/bone signal recorders + synthetic-pan probe + offline recording: // state and file writers live in native/skate3_native_diagnostics.cpp // (native/skate3_native_diag.h); capture call sites below read per-frame // locals and stay here. inline std::atomic g_guest_base{nullptr}; inline std::atomic g_frames_rendered{0}; // Loading -> gameplay takeover. Armed while the game reports menus/loading // (and by the F5 enable toggle, hence the atomic); the loading-screen // prewarm (below) does the decode heavy lifting behind the load, and the // FIRST substantial post-load scene renders natively immediately, no // emulated-gameplay stretch. The frames right after takeover run a budgeted // "settle" decode pass for whatever prewarm missed (dynamic entities, // late-registered textures); the draw path's miss budgets are clamped while // settling so leftovers render white/skip for a frame instead of freezing // the takeover frame. Everything except the armed flag is touched on the // render thread only, inside RenderScene. inline std::atomic g_warmup_armed{true}; // BuildFrameScene stops publishing during loading (no submissions / no // perspective view), so g_scene holds the PREVIOUS map's scene through the // whole loading screen. Only scenes published at or after this generation // (stamped while the loading screen shows) are eligible for takeover; // rendering the stale scene shows old-map garbage at the flip. inline uint64_t g_warmup_fresh_generation = 0; // Settle window: frames (of the native frame counter) still running the // per-frame settle decode pass after a takeover. inline uint64_t g_settle_until_frame = 0; // Loading-screen prewarm queue: world meshes pushed by the // tROptiMeshData::Unfix hook (the rw-arena LOAD-time pointer resolve; it // fires once per world mesh as its arena streams in, i.e. exactly during // loading screens and gameplay world streaming). The render thread drains // the queue in RenderScene: with the prewarm budget behind the loading // screen (where the heavy lifting belongs), with the warmup budget's // leftover while warming, and with a small fixed slice during gameplay // (streamed-in areas decode before their first draw instead of hitching // it). Entries whose buffer objects are not initialized yet retry a bounded // number of drains. struct PrewarmEntry { uint32_t mesh; uint16_t retries; // Environment-cube entry (mesh == 0, tex != 0): the one object-keyed // texture path left (see EnqueueCubeMiss). uint32_t tex = 0; // Fetch-words entry (mesh == 0, tex == 0, wkey != 0): a content-store // miss decoded from the captured stable words snapshot (see // EnqueueWordsMiss): every 2D/3D texture miss and heal. uint64_t wkey = 0; uint32_t words[6] = {}; // Environment-cube entry (tex != 0 && cube): a cube-cache miss: a single // cube decode measured up to ~100 ms, the largest remaining traversal // hitch when a vehicle / reflective area streamed in. bool cube = false; // Draw-path miss (vs speculative prewarm registration): the result is // visible RIGHT NOW, so the commit takes it this frame regardless of the // gameplay per-frame commit cap. bool miss = false; // 2D/HUD overlay miss (large-art async routing, see // skate3_native_render_scene_2d_async_px): the commit skips the // payload-stability verify for these; APT re-rasterizes animating UI // art every guest frame, so "payload moved between decode and commit" is // the NORMAL state mid-animation and the verify would reject every // commit, freezing the element (spinners, ramping fades). The per-frame // content probe in the 2D resolve is the heal path for torn reads, the // same exposure the old inline decode had. bool ui = false; }; inline std::mutex g_prewarm_mutex; inline std::condition_variable g_prewarm_cv; // wakes the decode workers inline std::vector g_prewarm_queue; // Draw-path misses (EnqueueMeshMiss/EnqueueWordsMiss/EnqueueCubeMiss): // content that is visible RIGHT NOW (white / skipped), // served FIFO with strict priority over the speculative registration // backlog in g_prewarm_queue. Guarded by g_prewarm_mutex. inline std::vector g_miss_queue; // Steady-state draw-path misses currently being decoded on the workers (one // in-flight decode per key; the commit erases). Guarded by g_prewarm_mutex. // This is the panning-hitch fix: a texture decode averages ~10 ms (max ~70 ms // measured) and a pan surfaces dozens of new textures/meshes in one frame; // decoding them inline on the render thread was the lag spike. Misses now // render white / keep the previous decode for the 1-3 frames the workers // need. inline std::unordered_set g_miss_inflight_mesh; inline std::unordered_set g_miss_inflight_tex; inline std::unordered_set g_miss_inflight_words; // Dynamic-payload decode jobs: cloth-sim garments (ropa) have their vertex // buffers rewritten by the CPU sim EVERY frame, so their mesh decode used to // run inline on the render thread every frame (~0.7 ms per garment; a // 4-garment outfit = ~2.9 ms = the 160 fps cap during real play once // everything else was fixed; the demo character wears no ropa, which hid // it). The guest thread snapshots the changed payload at publish time; a // worker converts it into fresh upload buffers; PrewarmCommit swaps the mesh // cache entry. The render thread keeps drawing the previous decode, one // frame of cloth lag, invisible (the ropa sim is frame-paced anyway). struct DynDecodeJob { DrawItem item; // offsets/formats/fingerprint (bones not needed) uint64_t seq = 0; // enqueue order; the commit drops stale results std::vector vb; // big-endian guest payload snapshots std::vector ib; }; inline std::vector g_dyn_jobs; // FIFO; guarded by g_prewarm_mutex // Push-side dedupe: clone instances share one tRModelData, so the same mesh // registers many times per load, but a permanent per-address set silently // DISABLED the prewarm for re-streamed content (the streamer reuses the // same mesh/texture object addresses all session): after a few minutes // every re-registered mesh was "seen", its re-decode was skipped, and every // streamed texture surfaced as a draw-path first-sight miss instead: // 388 white-flash textures in one 45 s traversal = the // residual medium-distance pop-in. Now a mesh -> last-queued-frame map: // clone bursts still dedupe inside the window, re-streams past it re-queue // (registration is the game's own "content changed here" signal, see // InvalidateCachedItem above). Cleared when the game enters menus/loading. inline std::unordered_map g_prewarm_seen; // Texture-object dedupe for the workers (they cannot read the render // thread's g_r caches): address -> fetch-words key at last staging; a // words change at a reused address re-stages, an unchanged one skips. inline std::unordered_map g_prewarm_tex_seen; inline std::atomic g_prewarm_done{0}; // entries fully warm-decoded inline std::atomic g_prewarm_dropped{0}; // entries given up on inline std::atomic g_prewarm_workers_started{false}; // tInstance::m_pRModel offset, confirmed at runtime by the material // cross-check probe in OnAddRenderInstance (Skate 2 layout: +0x78). inline std::atomic g_instance_rmodel_offset{0}; // guid -> guest renderengine::Texture, from the RegisterTexture hook. Keys // masked of the top bit: material channel guids carry an extra flag bit // relative to the registered cAssetIDs. constexpr uint64_t kGuidMask = 0x7FFFFFFFFFFFFFFFull; inline std::mutex g_texture_map_mutex; inline std::unordered_map g_texture_map; // VS constant staging bank (bank id 0x4000, device+1920). // D3D::SetPending_AluConstants(device, dirty group mask, bank, ptr) is // called from inside the guest Draw* functions; ptr is the device's // positional 256-register shadow (partial uploads leave the rest intact). // Two verified layouts: pre-pass keeps the per-draw transform / bone // palette at c4; main-pass keeps camera at c4, palette at c7, rigid world // at c8..c11 (see BankPaletteBase / BankRigidWorld). Because the dynamic // capture runs right after RenderMesh returns, the bank holds exactly that // mesh's constants when its draws ran inline. inline std::atomic g_vs_bank{0}; // Pixel constant shadow bank (bank id 0x4400, device+6016): material colors // (e.g. the CAS hair tint) are staged here per draw. inline std::atomic g_ps_bank{0}; // Global per-frame fog rows (see FrameScene::fog_ramp): main-pass VS banks // keep the camera at c4 and the frame's fog params at c5 (ramp) / c6 (color) // - identical across every main-pass draw of a frame (verified from a // recorded draw stream). OnDrawDone grabs them from the first 3D draw whose // c4 matches the last built scene's camera; BuildFrameScene publishes them // and re-arms the capture. All on the guest render thread. inline float g_fog_cam[3] = {}; inline float g_fog_rows[8] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; inline bool g_fog_have = false; inline bool g_fog_frame_done = false; // Sky-dome viewpos height (see FrameScene::sky_height): sky.fx's VS adds // g_vViewPos to every dome vertex, but the game feeds the SKY draw a viewpos // whose Y is a fixed level elevation (165.0 in every Port Carverton capture) // instead of the camera's; the baked skyline must not bob with the skater. // Captured from the sky bank's unique signature: c4.xz matches the camera // while c4.y sits far above it (every other main-pass draw has c4 == camera; // water-reflection passes mirror y DOWNWARD and are excluded by dy > 0). inline float g_sky_height = 165.0f; // every Port Carverton capture to date inline bool g_sky_have = false; inline bool g_sky_frame_done = false; // Sky sun rows from the same draw's PIXEL bank (see FrameScene::sky_sun): // light dir (c0.xyz), sun angular scale (c4.x), pre-tone multiplier (c4.y), // exposure (c3.x). Persist like g_sky_height once captured. inline float g_sky_sun[6] = {}; inline bool g_sky_sun_have = false; // The CAPTURED (un-overridden) sun direction, published each frame for the // debug dialog: enabling the sun override seeds its azimuth/elevation from // this so the sun starts at its true position instead of jumping. inline std::atomic g_sun_captured[3] = {0.0f, 1.0f, 0.0f}; // UI background blur (see FrameScene::ui_blur and kBlurShaderSource): while // a frontend popup is up the game appends blur_hBlur/vBlur + basictex passes // after the postfx uber. g_ui_blur holds the captured kernel scale (PS c0.x, // 8 in every capture); g_ui_blur_seen latches per frame on the blur_hBlurPS // draw and is cleared at publish; the pass chain only exists while the // popup is actually up, so this can never stick on. inline float g_ui_blur = 8.0f; inline bool g_ui_blur_seen = false; // Blur modulate color (the blur passes' PS c1): both blur ucodes end in // `mul oC0, r0, c1`: the game's menu fade. Gameplay popups stage (1,1,1) // (why the exact port originally had no multiply); the pause menu stages // ~(0.35,0.33,0.32), squared across the H+V passes = the darkened pause // backdrop. Read live on the blur_hBlurPS draw, accepted only when the // bank's c0 kernel row reads exactly (8,8); a stale mid-stage bank fails // that gate (the staleness class that pulsed the blur radius when c0.x was // fed live, see g_ui_blur above). inline float g_ui_blur_color[3] = {1.0f, 1.0f, 1.0f}; // Hold the blur across publishes that carry no blur draw: the game does not // issue the pass chain on every swap while the popup is up, and publishing // the raw per-swap flag alternated blur on/off: a visible brightness // shimmer. Two publishes of hysteresis bridges the gaps; on popup close the // blur lingers ~2 guest frames, imperceptible. inline int g_ui_blur_hold = 0; // Host settings-overlay backdrop: forces the same blur chain while the SDK // settings menu is open (the game issues no popup blur then). Set from the // app UI thread, read at scene publish. inline std::atomic g_settings_menu_blur{false}; // Emulated-post blur diagnostics: reset on each menu open so every open // logs a fresh burst. inline std::atomic g_post_blur_log_count{0}; // Selected-object outline capture (see DrawItem::selected): in the park // editor / object mover the game EXCLUDES the selected object from the main // color pass and re-draws it right after the sky, twice, back to back, the // stencil-marking passes the postfx edge-detect turns into the blue contour // (verified in capture: consecutive environmentpark decal + // diffuse parts, identical VS banks, vs the same mesh at OTHER placements in // the main pass). Hook side records each post-sky environmentpark/ // dynamicobject draw's (ib, vb, world translation); entries seen >= 2 times // mark the matching published items. Guest render thread only, like the fog // capture state. struct SelectedDrawKey { uint32_t ib; uint32_t vb; float t[3]; uint32_t count; }; inline bool g_sky_seen_this_frame = false; inline std::vector g_frame_selected; // The guest issued its postfx_edgedetectstencil draw this frame: the game's // own "an outline is being drawn" signal (park editor / object mover with an // active selection). Post-sky double-draws alone are NOT sufficient evidence // of a selection: normal gameplay draws some small props twice after the sky // (far LOD/imposter passes), which used to sprout phantom outlines on distant // objects. Guest render thread only; reset per frame in BuildFrameScene. inline bool g_outline_edge_seen = false; // Outline color, refreshed from the guest postfx_edgedetectstencil draw's // PS c0 (the park-editor blue in every capture) whenever that pass runs. inline float g_outline_color[4] = {0.21569f, 0.64706f, 1.0f, 1.0f}; // PIXEL banks keep the CSM constants at c0..c8, pass-global (identical on // every environment-family draw of the pass; character/hair/tree PSes // allocate differently and are rejected by the sanity gate). Captured on the // same camera-keyed main-pass draws as the fog rows. inline float g_shadow_rows[48] = {}; inline bool g_shadow_have = false; inline bool g_shadow_frame_done = false; // Frame-global rows of the tree / proxyworld shader families (see // FrameScene::family_rows), captured from their PS banks when a draw with // the matching debug path runs. Guest render thread only. inline float g_family_rows[4] = {0.3435f, 0.02f, 1.0f, 0.45f}; inline bool g_tree_frame_done = false; inline bool g_proxy_frame_done = false; // flowingwateralpha m_params + animation-time rows (see // FrameScene::water_rows), captured from a flowingwateralpha PS bank // (debug-path classified). Guest render thread only. inline float g_water_rows[17] = {}; inline bool g_water_have = false; inline bool g_water_frame_done = false; // ocean_defaultPS / oceanreflection_defaultPS rows (see // FrameScene::ocean_rows / oceanrefl_rows). Guest render thread only. inline float g_ocean_rows[40] = {}; inline bool g_ocean_have = false; inline bool g_ocean_frame_done = false; inline float g_oceanrefl_rows[4] = {}; inline bool g_oceanrefl_have = false; inline bool g_oceanrefl_frame_done = false; // scrollincandescent.fx rows (see FrameScene::scroll_rows): [0] = // g_fAnimationTime (VS c9.x), [1] = the material multiplier m_params[0].y // (PS c3.y). Captured from a scrollincandescent draw (debug-path // classified). Guest render thread only. inline float g_scroll_rows[2] = {}; inline bool g_scroll_have = false; inline bool g_scroll_frame_done = false; // dynamicobject.fx frame-global lighting rows (see FrameScene::dynobj_rows), // captured from a dynamicobject/alphatestdynamicobject PS bank (debug-path // classified). Guest render thread only. inline float g_dynobj_rows[10] = {}; inline bool g_dynobj_have = false; inline bool g_dynobj_frame_done = false; // dynamicobject static world-shadow transform rows (PS c5/c6/c7), captured // alongside g_dynobj_rows, see FrameScene::dynobj_ws. inline float g_dynobj_ws[12] = {}; inline bool g_dynobj_ws_have = false; // Dynamic entities (characters, movable props) live entirely in transient // per-frame arenas: the context, its record, mesh pointers, transforms and // bone palettes are all recycled before frame end. The complete DrawItem is // therefore built at RenderMesh hook time and stored here. inline std::mutex g_palette_mutex; inline std::vector g_frame_dynitems; // (ib_obj << 32 | vb_obj) -> indices of pending items in g_frame_dynitems, // for the post-draw state fixup (deferred / world-path captures draw after // their capture; the fixup fills palette or world from the actual draw). inline std::unordered_multimap g_frame_pending_by_buffers; // Contexts submitted through an ORTHO (shadow caster-cascade) bank this // frame, i.e. items the GAME drew into its own shadow passes. The game's // caster list is per-piece: the CAS trucker hat submits ONLY to the main // view (verified in capture: every char piece has shadow-pass draws // except the hat); natively casting it painted a hard low-sun brim band // across the editor face that the emulated frame never shows. The native // atlas pass renders char pieces only when their ctx is in this set // (rescued/gap items inherit last frame's flag). Guarded by // g_palette_mutex; cleared at the end of BuildFrameScene. inline std::unordered_set g_frame_ortho_ctx; // (ib_obj << 32 | vb_obj) -> dynitem indices whose character-lighting rows // should refresh on every later matching draw this frame. The one-shot // palette fixup usually pops on the shadow-CASTER draw, whose PS bank is // stale (shadowPS uploads no PS constants); re-capturing on the main-pass // draw (last writer) lands the real per-instance rows (stamp tints etc.). inline std::unordered_multimap g_frame_char_refresh; // (ib_obj << 32 | vb_obj) -> device fetch-shadow slots 3+4 (12 dwords) at // this frame's LAST indexed 3D draw with those buffers (the z-prepass draws // first, so the main pass wins). Consumed at frame end by the // streamed-artwork diffuse override (DrawItem::diffuse_fetch): poster/advert // materials keep a 16x16 min-mip placeholder in the material channel while // the engine binds the streamed art via the fetch constants at draw time. // Guarded by g_palette_mutex; cleared at the end of BuildFrameScene. inline std::unordered_map> g_frame_draw_fetch; // mesh -> last VALIDATED character-lighting rows (see CaptureCharLighting). // The per-frame capture depends on a fragile draw-order chain (capture at // submit-exit -> pending fixup -> per-draw refresh, commit-on-success); on // frames where no draw in the chain carried this character's main-pass PS // bank the item ends with INVALID rows and the renderer falls back to the // legacy empirical shading; the player visibly flickered between the two // looks while moving, and the hair left the blended sub-pass (opaque "hair // helmet" covering the cap crown). The rows are frame-coherent (sun/ambient // move slowly), so reusing the previous valid capture is visually seamless. // TRAP: NPC clones share a mesh with PER-INSTANCE rows (stamp tints, the // teal-vest twins), so the fallback applies only to meshes with a single // instance in the frame. Guest-render-thread only (capture hooks + // BuildFrameScene), no lock needed. inline std::unordered_map> g_char_rows_cache; // Frame-global character CSM receive biases (see FrameScene::char_shadow_bias) // + the guest frame they were captured on (served while <120 frames stale; // they only change with the scene lighting setup). Guest render thread only. inline float g_char_shadow_bias[3] = {}; inline uint64_t g_char_shadow_bias_frame = 0; inline std::atomic g_char_rows_reused{0}; // Cached palette entry for the per-instance rescue below. A refused // capture re-publishes with the cached palette: one frame of pose lag is // invisible; a one-frame disappearance is not. Guest-render-thread only. struct CachedBones { std::vector bones; // g_guest_frame at refresh: rescues/heals must be FRESH; a close-pass // refusal resurrecting a seconds-old palette rendered the vehicle 10-20 m // BEHIND its live position (the momentary ghost-back). uint64_t frame = 0; }; // ctx -> last published palette for skinned character-family items. The // MeshContext is the game's own per-instance identity, so this cache // rescues each instance with ITS OWN last palette regardless of how many // clones are alive. inline std::unordered_map g_bones_cache_ctx; inline std::atomic g_lw_ctx_rescued{0}; // LW entity store consumption counters (stats line lw[...]). inline std::atomic g_lw_stamped{0}; inline std::atomic g_lw_fade0{0}; inline std::atomic g_lw_gap_filled{0}; // Authoritative caster-palette substitutions + per-ctx lighting-rows serves // (edge-of-view vehicles; see the stamp pass). inline std::atomic g_lw_pal_sub{0}; inline std::atomic g_lw_rows_served{0}; // ctx -> last VALIDATED lighting/paint rows, entity-checked (a recycled // instance must not inherit the previous occupant's paint). The mesh-keyed // g_char_rows_cache fallback is gated to single-instance meshes; cloned // traffic never qualified, so a caster-only stretch (main view culls the // vehicle at the screen edge before it leaves the screen) dropped to // legacy flat shading: the "vehicle loses its texture/color at the edge" // sighting. struct CharRowsCtx { std::array rows; uint32_t entity = 0; }; inline std::unordered_map g_char_rows_cache_ctx; // (mesh,ctx) -> last VALIDATED rows for EVERY character item with an // instance context: the general rescue the two caches above each cover // only a slice of: the mesh-keyed cache is gated to single-instance meshes // (character pieces also publish caster items of the same mesh, so gameplay // pieces rarely qualify) and the ctx cache is gated to LW-mapped entities // (the PLAYER is not one). A capture-failed frame without a rescue drops // hair out of the blended sub-pass onto the legacy opaque path: a // frame-to-frame hair/garment appearance flip (the // failure rate scales with guest fps, so it reads as a high-fps // flicker). Entity-stamped like CharRowsCtx (0 for non-LW instances) so a // recycled ctx never inherits the previous occupant's rows. struct CharRowsInst { std::array rows; uint32_t entity = 0; }; inline std::unordered_map g_char_rows_cache_inst; // Entity-keyed lighting rows for ROPA garments only: the sim on/off switch // swaps between a garment's two renderables (cloth-target vs original // model, different mesh AND ctx), so the (mesh,ctx) caches never carry // rows across the switch and the freshly resolved model rendered dark/flat // until its own capture validated. Both models map to the same owning // entity in the identity store, and they are the same garment; sharing // rows across the pair is exact. Garment-to-garment only; body pieces keep // their per-piece caches. struct CharRowsEnt { std::array rows; uint8_t fam = 0; // donor family: the row layout is family-specific }; inline std::unordered_map g_char_rows_cache_ent; inline std::atomic g_char_rows_ent_served{0}; inline std::atomic g_char_rows_inst_served{0}; // Items served authoritative pack palettes this frame / total (see // native_palette::ServeAuthoritativePalettes). inline uint32_t g_pal_served_frame = 0; inline std::atomic g_pal_served_total{0}; // Route-flip probe: counts hair/character-alpha pieces whose pass route // (blended sub-pass vs legacy opaque) changed between consecutive frames; // each count is one visible flicker event. inline std::atomic g_hair_route_flips{0}; // A,B,A,B alternation probes: bone palettes (per piece) and the shadow // cascade state (scene-wide) returning EXACTLY to the previous-but-one // frame's value (legitimate animation/lighting drift never does). inline std::atomic g_hair_bone_alternations{0}; inline std::atomic g_shadow_alternations{0}; // ctx -> last PUBLISHED item of a live LW entity (skinned character // families, non-ropa): when a ctx drops out of the submit records for a // frame or two while its entity is still alive in the store (observed as // "dyn publish GAP" incidents on fam-3/5 ped meshes, 1-3 // frames each: an in-view NPC vanishing for a frame reads as a blink or, // moving, a small teleport), the whole item republishes. Console behavior: // a live entity never skips a frame. Self-limiting: entries expire after 2 // unpublished frames and refresh only from LIVE publishes (never from a // fill, dbg_src 9), so a real despawn shows at most 2 filled frames, at // the entity's own served alpha. struct LwRetained { DrawItem item; uint64_t frame = 0; }; inline std::unordered_map g_lw_last_items; // mesh -> last RESOLVED ropa garment state (rigid world OR skinned palette). // Ropa items must NOT ride the palette-only rescue above: a ropa capture is // refused exactly when the bank is stale (g_ropa_stale), and while the cloth // sim is ACTIVE the VB holds sim-deformed root-local positions; skinning // those with a cached palette is the mangled-ribbon interpretation (verified // 0/31 in-clip; on-screen as map-length stretched strips, with matching // shadows since the caster pass shares the item). And with no rescue at all // a refused frame drops the garment (the momentary invisible torso). The // dyn decode workers already keep the drawn cloth VB one frame behind the // sim, so re-publishing last frame's resolved MODE + transform is exactly // age-consistent with the vertices being drawn. Single-instance meshes only // - clones share meshes with per-instance transforms. Guest-render-thread // only. struct RopaResolvedState { bool skinned = false; float world[16] = {}; std::vector bones; // Publish frame of the cached state: the rescues below exist to bridge // 1-3 frame capture gaps, and re-publishing an unbounded-age state pins a // ghost of the garment at a stale pose for as long as captures keep // failing (the frozen-in-the-air garment behind a skater whose capture // path was refusing every frame). uint64_t frame = 0; }; inline std::unordered_map g_ropa_state_cache; inline std::atomic g_ropa_rescued{0}; // mesh -> sample verts of the most recent DECODE of a skinned mesh (bind- // space position + raw blend attribs, ~32 evenly spaced), written by // DecodeMesh on whichever thread decodes (workers / render), read by the // guest thread's draw-time STRETCH VETO in BuildFrameScene. The veto skins // these samples with the FINAL post-interpolation palette, i.e. it judges // what the GPU will ACTUALLY draw. Every upstream gate (capture acceptance, // publish coherence) judges the LIVE guest VB instead, so a decode-content // vs palette pairing mismatch, or junk introduced by the interpolation // substitutions, passes all of them and still flashes the 1-frame // map-length ribbon (observed with every ropa[] counter clean). // `fp` = the decode's payload fingerprint so the veto can log whether the // drawn content even matches the frame's payload (content lag vs junk // palette: the decisive diagnostic split). struct SkinProbeSample { float p[3]; uint32_t bw; uint32_t bi; }; struct SkinProbe { std::vector s; uint64_t fp = 0; }; inline std::mutex g_skin_probe_mutex; inline std::unordered_map g_skin_probe; inline std::atomic g_cur_ib{0}; inline std::atomic g_cur_vb{0}; inline std::atomic g_cur_vb_offset{0}; inline std::atomic g_cur_vb_stride{0}; // Guest D3D::CDevice: the fetch-constant shadow lives at device+0x480, // 6 dwords per fetch group (from recompiled SetPending_FetchConstants). inline std::atomic g_device{0}; // 2D/APT phase depth counters (see On2dPhase). All HUD/menu 2D elements are // Flash SWFs converted to APT; draws issued inside these brackets are the 2D // draw stream. inline std::atomic g_phase2d_depth[6]; inline std::atomic g_draws_2d{0}; // 2D draws seen through paths the overlay does not replay yet // (DrawIndexedVertices / DrawVertices in the 2D phase; gameplay HUD uses // only the BeginVertices inline path, verified by capture). inline std::atomic g_draws_2d_other{0}; inline std::atomic g_draws_2d_dropped{0}; // Large-art async decode routing (see skate3_native_render_scene_2d_async_px): // quads skipped while their first decode is in flight on the workers, and // stale decodes served while a content-change re-decode is in flight. inline std::atomic g_2d_async_skip{0}; inline std::atomic g_2d_async_stale{0}; // One captured 2D draw (verified layout): BeginVertices // inline vertices, stride 24 = {float x, y, z, w; float u, v} in 1280x720 // APT movie space; VS c0..c3 = ortho projection rows, c4..c7 = the // element's 2D transform, c8 = color multiplier; diffuse texture in fetch // shadow slot 0. Vertex payload is written by the CPU after BeginVertices // returns, so it is read at frame end (addr) rather than at capture. // NOTE on render-to-texture: in gameplay the ENTIRE HUD renders inside // AptRenderingIntegration::UpdateRenderToTexture (bracket bit 3) into a // screen-sized overlay texture, at true screen coordinates; the game // composites that overlay over the 3D frame in a later (emulated, // suppressed) pass. Replaying the draws directly onto the native output IS // that composite, so bit 3 is diagnostic only, not a routing signal. The // overlay content is straight-alpha art (verified by decoding the clock // face/needle/sheen textures offline). struct Draw2d { uint32_t prim; // xenos primitive: 4 trilist, 5 tristrip, 13 quadlist uint32_t count; // vertex count uint32_t stride; // bytes per vertex at capture (see publish normalization) // Capture-time stride, preserved across the publish normalization (which // rewrites stride to the renderer's 28). The FMV substitution keys on it: // the movie quad is the stride-24 textured layout inside the // AptMovieIntegration bracket (bit 1). uint32_t src_stride = 0; uint32_t addr; // guest inline-ring write pointer uint32_t flags; // bracket bits at capture (layout disambiguation) // Texture fetch constants, shadow slots 0-2 (6 dwords each). Slot 0 is // the draw's own texture (all the replay paths read only [0..5]); slots // 1-2 exist for the VIDEO quads, whose console YUV shader binds the U and // V planes there; the replay's YUV-triple detection reads them. uint32_t fetch[18]; float consts[36]; // VS c0..c8 std::vector verts; // filled at frame end (little-endian dwords) }; inline std::mutex g_2d_mutex; inline std::vector g_frame_2d; // capture in submission order inline std::vector g_scene_2d; // published at frame end inline uint64_t g_scene_2d_generation = 0; // One captured in-world neon spline draw (waypoint arrows / marker beams; // decoded from a live capture + the game's own spline.fx source): a // DrawVertices triangle strip whose 12-byte float3 "vertices" are only // parameters: x = control-point index + fractional t, y = U texcoord, // z = side flag (0/1) selecting the extrusion offset and V texcoord. The // guest VS (43309A8C) evaluates a uniform cubic B-spline through i_cp[] // (VS c7..) transformed by world columns c4..c6, adds the world-rotated // extrusion offset c[151 + z], projects by VP columns c0..c3, and fades by // clip-z against i_clipvalues (c150) with i_intensity (c149) as the pass // gain. Two passes per element: spline_darkenPS (straight alpha) then // spline_defaultPS (additive glow), both depth-tested with no z-write, // replayed inside the native scene pass with the spline evaluated on the // CPU at publish time. struct SplineDraw { uint32_t pass; // 1 = darken (straight alpha), 2 = default (additive) uint32_t count; // strip vertex count uint32_t fetch[6]; // texture fetch slot 3 (the neon gradient) float consts[153 * 4]; // VS c0..c152 as staged // capture: raw big-endian float3 params; publish: evaluated little-endian // {float4 clip_pos, float2 uv, float fade} (28-byte stride). std::vector verts; }; inline std::vector g_frame_spline; // guarded by g_2d_mutex inline std::vector g_scene_spline; inline std::atomic g_draws_spline{0}; // Current guest shader objects and render-state shadow bank, for the 2D // recon recording (grouping the stream by shader / reading blend state). inline std::atomic g_cur_ps_obj{0}; inline std::atomic g_cur_vs_obj{0}; inline std::atomic g_rs_bank{0}; inline std::atomic g_cur_viewport[6]; inline std::atomic g_cur_scissor[4]; // All four vertex streams (vb, offset, stride); cloth binds its simulated // vertices on a stream other than 0. inline std::atomic g_cur_streams[4][3]; // Offline-analysis recording structs/state: native/skate3_native_diag.h. inline std::atomic g_vs_uploads{0}; inline std::atomic g_palette_snapshots{0}; // Palettes captured at base+1 (the cloth/morph VS layout with an extra // parameter row before the palette, see RefinePaletteBase). inline std::atomic g_palette_base_plus1{0}; // Reflective-glass (fam 5/6) normal-map pair telemetry: pair = draws with // the masks+normal t4/t5 descriptor pair bound (overlay.w == 4), flat = // spec-bound reflective draws still on the flat-normal path, gate = the // last no-pair reason bitmask (1 no spec masks, 2 no normal channel, 4 // masks SRV recipe missing, 8 normal unresolved/white, 16 normal invalid, // 32 normal recipe missing, 64 pair slots exhausted). inline std::atomic g_refl_pair{0}; inline std::atomic g_refl_flat{0}; inline std::atomic g_refl_gate{0}; // ---- F7 scene-composition ring (see RequestSceneRingDump in the header) ---- struct SceneRingItem { uint32_t ctx; uint32_t mesh; uint32_t diffuse; uint32_t lightmap; uint32_t vb_obj; // Full material texture set: a one-frame spec/normal/macro/decal-art // resolution glitch changes shading (e.g. an unmasked sky reflection on a // reflective bank face) with the diffuse/lightmap identical; the first // ring revision could not see those. uint32_t spec; uint32_t macro; uint32_t decal_art; uint32_t wnormal; uint32_t indices; // summed index counts of the item's draw entries uint16_t drawn; // draw calls actually issued (0 = skipped at draw) uint8_t env_family; uint8_t char_family; uint8_t flags; // 1 transparent 2 water 4 skinned 8 retained 16 pending // 32 caster_bank 64 decal uint8_t route; // 0 skipped pre-pass, 1 opaque pass, 2 blended sub-pass // SERVED-content fingerprints, stamped in draw_item after the resolves: // the object pointers above cannot see an in-place content swap (a heal // commit changes what a texture shows with every pointer identical); // a fp that changes A->B->A across the flash frame names the texture, // the slot, and both contents. 0 = white fallback / not resolved. uint64_t fp_diffuse; uint64_t fp_lightmap; uint64_t fp_macro; uint64_t fp_decal; }; struct SceneRingFrame { uint64_t frame = 0; float cam[3] = {}; // Per-frame captured globals: a one-frame glitch in any of these shifts // shading on every consumer with the composition identical. float fog[6] = {}; // ramp xyz + color rgb float family_rows[4] = {}; float sky_height = 0.0f; bool shadow_valid = false; std::vector items; }; inline std::deque g_scene_ring; // render thread only constexpr size_t kSceneRingFrames = 2400; inline std::atomic g_scene_ring_dump{false}; // Render thread, frame end: write the whole ring as CSV. One F line per // frame, then one I line per item (hex object addresses to match every // other diagnostic). inline void MaybeDumpSceneRing() { if (!g_scene_ring_dump.exchange(false, std::memory_order_acq_rel)) { return; } char path[128]; std::snprintf(path, sizeof(path), "logs/scene_ring_%lld.csv", static_cast(std::time(nullptr))); std::ofstream f(path); if (!f) { REXLOG_WARN("native-scene ring: cannot open {}", path); return; } f << "kind,frame,ctx,mesh,diffuse,lightmap,vb,spec,macro,decal_art," "wnormal,indices,drawn,env_fam,char_fam,flags,route,fp_diffuse," "fp_lightmap,fp_macro,fp_decal\n"; char line[256]; for (const SceneRingFrame& fr : g_scene_ring) { std::snprintf(line, sizeof(line), "F,%llu,cam,%.2f,%.2f,%.2f,items,%zu,fog,%.5f,%.4f," "%.3f,%.4f,%.4f,%.4f,fam,%.4f,%.4f,%.4f,%.4f,sky,%.1f," "shadow,%d\n", static_cast(fr.frame), double(fr.cam[0]), double(fr.cam[1]), double(fr.cam[2]), fr.items.size(), double(fr.fog[0]), double(fr.fog[1]), double(fr.fog[2]), double(fr.fog[3]), double(fr.fog[4]), double(fr.fog[5]), double(fr.family_rows[0]), double(fr.family_rows[1]), double(fr.family_rows[2]), double(fr.family_rows[3]), double(fr.sky_height), fr.shadow_valid ? 1 : 0); f << line; for (const SceneRingItem& it : fr.items) { std::snprintf(line, sizeof(line), "I,%llu,%08X,%08X,%08X,%08X,%08X,%08X,%08X,%08X,%08X," "%u,%u,%u,%u,%u,%u,%016llX,%016llX,%016llX,%016llX\n", static_cast(fr.frame), it.ctx, it.mesh, it.diffuse, it.lightmap, it.vb_obj, it.spec, it.macro, it.decal_art, it.wnormal, it.indices, unsigned(it.drawn), unsigned(it.env_family), unsigned(it.char_family), unsigned(it.flags), unsigned(it.route), static_cast(it.fp_diffuse), static_cast(it.fp_lightmap), static_cast(it.fp_macro), static_cast(it.fp_decal)); f << line; } } REXLOG_INFO("native-scene ring: wrote {} frames -> {}", g_scene_ring.size(), path); } // Character-lighting capture telemetry: attempts vs validated captures per // family (see CaptureCharLighting). inline std::atomic g_char_attempts{0}; inline std::atomic g_char_valid{0}; inline std::atomic g_char_drawn{0}; inline std::atomic g_dynobj_drawn{0}; inline std::atomic g_water_drawn{0}; // exact-branch water draws // character.cloth_ropa items captured in the sim-active RIGID mode (world // from c188/c191 instead of a bone palette, see CaptureSkinnedState). inline std::atomic g_ropa_rigid{0}; // ropa rigid captures REFUSED because the bank's c188/c191 was implausible // or projected the garment off-clip (stale bank from another mesh's draw); // the item stays pending for the post-draw fixup. inline std::atomic g_ropa_stale{0}; // ropa captures accepted through the RELAXED near-camera criterion (all // samples in FRONT of the projection inside a loose 6x guard band + sane // spread) after the strict half-in-clip score refused them. Up close the // garment fills/overflows the screen and most of the 6 sample verts clip; // the strict gate refused the CORRECT palette every frame, and with no // cached resolved state to rescue from the torso stayed invisible until the // NPC was far enough away again (log signature: `ropa mesh=... score=2 // flag=(1.000,...)` repeating while stale climbed with rescued flat). inline std::atomic g_ropa_relaxed{0}; // mesh -> newest enqueued cloth-shape generation (DynDecodeJob seq). GUEST // render thread only: written by the dyn-job enqueue, read by the interp // ring's pose ingestion (the pose <-> shape pairing). inline std::unordered_map g_ropa_last_seq; // Shape blends served / skipped (generation missing from the ring) per // stats window. inline std::atomic g_ropa_blend_drawn{0}; inline std::atomic g_ropa_blend_miss{0}; // Ropa garments PUBLISHED with a caster-cascade (ortho) bank's palette: // the ~40 ms-stale rows behind the garment-offset-from-body symptom. The // same-mode graft in the dyn_slot merge should keep this at ~0 whenever a // perspective capture exists in the same frame. inline std::atomic g_ropa_caster{0}; // Published skinned items whose palette FAILED the dense 32-sample // publish-time coherence gate (see PublishedPaletteSane): the 1-frame // junk-palette ribbon flash that passes every 6-sample capture gate. The // item re-publishes its cached state instead (or drops for one frame). inline std::atomic g_pub_incoherent{0}; // Draw-time STRETCH VETO trips (see g_skin_probe): the final palette skins // the resident decode's sample verts wider than bind size; the item's // draws are cleared for the frame (blink, not ribbon) and the first events // dump full palette + samples to logs/stretch_*.txt for offline diagnosis. inline std::atomic g_stretch_veto{0}; // skinned/ropa meshes that published after a 1-3 frame publish GAP (was on // screen, vanished, came back): the "flickered invisible" telemetry. inline std::atomic g_dyn_gap{0}; inline std::atomic g_skinned_items{0}; inline std::atomic g_skinned_skipped{0}; // Rigid items whose transform had to wait for the post-draw fixup (deferred // draws), and those still pending at frame end (dropped). inline std::atomic g_rigid_pending{0}; inline std::atomic g_rigid_dropped{0}; // Model-space props in the world sort lists, excluded from the identity // world path (their placed copies come from the kind-2 capture). inline std::atomic g_world_props{0}; inline std::atomic g_rej_no_dynstate{0}; inline std::atomic g_rej_dyn_range{0}; // Render-side silent-skip diagnostics: a scene item that decodes badly or // loses its bone binding produces no (or wrong) pixels with no other trace. inline std::atomic g_rr_decode_fail{0}; inline std::atomic g_rr_no_bones{0}; // Decodes pushed past the per-frame budget (item/texture appears a few // frames late instead of hitching the render thread). inline std::atomic g_rr_mesh_deferred{0}; inline std::atomic g_rr_tex_deferred{0}; inline std::atomic g_rej_chain{0}; inline std::atomic g_rej_geom{0}; inline std::atomic g_rej_draws{0}; inline std::atomic g_rej_bbox{0}; // ---- Perf telemetry (windowed; logged + reset with the 600-frame stats) ---- // Written by the guest render thread (capture/build) and the command // processor thread (render segments); read by the render thread's stats log. // Atomics with relaxed ordering: telemetry, not synchronization. using PerfClock = std::chrono::steady_clock; inline uint64_t PerfNsSince(PerfClock::time_point t0) { return uint64_t( std::chrono::duration_cast(PerfClock::now() - t0) .count()); } struct PerfWindow { std::atomic ns{0}; std::atomic max_ns{0}; std::atomic count{0}; void Add(uint64_t v) { ns.fetch_add(v, std::memory_order_relaxed); count.fetch_add(1, std::memory_order_relaxed); uint64_t prev = max_ns.load(std::memory_order_relaxed); while (v > prev && !max_ns.compare_exchange_weak(prev, v, std::memory_order_relaxed)) { } } double AvgMs() const { const uint64_t c = count.load(std::memory_order_relaxed); return c != 0 ? double(ns.load(std::memory_order_relaxed)) / double(c) * 1e-6 : 0.0; } double MaxMs() const { return double(max_ns.load(std::memory_order_relaxed)) * 1e-6; } void Reset() { ns.store(0, std::memory_order_relaxed); max_ns.store(0, std::memory_order_relaxed); count.store(0, std::memory_order_relaxed); } }; // Guest frame counter (guest render thread only; incremented at // BuildFrameScene entry): paces the world-item cache's revalidation. inline uint64_t g_guest_frame = 0; // Guest render thread: per-frame sums (one Add per guest frame). inline PerfWindow g_pw_capture; // hook-time capture work (sort lists + RenderMesh) inline PerfWindow g_pw_build; // BuildFrameScene inline PerfWindow g_pw_pal_tail; // frame tail: palette snapshot-ring rotation inline PerfWindow g_pw_guest_dt; // guest frame interval (guest fps + spike max) inline uint64_t g_capture_frame_ns = 0; // guest render thread only; folded per frame // Command processor thread: per-RenderScene-call segments. inline PerfWindow g_pw_render; // whole native render inline PerfWindow g_pw_items; // opaque + transparent item loop (incl. decodes) inline PerfWindow g_pw_shadow; // dynamic-shadow atlas pass inline PerfWindow g_pw_mesh_decode; // inline DecodeMesh calls (count = decodes) inline PerfWindow g_pw_tex_decode; // inline texture-ish decodes (cubes, HUD words) inline PerfWindow g_pw_commit; // PrewarmCommit batches (count = non-empty runs) // RenderScene phase attribution (the sustained "render=13ms, items=0.7ms" // Vulkan mystery): everything render= covers that items=/shadow= // do not. pre = EnsurePipeline..prewarm+evict+retire before the scene pass; // twod = the 2D/HUD overlay replay (incl. inline decodes + video planes); // tail = MSAA resolve, outline, pfx chain, photo grab, popup blur, final // transitions: RenderScene minus everything else. inline PerfWindow g_pw_pre; inline PerfWindow g_pw_2d; inline PerfWindow g_pw_tail; // Settle-pass attribution (the sticky ~80fps post-map-change state: // pre=8.4ms == the whole warmup budget, GPU idle at // P0): the takeover settle pass extends itself +8 frames whenever a frame's // warm walk leaves work deferred, so content that keeps failing revalidation // (old-map items over reused arenas, streamed payloads) can hold it open for // the rest of the session, burning the full budget every frame. settle= times // the block; wdec/wdef count warm-path decodes/deferrals; the offender logs // name what churns. inline PerfWindow g_pw_settle; // Guest-thread build-phase attribution (the standing-still 320->250 fps // oscillation): build= runs on the guest render thread and its // variance IS guest frame-time variance. b2d/bspl/bpal split the phases; // the item walk is the remainder. dt histogram + slow-frame log quantify // and attribute the dips. inline PerfWindow g_pw_b2d; // Publish2dDraws inline PerfWindow g_pw_bspl; // PublishSplineDraws inline PerfWindow g_pw_bpal; // ServeAuthoritativePalettes // This frame's phase costs (guest render thread only - no atomics needed). inline uint64_t g_frame_b2d_ns = 0; inline uint64_t g_frame_bspl_ns = 0; inline uint64_t g_frame_bpal_ns = 0; inline uint64_t g_frame_pal_tail_ns = 0; inline std::atomic g_dt_hist[5] = {}; // <3.6 / <4.5 / <6 / <10 / >=10 ms inline std::atomic g_slow_frame_log_budget{0}; inline std::atomic g_warm_decodes{0}; inline std::atomic g_warm_deferred{0}; inline std::atomic g_warm_mesh_log_budget{0}; inline std::atomic g_warm_tex_log_budget{0}; inline uint64_t g_takeover_frame = 0; // Camera-update cadence (the "world judders while panning at 320 fps" // question): if the guest updates its camera on a fixed sim tick while the // render loop free-runs, view_proj repeats for several rendered frames and // then steps, visible as the surroundings skipping to catch up. Counted per // published scene on the guest render thread. inline std::atomic g_cam_changes{0}; inline std::atomic g_cam_repeats{0}; inline std::atomic g_cam_max_streak{0}; inline bool SceneEnabled() { return REXCVAR_GET(skate3_native_render_scene); } // ---- Photo / movie / CAS-editor state shared with the render thread ---- // drew_inside captures whose bank provably belonged to another mesh's draw // (now deferred to the post-draw fixup instead of consumed). inline std::atomic g_capture_foreign_bank{0}; inline std::atomic g_item_cache_hits{0}; inline std::atomic g_item_cache_builds{0}; // ---- Photo-editor postfx capture (FrameScene::PhotoFx / photo_fx.hlsl) ---- // While a photo flow is active (g_photo_flow_frame, armed by // UpdatePhotoGrabWindow), the OnDrawDone hook snapshots each postfx pass's // PS/VS constant rows (g_ps_bank/g_vs_bank, the game's staging banks) plus // the device fetch-constant shadow (device+0x480) at DRAW time, the same // reads the F11 per-draw records use, verified correct for every postfx // pass in captures 1783957072 and 1783963636. Upload-time (SetPending) // capture was removed: the game stages the next pass's rows while the // previous pass's shader is still bound, so upload-time snapshots mix // neighbouring passes' constants. enum PfxPass { kPfxVisualFx = 0, kPfxDofDown, kPfxDofMB, kPfxDof, kPfxUber, kPfxFisheye, kPfxPassCount }; inline std::atomic g_photo_flow_frame{false}; // Photo display-card sighting stamp (see the photo-grab helpers for the // full story): stamped by Publish2dDraws on the guest thread whenever the // card-signature quad is in the 2D stream; read by YieldForPhotoDisplay. inline std::atomic g_photo_card_seen_ns{-1}; // Last ScreenshotBackEnd grab REQUEST (PerfClock ns): the game arms a grab // 1-2 frames before it renders the shot + card-composite frame sequence and // CPU-reads the resolves (GrabScreenshot -> OnScreenShot compose). The // shutter burst (UpdatePhotoGrabWindow) keys on this: the ONLY moment the // card-build's inputs can be made real under the native path. inline std::atomic g_photo_grab_request_ns{-1}; // GrabScreenshot returned: the grab CPU-read the resolved shot, and the // OnScreenShot card compose that follows (same guest thread, same Update) // only reads memory the burst already copied. The burst closes here: // event-driven, no timers (a short grace in the window logic covers the // aux-target grab some flows issue on the following frame). inline std::atomic g_photo_grab_done_ns{-1}; // Last PhotoReplayController::Update heartbeat, nanoseconds on PerfClock // (steady). -1 until the first heartbeat. Written from the guest thread, // read by RenderScene on the render thread. inline std::atomic g_photo_replay_last_ns{-1}; // Last FrontEndState_Replay2::TakePhoto heartbeat: a photo was just taken // (replay editor or photo mission). The frames that follow render and // CPU-grab the 1152x640 screenshot target, so the forced-readback window // (UpdatePhotoGrabWindow) stays armed for a few seconds after it. Same // thread contract as the photo-replay heartbeat above. inline std::atomic g_take_photo_last_ns{-1}; // Last rw::movie::MovieDecoder::Decode heartbeat (FMV playback: intro // logos, any full-motion video). Same contract as the photo-replay // heartbeat above. inline std::atomic g_movie_decode_last_ns{-1}; // Last frame the 2D replay actually SUBSTITUTED a movie quad (ps_yuv2d). // Consulted by YieldForMovie: while the native substitution is serving the // video, the emulated yield must never engage; the emulated framebuffer // is STALE under draw suppression, and presenting it flashed the previous // video's last frame at every video boundary. inline std::atomic g_movie_native_last_ns{-1}; // Last frame the 2D replay CONTAINED a video quad at all (substituted or // not; detection runs regardless of movie_sub). Consulted by YieldForMovie: // with no quad on screen there is nothing a yield could display - a // skipped/force-completed video (intro skip) leaves the decoder heartbeat // alive ~0.5 s with no quad anywhere, and yielding then just flashed the // stale emulated buffer (the "momentary emulated switch" at intro skip). inline std::atomic g_movie_quad_last_ns{-1}; // YUV plane fetch words of the FMV frames handed to // VideoRenderer_RwTexture::Render (Y full res, U/V half res, CPU-filled via // Texture::Lock). Multiple videos play at once (the camera-angle select // page runs TWO preview movies), so this is a small table keyed by the Y // plane's fetch-words hash; the 2D replay matches each captured draw's own // slot-0 fetch against it; a video quad samples its Y plane there (the // camera-page previews rendered as slow GREYSCALE through the plain 2D // shader: exactly that luma). Movie threads publish under the mutex; // RenderScene copies per frame. struct MoviePlanes { // Y plane identity = the fetch constant's ADDRESS dword (words[0][1]). // NOT a full-words hash: the words staged into the device shadow at draw // time carry per-draw sampling flags that differ from the texture // object's stored constant (observed: both camera-page videos // published, exact-words match still 0 hits): the address dword is the // physical identity both sides agree on. uint32_t y_addr = 0; uint32_t words[3][6]; // Y, U, V fetch constants int64_t ns = -1; }; constexpr int kMaxMovies = 4; inline std::mutex g_movie_mutex; inline MoviePlanes g_movies[kMaxMovies]; // Heartbeat of the create-a-skater editor's OWN pixel shaders: the editor's // in-view skater draws use "_nis" compiles (cacstamp_skin_nisPS, // cac_cloth_nisPS, cac_face_nisPS, cac_hair_nisPS, defaultcharacter_nisPS, // cacstamp_shift_nisPS) that exist nowhere else in the game; a fresh // sighting means the CAS editor is on screen NOW. Backs the FE-stack id-15 // detection in YieldForCasEditor: the startup new-game flow reaches the // editor through a different FE screen (observed: the indicator stayed // NATIVE there). Deliberately NOT matched: the "_unwrap" texture-space // composite variants; outfit composition also runs mid-gameplay after an // outfit change and must never yield a gameplay frame. inline std::atomic g_cas_ps_last_ns{-1}; // Capture-side helpers the render thread calls (defined in // skate3_native_scene.cpp). bool GuestTryLoadU32(uint8_t* base, uint32_t addr, uint32_t* out); void ClearItemCache(); bool CasEditorActive(uint8_t* base); bool PortraitRttWindowActive(); // Parse a guest mesh struct into a DrawItem (prewarm decode workers build // items off the queue without a live capture). bool BuildItemFromMesh(uint8_t* base, uint32_t mesh, DrawItem& item); // Render-side helper the capture thread calls (defined in // skate3_native_scene_gpu.cpp): photo-editor detection shared by the yield // and the photo-grab readback window: nullptr when inactive, else the name // of the active signal. const char* PhotoEditorSignal(uint8_t* base); } // namespace skate3::native_scene