scene: native FMV playback (YUV plane substitution)

This commit is contained in:
mchughalex 2026-07-13 15:01:31 +01:00
parent ba50240655
commit 592700b4b5
4 changed files with 409 additions and 13 deletions

View file

@ -61,6 +61,23 @@ float4 SampleCR(float2 uv, float2 ts) {
c += tex.Sample(smp, float2(t3.x, t3.y)) * (w3.x * w3.y);
return c;
}
// Native FMV: the movie quad (AptMovieIntegration bracket, stride-24
// textured) samples three CPU-filled YUV planes: Y at t0, U at t1, V at
// t4 (the root signature's existing single-SRV tables). BT.601 studio
// range. c8 / vertex color are deliberately ignored: the game stages c8 =
// opaque BLACK on the movie quad (its own movie shader never reads it),
// which is exactly what rendered the video as a black cover through
// ps_main.
Texture2D<float4> tex_u : register(t1);
Texture2D<float4> tex_v : register(t4);
float4 ps_yuv2d(VSOut i) : SV_Target {
float y = tex.Sample(smp, i.uv).r;
float u = tex_u.Sample(smp, i.uv).r - 0.5;
float v = tex_v.Sample(smp, i.uv).r - 0.5;
y = (y - 16.0 / 255.0) * (255.0 / 219.0);
float3 c = float3(y + 1.596 * v, y - 0.813 * v - 0.391 * u, y + 2.018 * u);
return float4(saturate(c), 1.0);
}
float4 ps_main(VSOut i) : SV_Target {
float4 c = tex.Sample(smp, i.uv);
// Sharp magnification (m[9].y = skate3_native_render_scene_2d_sharp):

View file

@ -751,6 +751,31 @@ extern "C" REX_FUNC(sub_825623F0) {
__imp__sub_825623F0(ctx, base);
}
// rw::movie::MovieDecoder::Decode(int, VideoRenderable**,
// SubtitleRenderable*): fires per decoded FMV frame while any movie plays
// (boot intro logos and all other rw::movie playback). Heartbeat for the
// native scene's FMV yield: the video frame is CPU-decoded into a texture
// and reaches the screen through the game's postfx chain + swap without a
// capturable 2D draw, so only the emulated path can show it.
extern "C" REX_FUNC(sub_82A92DC8) {
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnMovieDecode();
}
__imp__sub_82A92DC8(ctx, base);
}
// VideoRenderer_RwTexture::Render(VideoRenderable*, int): fills the three
// YUV plane textures (members this+12/+124/+68) with the decoded FMV frame
// via Texture::Lock/FillTextureData/Unlock. Post-call, the planes hold the
// finished frame: publish their fetch words for the native FMV blit.
extern "C" REX_FUNC(sub_8263C498) {
const uint32_t self = ctx.r3.u32;
__imp__sub_8263C498(ctx, base);
if (skate3::native_render::Enabled()) {
skate3::native_scene::OnMovieFrame(base, self);
}
}
// Sk8::WorldPresentation::AddRenderInstance(pegasus::tInstance*): the world
// registry add, fired per placed instance while a map loads (r4 = tInstance).
// The prewarm's primary mesh source: the instance's tRModelData mesh table

View file

