scene: FMV boundary flash = instant emulated yield (stale framebuffer); async video planes

This commit is contained in:
mchughalex 2026-07-13 16:14:44 +01:00
parent 01a8ffbb9a
commit 700e1e2d82

View file

@ -2866,6 +2866,12 @@ void OnPhotoReplayUpdate() {
// logos, any full-motion video). Same contract as the photo-replay // logos, any full-motion video). Same contract as the photo-replay
// heartbeat above. // heartbeat above.
static std::atomic<int64_t> g_movie_decode_last_ns{-1}; static std::atomic<int64_t> 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.
static std::atomic<int64_t> g_movie_native_last_ns{-1};
void OnMovieDecode() { void OnMovieDecode() {
g_movie_decode_last_ns.store( g_movie_decode_last_ns.store(
@ -13817,18 +13823,37 @@ bool YieldForMovie() {
PerfClock::now().time_since_epoch()) PerfClock::now().time_since_epoch())
.count(); .count();
const bool active = now_ns - last_ns < 500'000'000; const bool active = now_ns - last_ns < 500'000'000;
// LAST-RESORT gating: yielding INSTANTLY at video start presented the
// emulated framebuffer, which under draw suppression still holds a stale
// frame: the flash of the PREVIOUS video at every video boundary
// (measured: yield, planes published +16 ms, substitution
// native +38 ms, a ~5-frame stale-frame window). The substitution path
// gets 400 ms from the first heartbeat to engage, and once it has drawn
// recently the yield stays off entirely (covers the video-END race the
// same way). Genuine native-FMV failure (fmv_native off, pso missing,
// plane decode failure) still reaches the emulated yield after the
// grace window.
static int64_t s_fresh_since = -1;
if (!active) {
s_fresh_since = -1;
} else if (s_fresh_since < 0) {
s_fresh_since = now_ns;
}
const int64_t native_ns = g_movie_native_last_ns.load(std::memory_order_relaxed);
const bool yield = active && now_ns - s_fresh_since >= 400'000'000 &&
(native_ns < 0 || now_ns - native_ns > 1'000'000'000);
static bool s_active = false; static bool s_active = false;
if (active != s_active) { if (yield != s_active) {
s_active = active; s_active = yield;
if (active) { if (yield) {
REXLOG_INFO( REXLOG_INFO(
"native-scene: FMV playing - yielding to emulated output " "native-scene: FMV playing - yielding to emulated output "
"(MovieDecoder heartbeat)"); "(MovieDecoder heartbeat; substitution did not engage)");
} else { } else {
REXLOG_INFO("native-scene: FMV ended - native output resumes"); REXLOG_INFO("native-scene: FMV ended - native output resumes");
} }
} }
return active; return yield;
} }
bool YieldForPhotoEditor(uint8_t* base) { bool YieldForPhotoEditor(uint8_t* base) {
@ -16285,7 +16310,22 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
if (regen < 2 || !EnsureHudTileRegen(context, base, fetch, regen, gt)) { if (regen < 2 || !EnsureHudTileRegen(context, base, fetch, regen, gt)) {
EnsureGuestTextureFromWords(context, base, fetch, gt); EnsureGuestTextureFromWords(context, base, fetch, gt);
} }
g_pw_tex_decode.Add(perf_ns_since(hud_t0)); const uint64_t decode_ns = perf_ns_since(hud_t0);
g_pw_tex_decode.Add(decode_ns);
// Attribution for residual render-thread stalls: anything still
// decoding inline for >3 ms should either move over the async
// threshold or explain itself here.
if (decode_ns > 3'000'000) {
static std::atomic<uint32_t> s_slow{0};
const uint32_t n = s_slow.fetch_add(1, std::memory_order_relaxed);
if (n < 24 || (n & 255u) == 0) {
REXLOG_INFO(
"native-scene: SLOW inline 2D decode {:.1f}ms {}x{} "
"fetch=[{:08X} {:08X} {:08X}] forced={} (n={})",
double(decode_ns) / 1e6, px_w, px_h, fetch[0], fetch[1],
fetch[2], force_inline ? 1 : 0, n);
}
}
if (!gt.valid) { if (!gt.valid) {
static std::unordered_set<uint64_t> logged; static std::unordered_set<uint64_t> logged;
if (logged.size() < 32 && logged.insert(key).second) { if (logged.size() < 32 && logged.insert(key).second) {
@ -16662,8 +16702,17 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
bool movie_drawn = false; bool movie_drawn = false;
for (const Draw2d& d : scene_2d) { for (const Draw2d& d : scene_2d) {
const uint32_t* yuv = nullptr; const uint32_t* yuv = nullptr;
if (movie_sub && d.composite_srv < 0) { // Video-quad detection runs regardless of movie_sub: a quad whose
if (yuv_triple(d)) { // own fetch slots form a YUV triple must NEVER draw through ps_main
// - that renders the raw Y plane (greyscale luma, or the PREVIOUS
// video's frame while the plane copies are still stale), the
// video-boundary flash class. If the planes can't resolve yet
// (async decode in flight, substitution off) the quad is SKIPPED;
// black under a starting video is what the real thing looks like.
bool video_quad = false;
if (d.composite_srv < 0 && yuv_triple(d)) {
video_quad = true;
if (movie_sub) {
TripleCacheEntry* e = nullptr; TripleCacheEntry* e = nullptr;
for (int t = 0; t < triple_count && e == nullptr; ++t) { for (int t = 0; t < triple_count && e == nullptr; ++t) {
if (triple_cache[t].y_addr == d.fetch[1]) { if (triple_cache[t].y_addr == d.fetch[1]) {
@ -16674,18 +16723,24 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
e = &triple_cache[triple_count++]; e = &triple_cache[triple_count++];
e->y_addr = d.fetch[1]; e->y_addr = d.fetch[1];
e->ok = true; e->ok = true;
bool decode_failed = false;
for (int p = 0; p < 3 && e->ok; ++p) { for (int p = 0; p < 3 && e->ok; ++p) {
const uint32_t* w = d.fetch + p * 6; const uint32_t* w = d.fetch + p * 6;
auto hot = find_words_texture(FetchWordsKey(w)); auto hot = find_words_texture(FetchWordsKey(w));
if (hot != g_r.tex_store.end()) { if (hot != g_r.tex_store.end()) {
hot->second.recheck_frame = 0; // content-hot: per-frame probe hot->second.recheck_frame = 0; // content-hot: per-frame probe
} }
const GuestTexture* t = // Async-capable resolve: a content change serves the stale
resolve_2d_texture(w, /*force_inline=*/true); // plane while the worker re-decodes (the per-frame inline
e->ok = t != &g_r.white && t->texture != nullptr; // re-decode of a 720p plane set cost ~3x4 ms on the render
e->slots[p] = t->srv_slot; // thread every content change: video judder + boot
// sluggishness); nullptr = first decode still in flight.
const GuestTexture* t = resolve_2d_texture(w);
e->ok = t != nullptr && t != &g_r.white && t->texture != nullptr;
decode_failed |= t == &g_r.white;
e->slots[p] = e->ok ? t->srv_slot : 0;
} }
if (!e->ok) { if (decode_failed) {
static std::atomic<uint32_t> s_triple_failed{0}; static std::atomic<uint32_t> s_triple_failed{0};
if (s_triple_failed.fetch_add(1, std::memory_order_relaxed) < 8) { if (s_triple_failed.fetch_add(1, std::memory_order_relaxed) < 8) {
REXLOG_INFO( REXLOG_INFO(
@ -16699,7 +16754,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
yuv = e->slots; yuv = e->slots;
} }
} }
if (yuv == nullptr && (d.flags & 0x2u) != 0 && d.src_stride == 24) { }
if (yuv == nullptr && movie_sub && d.composite_srv < 0 &&
(d.flags & 0x2u) != 0 && d.src_stride == 24) {
// Bracketed movie quad without a readable triple: through ps_main
// it draws its c8 opaque-black cover, also a video quad.
video_quad = true;
yuv = fallback_yuv(); yuv = fallback_yuv();
if (yuv != nullptr) { if (yuv != nullptr) {
static std::atomic<uint32_t> s_fb_logged{0}; static std::atomic<uint32_t> s_fb_logged{0};
@ -16710,10 +16770,23 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
} }
} }
} }
if (video_quad && yuv == nullptr) {
static std::atomic<uint32_t> s_vskip{0};
const uint32_t n = s_vskip.fetch_add(1, std::memory_order_relaxed);
if (n < 8 || (n & 511u) == 0) {
REXLOG_INFO(
"native-scene: video quad skipped (planes not ready, "
"y={:08X}, movie_sub={}) (n={})",
d.fetch[1], movie_sub ? 1 : 0, n);
}
continue;
} }
emit_draw(d, yuv); emit_draw(d, yuv);
movie_drawn |= yuv != nullptr; movie_drawn |= yuv != nullptr;
} }
if (movie_drawn) {
g_movie_native_last_ns.store(movie_now_ns, std::memory_order_relaxed);
}
if (movie_drawn) { if (movie_drawn) {
static std::atomic<bool> s_movie_logged{false}; static std::atomic<bool> s_movie_logged{false};
if (!s_movie_logged.exchange(true, std::memory_order_relaxed)) { if (!s_movie_logged.exchange(true, std::memory_order_relaxed)) {