@ -167,6 +167,67 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_boot_native, true, "Skate 3",
"yielding. With this and the pause/loading modes on, the emulated "
"GPU output is never presented.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_fmv_native, true, "Skate 3",
"Render FMVs natively: the movie player CPU-decodes VP6 into "
"three YUV plane textures (VideoRenderer_RwTexture members, "
"published per frame by the Render hook), and the captured "
"movie quad (the AptMovieIntegration stride-24 draw; through "
"the plain 2D shader it rendered as an opaque black cover, its "
"c8 is black) is substituted with the ps_yuv2d combine inside "
"the 2D replay, order-faithful and at the quad's own geometry "
"(windowed movies place exactly). When off (or the planes fail "
"to publish), FMVs fall back to the emulated yield "
"(skate3_native_render_scene_fmv_yield).")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_fmv_yield, true, "Skate 3",
"Yield to the emulated output while an FMV is playing (intro "
"logos, any rw::movie playback). The video frame is CPU-decoded "
"into a texture and reaches the screen through the game's postfx "
"chain + swap without any capturable 2D draw (F11-proven on the "
"boot intro: 15 draws, all postfx passes + fade fills, none "
"sampling the video), so the native path has nothing to replay; "
"the emulated frame is complete and correct there, same class as "
"the photo editor. Detected via the MovieDecoder::Decode "
"heartbeat; native rendering resumes within 0.5 s of the last "
"decoded frame.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_cas_yield, true, "Skate 3",
"Yield to the emulated output while the create-a-skater editor "
"(the CAS 'Edit Skater' screen) is up. The editor is a special FE "
"renderer: its own CAC shader variants (cac*_nisPS, different "
"constant layouts than gameplay, so the char-lighting capture "
"rejects every bank), per-frame texture-space composite passes "
"(cac*_unwrapPS render the edited garment/skin art into textures) "
"and its own DOF postfx chain, none of which the native scene "
"models. Natively it showed wrong clothing tints/skin tone and "
"broken live-edit previews. The emulated "
"frame is complete and exact there (photo-editor class). Detected "
"by polling the FrontEndManager push-state stack for screen id 15 "
"(surveyed unique to the CAS editor across 40 gsnaps).")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(
skate3_native_render_scene_menu_unsuppress, true, "Skate 3",
"While a menu/pause/loading context (presence context 0) renders "
"natively, temporarily clear native_render_suppress_emulated_draws so "
"the game's off-screen one-shot renders keep executing; the team-menu "
"skater portrait boxes are a render-to-texture the game produces once "
"when the screen opens (and again after an edit); with emulated draws "
"suppressed that pass never ran and the boxes stayed empty in every "
"native menu session (even the F11 emulated pair-shot inherited the "
"never-filled texture). Costs the emulated pipeline's GPU time during "
"menus only; gameplay suppression is untouched. Restored to the user's "
"configured value on the first gameplay frame.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_2d_unbracketed, true, "Skate 3",
"During native loading/boot frames, also capture full-screen "
"inline 2D draws issued OUTSIDE the FE/APT brackets: the boot "
"intro video's to-screen quad draws before the FrontEndManager "
"render path exists (VideoRenderer_RwTexture fills a CPU-locked "
"texture; something below the FE draws it), which left the video "
"black with audio. Gated on a >=960-wide viewport so small "
"render-to-texture helper quads never leak into the overlay. "
"No effect during gameplay/pause frames.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_snapshot_all_draws, false, "Skate 3",
"Record the draw stream on every recorded frame instead of 2 of every "
"60 (large .draws.bin; for targeted investigations)")
@ -936,6 +997,12 @@ std::atomic<uint32_t> g_device{0};
// Flash SWFs converted to APT; draws issued inside these brackets are the 2D
// draw stream.
std::atomic<uint32_t> g_phase2d_depth[6];
// Set by the render thread while rendering native loading/boot frames
// (no world scene): the guest-side 2D capture also accepts full-screen
// inline draws OUTSIDE the brackets there (the boot movie player's video
// quad draws below every FE hook). See the capture site + the
// skate3_native_render_scene_2d_unbracketed cvar.
std::atomic<bool> g_capture_2d_unbracketed{false};
std::atomic<uint64_t> g_draws_2d{0};
// 2D draws seen through paths the overlay does not replay yet
// (DrawIndexedVertices / DrawVertices in the 2D phase; gameplay HUD uses
@ -961,6 +1028,11 @@ 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)
uint32_t fetch[6]; // texture fetch constant (shadow slot 0)
@ -2705,6 +2777,76 @@ void OnPhotoReplayUpdate() {
std::memory_order_relaxed);
}
// Last rw::movie::MovieDecoder::Decode heartbeat (FMV playback: intro
// logos, any full-motion video). Same contract as the photo-replay
// heartbeat above.
static std::atomic<int64_t> g_movie_decode_last_ns{-1};
void OnMovieDecode() {
g_movie_decode_last_ns.store(
std::chrono::duration_cast<std::chrono::nanoseconds>(
PerfClock::now().time_since_epoch())
.count(),
std::memory_order_relaxed);
}
// YUV plane fetch words of the last FMV frame handed to
// VideoRenderer_RwTexture::Render (Y full res, U/V half res, CPU-filled via
// Texture::Lock: the game's postfx uber composites these into the frame;
// natively the ps_yuv fullscreen pass does). Guest thread publishes under
// the mutex; RenderScene copies per frame.
struct MovieFrame {
uint32_t words[3][6]; // Y, U, V fetch constants
int64_t ns = -1;
};
std::mutex g_movie_mutex;
MovieFrame g_movie_frame;
void OnMovieFrame(uint8_t* base, uint32_t renderer) {
// Plane texture members of VideoRenderer_RwTexture, from the recompiled
// Render body (Lock/FillTextureData/Unlock trios): [this+12] = Y,
// [this+124] = U, [this+68] = V; the fill sources are the renderable's
// y/u/v buffers at +8/+12/+16 (TransferYUVBuffer order).
static constexpr uint32_t kPlaneOfs[3] = {12, 124, 68};
MovieFrame mf;
for (int p = 0; p < 3; ++p) {
uint32_t obj = 0;
if (!GuestTryLoadU32(base, renderer + kPlaneOfs[p], &obj) || obj < 0x10000) {
return;
}
// Stable fetch-words read at tex+0x1C (the D3D12 section's
// ReadStableTexWords, inlined to keep this compiled guest-side).
uint32_t raw[6], raw2[6];
if (!GuestTryCopy(raw, base + obj + 0x1C, sizeof(raw)) ||
!GuestTryCopy(raw2, base + obj + 0x1C, sizeof(raw2)) ||
std::memcmp(raw, raw2, sizeof(raw)) != 0) {
return;
}
for (int i = 0; i < 6; ++i) {
mf.words[p][i] = BSwap32(raw[i]);
}
if ((mf.words[p][0] & 3u) != 2 || mf.words[p][1] == 0) {
return; // not a live texture fetch constant
}
}
mf.ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
PerfClock::now().time_since_epoch())
.count();
{
std::lock_guard<std::mutex> lock(g_movie_mutex);
g_movie_frame = mf;
}
OnMovieDecode(); // shared FMV heartbeat (the emulated-yield fallback)
static std::atomic<bool> s_logged{false};
if (!s_logged.exchange(true, std::memory_order_relaxed)) {
REXLOG_INFO(
"native-scene: FMV planes published (y=[{:08X} {:08X}] u=[{:08X} "
"{:08X}] v=[{:08X} {:08X}])",
mf.words[0][0], mf.words[0][1], mf.words[1][0], mf.words[1][1],
mf.words[2][0], mf.words[2][1]);
}
}
void On2dPhase(uint32_t bit, bool enter) {
if (bit >= 6) {
return;
@ -3672,6 +3814,41 @@ uint32_t ClassifySplineShader(uint8_t* base, uint32_t obj) {
return kind;
}
// Cached shader debug-path leaf (obj+0x54, e.g. "postfx_basictex.updb" ->
// "postfx_basictex") for the unbracketed-2D capture diagnosis log. Guest
// render thread only, like ClassifySplineShader above.
const char* ShaderDebugLeaf(uint8_t* base, uint32_t obj) {
static std::unordered_map<uint32_t, std::string> cache;
if (obj < 0x10000 || !GuestReadableApprox(base, obj)) {
return "?";
}
auto it = cache.find(obj);
if (it == cache.end()) {
char text[97] = {};
if (!GuestTryCopy(text, base + obj + 0x54, 96)) {
return "?";
}
text[96] = '\0';
std::string name;
const char* leaf = std::strrchr(text, '\\');
for (const char* p = leaf ? leaf + 1 : text; *p != '\0'; ++p) {
if (*p < 0x20 || *p > 0x7E) {
name.clear();
break;
}
name += *p;
}
if (name.empty()) {
name = "?";
}
if (cache.size() > 4096) {
cache.clear();
}
it = cache.emplace(obj, std::move(name)).first;
}
return it->second.c_str();
}
} // namespace
void OnSetIndices(uint32_t ib_obj) { g_cur_ib.store(ib_obj, std::memory_order_relaxed); }
@ -4192,7 +4369,35 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
// Live 2D overlay capture: the HUD renders exclusively through the
// BeginVertices inline path (func 2). Vertex payloads are read at frame
// end; everything else (transform constants, texture fetch) is staged now.
if (flags2d != 0 && SceneEnabled() && REXCVAR_GET(skate3_native_render_scene_2d)) {
//
// During native loading/boot frames (no world scene) full-screen inline
// draws OUTSIDE the brackets are accepted too: the boot intro video's
// to-screen quad draws below every FE hook (the movie plays before the
// FrontEndManager render path exists), which left the video black with
// audio. The >=960-wide viewport gate keeps small render-to-texture
// helper quads out; the capped log names every candidate (func 1 =
// DrawVertices video blits would need their own capture; the log is the
// evidence trail if that path exists).
bool unbracketed_2d = false;
if (flags2d == 0 && (func == 1 || func == 2) && SceneEnabled() &&
g_capture_2d_unbracketed.load(std::memory_order_relaxed) &&
REXCVAR_GET(skate3_native_render_scene_2d_unbracketed) &&
g_cur_viewport[2].load(std::memory_order_relaxed) >= 960) {
unbracketed_2d = func == 2;
static std::atomic<uint32_t> s_unbr_logged{0};
if (s_unbr_logged.fetch_add(1, std::memory_order_relaxed) < 24) {
REXLOG_INFO(
"native-scene: unbracketed 2D draw func={} r4={} r5={} r6={} "
"r7={:08X} ps={} vs={} vp={}x{}",
func, r4, r5, r6, r7,
ShaderDebugLeaf(base, g_cur_ps_obj.load(std::memory_order_relaxed)),
ShaderDebugLeaf(base, g_cur_vs_obj.load(std::memory_order_relaxed)),
g_cur_viewport[2].load(std::memory_order_relaxed),
g_cur_viewport[3].load(std::memory_order_relaxed));
}
}
if ((flags2d != 0 || unbracketed_2d) && SceneEnabled() &&
REXCVAR_GET(skate3_native_render_scene_2d)) {
if (func == 2) {
const uint32_t device = g_device.load(std::memory_order_relaxed);
if (device != 0 && r7 >= 0x10000 && r5 != 0 && r5 <= 65536 && r6 >= 8 &&
@ -6144,7 +6349,10 @@ void Publish2dDraws(uint8_t* base) {
for (uint32_t v = 0; v < d.count && ok; ++v) {
uint8_t* dst = norm_2d.data() + size_t(v) * 28;
const uint8_t* src = scratch_2d.data() + size_t(v) * d.stride;
if (d.stride == 16 && simple) { // SimpleDraw: pos4, untextured
// Unbracketed captures (d.flags == 0, native loading/boot frames)
// take the SimpleDraw layout readings for 16/28: EA's inline-quad
// helpers order color before texcoords everywhere.
if (d.stride == 16 && (simple || d.flags == 0)) { // pos4, untextured
std::memcpy(dst, src, 16);
std::memcpy(dst + 16, &zero, 4);
std::memcpy(dst + 20, &zero, 4);
@ -6172,7 +6380,7 @@ void Publish2dDraws(uint8_t* base) {
dst[25] = src[18];
dst[26] = src[17];
dst[27] = src[16];
} else if (d.stride == 28 && simple) { // SimpleDraw: pos4+color+uv
} else if (d.stride == 28 && (simple || d.flags == 0)) { // pos4+color+uv
std::memcpy(dst, src, 16);
std::memcpy(dst + 16, src + 20, 8);
dst[24] = src[19];
@ -6194,6 +6402,7 @@ void Publish2dDraws(uint8_t* base) {
std::memset(d.fetch, 0, sizeof(d.fetch));
}
scratch_2d = norm_2d;
d.src_stride = d.stride;
d.stride = 28;
}
if (d.prim == 13 && d.count % 4 == 0) {
@ -8403,6 +8612,10 @@ struct RendererState {
ID3D12PipelineState* pso_blur = nullptr;
ID3D12PipelineState* pso_blur_blit = nullptr;
ID3D12PipelineState* pso_blur_down = nullptr;
// Native FMV: overlay-2D pipeline variant whose PS combines the movie
// player's three CPU-filled YUV plane textures (ps_yuv2d; substituted for
// the captured movie quad in the 2D replay, see OnMovieFrame).
ID3D12PipelineState* pso_yuv2d = nullptr;
// Selection outline (see DrawItem::selected): selected items re-render
// into a single-sample R8 mask at OUTPUT resolution (RTV slot 7: a
// low-res mask stairstepped the contour centerline at 4K) and a
@ -11546,6 +11759,23 @@ bool Ensure2dPso(const NativeGuestOutputRenderContext& context) {
up.RTVFormats[0] = context.d3d12.guest_output_format;
up.SampleDesc.Count = 1;
const HRESULT hr4 = device->CreateGraphicsPipelineState(&up, IID_PPV_ARGS(&g_r.pso_2d));
// FMV variant: identical pipeline, ps_yuv2d combine (see the movie-quad
// substitution in the 2D replay). Failure only loses native FMV; the
// emulated yield fallback covers it.
ID3DBlob* uyuv = nullptr;
if (SUCCEEDED(D3DCompile(kShader2dSource, sizeof(kShader2dSource) - 1, "native_2d",
nullptr, nullptr, "ps_yuv2d", "ps_5_0", 0, 0, &uyuv,
nullptr))) {
up.PS = {uyuv->GetBufferPointer(), uyuv->GetBufferSize()};
if (g_r.pso_yuv2d) g_r.pso_yuv2d->Release();
if (FAILED(device->CreateGraphicsPipelineState(&up, IID_PPV_ARGS(&g_r.pso_yuv2d)))) {
REXLOG_WARN("native-scene: FMV 2D PSO creation failed - movies yield");
g_r.pso_yuv2d = nullptr;
}
uyuv->Release();
} else {
REXLOG_WARN("native-scene: ps_yuv2d compile failed - movies yield");
}
uvs->Release();
ups->Release();
if (uerrors) uerrors->Release();
@ -13041,6 +13271,9 @@ bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
in_loading && (s_seen_gameplay || boot_native) &&
REXCVAR_GET(skate3_native_render_scene_loading_native);
g_loading_native_frame = loading_native;
// Guest-side mirror for the unbracketed-2D capture; RenderScene re-stores
// the final verdict after the takeover-gate hold below.
g_capture_2d_unbracketed.store(loading_native, std::memory_order_relaxed);
if (in_loading != s_in_loading) {
s_in_loading = in_loading;
if (in_loading) {
@ -13132,6 +13365,41 @@ bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
// photo-editor capture shows a non-(-1) second field. The
// PhotoReplayController heartbeat (sub_825623F0 hook) is kept as a
// secondary signal for photo flows that bypass the photographer NIS.
// FMV playback (intro logos, any full-motion video): the frame is
// CPU-decoded into a texture (VideoRenderer_RwTexture Lock/Fill/Unlock) and
// reaches the screen through the game's postfx chain + swap; no capturable
// 2D draw exists (captured FMV frames show ~15 draws, all postfx
// passes + fade fills), so the native path has nothing to replay. Yield
// while the MovieDecoder::Decode heartbeat is fresh; the emulated frame is
// complete and correct there (photo-editor class). Touches no caches or
// takeover gates; native rendering resumes on the next frame after the
// movie ends.
bool YieldForMovie() {
if (!REXCVAR_GET(skate3_native_render_scene_fmv_yield)) {
return false;
}
const int64_t last_ns = g_movie_decode_last_ns.load(std::memory_order_relaxed);
if (last_ns < 0) {
return false;
}
const int64_t now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
PerfClock::now().time_since_epoch())
.count();
const bool active = now_ns - last_ns < 500'000'000;
static bool s_active = false;
if (active != s_active) {
s_active = active;
if (active) {
REXLOG_INFO(
"native-scene: FMV playing - yielding to emulated output "
"(MovieDecoder heartbeat)");
} else {
REXLOG_INFO("native-scene: FMV ended - native output resumes");
}
}
return active;
}
bool YieldForPhotoEditor(uint8_t* base) {
if (!REXCVAR_GET(skate3_native_render_scene_photo_yield)) {
return false;
@ -13704,6 +13972,7 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
loading_native = !ready;
}
g_loading_hold = loading_native;
g_capture_2d_unbracketed.store(loading_native, std::memory_order_relaxed);
std::shared_ptr<const FrameScene> scene_ptr;
if (loading_native) {
// Native loading screen: there is no current world scene (g_scene holds
@ -13729,6 +13998,32 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// Flush any barriers pushed by lazy resource creation (white texture).
context.d3d12.submit_barriers(context.d3d12.command_processor_user_data);
// FMV routing: prefer the native path, the captured movie quad (the
// AptMovieIntegration stride-24 draw, which the game's own shader paints
// with the YUV planes; through ps_main it rendered as an opaque BLACK
// cover, c8 = black) is SUBSTITUTED with the ps_yuv2d combine inside the
// 2D replay, order-faithful (the black backdrop fills land under it,
// like the emulated frame). Only when that path is unavailable does a
// live movie heartbeat yield to the emulated output.
MovieFrame movie;
{
std::lock_guard<std::mutex> lock(g_movie_mutex);
movie = g_movie_frame;
}
const bool movie_fresh =
movie.ns >= 0 &&
std::chrono::duration_cast<std::chrono::nanoseconds>(
PerfClock::now().time_since_epoch())
.count() -
movie.ns <
500'000'000;
const bool movie_sub = movie_fresh &&
REXCVAR_GET(skate3_native_render_scene_fmv_native) &&
g_r.pso_yuv2d != nullptr;
if (!movie_sub && YieldForMovie()) {
return false;
}
auto* command_processor = context.d3d12.command_processor;
auto& list = command_processor->GetDeferredCommandList();
@ -15662,7 +15957,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
list.D3DSetPipelineState(g_r.pso_2d);
// One shared draw routine for both the RTT passes and the screen pass.
// ui_region/ui_offset continue after the spline pass's allocations.
const auto emit_draw = [&](const Draw2d& d) {
// yuv != nullptr redirects this draw to the FMV combine: pso_yuv2d
// with the three movie plane slots on tables 1/2/5 (t0/t1/t4), the
// quad's own geometry/transform (so windowed movies place exactly).
const auto emit_draw = [&](const Draw2d& d, const uint32_t* yuv = nullptr) {
const uint32_t bytes = uint32_t(d.verts.size());
if (bytes == 0 || d.stride != 28) {
return;
@ -15672,9 +15970,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
return;
}
std::memcpy(g_r.ui_ring_cpu + ui_region + ui_offset, d.verts.data(), bytes);
const uint32_t srv_slot = d.composite_srv >= 0
? uint32_t(d.composite_srv)
: resolve_2d_texture(d.fetch)->srv_slot;
const uint32_t srv_slot =
yuv != nullptr ? yuv[0]
: d.composite_srv >= 0 ? uint32_t(d.composite_srv)
: resolve_2d_texture(d.fetch)->srv_slot;
float constants[40];
std::memcpy(constants, d.consts, sizeof(d.consts));
// 2D ortho draws have no translation row in the projection (c3 ==
@ -15690,17 +15989,28 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
constants[38] = 0.0f;
constants[39] = 0.0f;
list.D3DSetGraphicsRoot32BitConstants(0, 40, constants, 0);
D3D12_GPU_DESCRIPTOR_HANDLE srv_gpu =
g_r.srv_heap->GetGPUDescriptorHandleForHeapStart();
srv_gpu.ptr += size_t(srv_slot) * g_r.srv_size;
context.d3d12.set_graphics_root_descriptor_table(
context.d3d12.command_processor_user_data, 1, srv_gpu);
const auto srv_table_at = [&](uint32_t param, uint32_t slot) {
D3D12_GPU_DESCRIPTOR_HANDLE h =
g_r.srv_heap->GetGPUDescriptorHandleForHeapStart();
h.ptr += size_t(slot) * g_r.srv_size;
context.d3d12.set_graphics_root_descriptor_table(
context.d3d12.command_processor_user_data, param, h);
};
srv_table_at(1, srv_slot);
if (yuv != nullptr) {
list.D3DSetPipelineState(g_r.pso_yuv2d);
srv_table_at(2, yuv[1]);
srv_table_at(5, yuv[2]);
}
D3D12_VERTEX_BUFFER_VIEW vbv{
g_r.ui_ring->GetGPUVirtualAddress() + ui_region + ui_offset, bytes, d.stride};
list.D3DIASetVertexBuffers(0, 1, &vbv);
list.D3DIASetPrimitiveTopology(d.prim == 5 ? D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP
: D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
list.D3DDrawInstanced(d.count, 1, 0, 0);
if (yuv != nullptr) {
list.D3DSetPipelineState(g_r.pso_2d);
}
ui_offset += bytes;
++drawn_2d;
};
@ -15708,8 +16018,50 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
list.D3DOMSetRenderTargets(1, &output_rtv, FALSE, nullptr);
list.RSSetViewport(viewport);
list.RSSetScissorRect(scissor);
// Native FMV: resolve the three movie plane textures once (the
// content-liveness probe re-decodes them every frame while the video
// plays) and substitute the combine for every movie quad this frame.
uint32_t yuv_slots[3] = {};
bool yuv_ready = false;
if (movie_sub) {
yuv_ready = true;
for (int p = 0; p < 3 && yuv_ready; ++p) {
// A playing video rewrites its planes every few RENDERED frames
// (30 fps content vs 140 fps render); the liveness probe's
// 16-frame settle cadence after an unchanged sample would show
// one video frame in ~17 (the "sluggish video" report). Movie
// planes are known content-hot: force the probe every frame;
// the re-decode itself still only fires when the fingerprint
// actually changed (the video's own rate).
auto hot = find_words_texture(FetchWordsKey(movie.words[p]));
if (hot != g_r.tex_store.end()) {
hot->second.recheck_frame = 0;
}
const GuestTexture* t = resolve_2d_texture(movie.words[p]);
yuv_ready = t != &g_r.white && t->texture != nullptr;
yuv_slots[p] = t->srv_slot;
}
if (!yuv_ready) {
static std::atomic<uint32_t> s_yuv_failed{0};
if (s_yuv_failed.fetch_add(1, std::memory_order_relaxed) < 8) {
REXLOG_INFO("native-scene: FMV plane resolve FAILED this frame");
}
}
}
bool movie_drawn = false;
for (const Draw2d& d : scene_2d) {
emit_draw(d);
const bool movie_quad =
yuv_ready && (d.flags & 0x2u) != 0 && d.src_stride == 24;
emit_draw(d, movie_quad ? yuv_slots : nullptr);
movie_drawn |= movie_quad;
}
if (movie_drawn) {
static std::atomic<bool> s_movie_logged{false};
if (!s_movie_logged.exchange(true, std::memory_order_relaxed)) {
REXLOG_INFO(
"native-scene: FMV rendering NATIVELY (movie-quad YUV "
"substitution)");
}
}
}
}

View file

@ -364,6 +364,8 @@ void OnVsConstantUpload(uint8_t* base, uint64_t mask, uint32_t bank, uint32_t pt
// / contrast effects are the game's own postfx chain, which only the
// emulated path executes.
void OnPhotoReplayUpdate();
void OnMovieDecode();
void OnMovieFrame(uint8_t* base, uint32_t renderer);
// 2D/APT phase bracket (the HUD and every other 2D element is a Flash SWF
// converted to EA APT, rendered through FE::AptRenderingIntegration). Guest