diff --git a/aurora-main/lib/gfx/common.cpp b/aurora-main/lib/gfx/common.cpp index 2d87f1b..12cac7e 100644 --- a/aurora-main/lib/gfx/common.cpp +++ b/aurora-main/lib/gfx/common.cpp @@ -1610,6 +1610,45 @@ static void write_stereo_uniform(std::span uniform, const gx::UniformRe } } +// KARTFRAME_LOG_2D_QUADS=1 (diagnostics): the distinct 2D quads of the immersive replay, once each, with +// whether they went across the eye (full view) or onto the virtual screen. +static void log_replay_quad(const gx::DrawData& draw, bool fullView) noexcept { + // =1: full-view draws and quads sampling an EFB copy (menus draw thousands of others); =x: quads with + // an edge within 4 px of frame x = n. + static const float edgeX = [] { + const char* env = std::getenv("KARTFRAME_LOG_2D_QUADS"); + return env != nullptr && env[0] == 'x' ? static_cast(std::atof(env + 1)) : -1.f; + }(); + static const bool enabled = [] { + const char* env = std::getenv("KARTFRAME_LOG_2D_QUADS"); + return env != nullptr && (env[0] == '1' || env[0] == 'x'); + }(); + if (!enabled || !draw.screenRect) { + return; + } + if (edgeX >= 0.f) { + const auto& q = *draw.screenRect; + if (std::fabs(q.left - edgeX) > 4.f && std::fabs(q.left + q.width - edgeX) > 4.f) { + return; + } + } else if (!fullView && ((draw.quadSignature >> 32) & 1) == 0) { + return; + } + static std::vector> seen; + const auto& r = *draw.screenRect; + const uint64_t rect = uint64_t(int(r.left) + 4096) | (uint64_t(int(r.top) + 4096) << 13) | (uint64_t(int(r.width)) << 26) | + (uint64_t(int(r.height)) << 39) | (uint64_t(fullView) << 52); + const std::pair key{rect, draw.quadSignature}; + if (seen.size() >= 600 || std::find(seen.begin(), seen.end(), key) != seen.end()) { + return; + } + seen.push_back(key); + const uint64_t s = draw.quadSignature; + Log.info("Immersive 2D quad{}: {:.1f}x{:.1f} at ({:.1f}, {:.1f}); blend {} logic {} texgens {} tex0 {}x{}{}", + fullView ? " (full view)" : "", r.width, r.height, r.left, r.top, s & 3, (s >> 2) & 15, (s >> 6) & 15, + (s >> 10) & 2047, (s >> 21) & 2047, ((s >> 32) & 1) != 0 ? " (EFB copy)" : ""); +} + static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame, LateStereoData* history = nullptr) noexcept { const StereoDisplaySource displaySource = stereo_display_source(g_renderPasses); @@ -1630,6 +1669,33 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame, return (!multiplayer || !layout.nativeEfbEffect) && (layout.perspective || (hudScreen.valid() && !layout.nativeEfbEffect)); }; + // A 2D rectangle covering the whole frame is one of the game's fades (the black of a respawn, the race + // start and end wipes, the scene fader, a white flash): on a TV it covers the picture, so in the + // headset it covers the view. It is not claimed by the virtual screen and keeps its recorded + // transforms, which the eye replay maps from the frame onto the whole eye (head-locked, like a 2D + // draw with the virtual screen off). Not in the immersive window, nor split screen. + // KARTFRAME_FULL_VIEW_FADES=0 leaves them on the screen. + static const bool fullViewFadesEnabled = [] { + const char* env = std::getenv("KARTFRAME_FULL_VIEW_FADES"); + return env == nullptr || env[0] != '0'; + }(); + const bool fullViewFades = fullViewFadesEnabled && !stereoFrame.window && !multiplayer && hudScreen.valid() && + displayRegion.width > 0 && displayRegion.height > 0; + // Only over the scene: a full-frame quad before the pass's first perspective draw is a background (the race + // draws an opaque one every frame), which stays where it was. + bool passSawScene = false; + const auto fullView = [&](const gx::DrawData& draw) noexcept { + if (!fullViewFades || !passSawScene || draw.uniformReplayLayout.perspective || !draw.screenRect || + !draw.fadeCandidate) { + return false; + } + const auto& r = *draw.screenRect; + const float w = static_cast(displayRegion.width), h = static_cast(displayRegion.height); + const float x = static_cast(displayRegion.x), y = static_cast(displayRegion.y); + return r.left <= x + 0.02f * w && r.top <= y + 0.02f * h && r.left + r.width >= x + 0.98f * w && + r.top + r.height >= y + 0.98f * h; + }; + size_t fullViewDrawCount = 0; size_t requiredBytes = 0; size_t efbPassCount = 0; size_t perspectiveDrawCount = 0; @@ -1641,6 +1707,7 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame, if (pass.efbTarget) { ++efbPassCount; } + passSawScene = false; for (const auto& command : pass.commands) { if (pass.efbTarget && command.type == CommandType::SetViewport) { const auto& vp = command.data.setViewport; @@ -1650,6 +1717,15 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame, !replayed(command.data.draw.gx.uniformReplayLayout)) { continue; } + passSawScene = passSawScene || command.data.draw.gx.uniformReplayLayout.perspective; + if (pass.efbTarget && fullView(command.data.draw.gx)) { + ++fullViewDrawCount; + log_replay_quad(command.data.draw.gx, true); + continue; + } + if (pass.efbTarget && !command.data.draw.gx.uniformReplayLayout.perspective) { + log_replay_quad(command.data.draw.gx, false); + } const auto& draw = command.data.draw.gx; const auto& layout = draw.uniformReplayLayout; if (layout.perspective) { @@ -1687,17 +1763,20 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame, } } // 2D draws on the race's virtual screen, per 10 s (they carry the screen clip). - static uint64_t hudDrawsWindow = 0, hudFramesWindow = 0; + static uint64_t hudDrawsWindow = 0, hudFramesWindow = 0, fullViewDrawsWindow = 0, fullViewFramesWindow = 0; static auto hudWindowStart = std::chrono::steady_clock::now(); hudDrawsWindow += replayHudScreenDrawCount; + fullViewDrawsWindow += fullViewDrawCount; + fullViewFramesWindow += fullViewDrawCount != 0 ? 1 : 0; ++hudFramesWindow; if (const auto now = std::chrono::steady_clock::now(); now - hudWindowStart >= std::chrono::seconds(10)) { if (hudScreen.valid()) { - Log.info("Immersive replay: {:.1f} 2D draws a frame on the virtual screen (10 s); screen clip self-check {:.5f}", + Log.info("Immersive replay: {:.1f} 2D draws a frame on the virtual screen (10 s); screen clip self-check {:.5f}; " + "full-view fades: {} draws in {} frames", static_cast(hudDrawsWindow) / static_cast(std::max(hudFramesWindow, 1)), - g_screenClipCheck.load(std::memory_order_relaxed)); + g_screenClipCheck.load(std::memory_order_relaxed), fullViewDrawsWindow, fullViewFramesWindow); } - hudDrawsWindow = hudFramesWindow = 0; + hudDrawsWindow = hudFramesWindow = fullViewDrawsWindow = fullViewFramesWindow = 0; hudWindowStart = now; } static bool replayCoverageLogged = false; @@ -1744,6 +1823,7 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame, continue; } ClipRect drawScissor{}; // none until the pass sets one + passSawScene = false; for (auto& command : pass.commands) { if (command.type == CommandType::SetViewport) { drawViewport = command.data.setViewport; @@ -1759,6 +1839,10 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame, !replayed(command.data.draw.gx.uniformReplayLayout)) { continue; } + passSawScene = passSawScene || command.data.draw.gx.uniformReplayLayout.perspective; + if (fullView(command.data.draw.gx)) { + continue; + } auto& draw = command.data.draw.gx; const auto& layout = draw.uniformReplayLayout; const stereo_replay::SubviewRect viewportRect{drawViewport.left, drawViewport.top, drawViewport.width, diff --git a/aurora-main/lib/gx/command_processor.cpp b/aurora-main/lib/gx/command_processor.cpp index b3c3f87..a5fdc53 100644 --- a/aurora-main/lib/gx/command_processor.cpp +++ b/aurora-main/lib/gx/command_processor.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -2515,6 +2516,44 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g } else if (prim == GX_POINTS) { instanceCount = vtxCount; } + const auto drawScreenRect = screen_rect(prim, fmt, vertices, vtxCount, vtxStride); + // Fades are blended over the picture (the respawn's black filter, wipes, the scene fader). Opaque and + // logic-op full-frame quads are not: the race draws such quads every frame (backgrounds, a logic-op + // pass), and one shown across the eye blacked it out. + const bool fadeCandidate = g_gxState.colorUpdate && g_gxState.blendMode == GX_BM_BLEND; + const auto quad_signature = [] { + const auto& tex = get_texture(GX_TEXMAP0); + const uint32_t w = tex.ref ? std::min(tex.texObj.width(), 2047) : 0; + const uint32_t h = tex.ref ? std::min(tex.texObj.height(), 2047) : 0; + return uint64_t(g_gxState.blendMode & 3) | (uint64_t(g_gxState.blendOp & 15) << 2) | + (uint64_t(g_gxState.numTexGens & 15) << 6) | (uint64_t(w) << 10) | (uint64_t(h) << 21) | + (uint64_t(tex.ref && tex.ref->isEfbCopy) << 32); + }; + // Large 2D rectangles in VR (lane H: which full-frame fades the game draws), a few distinct kinds a run. + if (drawScreenRect && stereo) { + const auto& vp = g_gxState.renderViewport; + if (drawScreenRect->width * drawScreenRect->height >= 0.5f * vp.width * vp.height) { + static std::mutex loggedMutex; + static std::vector logged; + const auto& c = g_gxState.colorChannelState[0].matColor; + const uint64_t key = (uint64_t(g_gxState.blendMode) << 0) | (uint64_t(g_gxState.colorUpdate) << 4) | + (uint64_t(g_gxState.alphaUpdate) << 5) | (uint64_t(g_gxState.depthUpdate) << 6) | + (uint64_t(g_gxState.depthCompare) << 7) | (uint64_t(g_gxState.numTevStages) << 8) | + (uint64_t(g_gxState.numTexGens) << 16) | (uint64_t(drawScreenRect->width) << 24) | + (uint64_t(drawScreenRect->height) << 40); + std::lock_guard lock(loggedMutex); + if (logged.size() < 24 && std::find(logged.begin(), logged.end(), key) == logged.end()) { + logged.push_back(key); + Log.info("VR large 2D rectangle: {:.0f}x{:.0f} at ({:.0f}, {:.0f}) of a {:.0f}x{:.0f} viewport; blend {} colour " + "{} alpha {} depth update {} compare {} func {}; {} TEV stages, {} texgens; material colour " + "{:.2f} {:.2f} {:.2f} {:.2f}", + drawScreenRect->width, drawScreenRect->height, drawScreenRect->left, drawScreenRect->top, vp.width, + vp.height, int(g_gxState.blendMode), g_gxState.colorUpdate, g_gxState.alphaUpdate, + g_gxState.depthUpdate, g_gxState.depthCompare, int(g_gxState.depthFunc), g_gxState.numTevStages, + g_gxState.numTexGens, c[0], c[1], c[2], c[3]); + } + } + } gfx::push_draw_command(DrawData{ .pipeline = pipeline, .exactScreenDepthPipeline = stereo && screen ? pipelineState.screenRef : 0, @@ -2532,7 +2571,9 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g .instanceCount = instanceCount, .bindGroups = bindGroups, .dstAlpha = pipelineState.dstAlpha, - .screenRect = screen_rect(prim, fmt, vertices, vtxCount, vtxStride), + .screenRect = drawScreenRect, + .fadeCandidate = fadeCandidate, + .quadSignature = drawScreenRect ? quad_signature() : uint64_t{0}, }); // What the next draw must match to be allowed to fold into this one. nativeWheelLastDrawCommand = gfx::get_last_draw_command(); diff --git a/aurora-main/lib/gx/pipeline.hpp b/aurora-main/lib/gx/pipeline.hpp index d28ed1f..2bde23e 100644 --- a/aurora-main/lib/gx/pipeline.hpp +++ b/aurora-main/lib/gx/pipeline.hpp @@ -30,6 +30,12 @@ struct DrawData { // Recorded before merging so VR can omit desktop split masks without // modifying GX. std::optional screenRect; + // Writes colour, blended or opaque (not a logic-op or depth-only quad): a candidate for a full-frame + // fade shown across the whole eye. + bool fadeCandidate = false; + // Diagnostics (KARTFRAME_LOG_2D_QUADS): blend | logic << 2 | texgens << 6 | tex0 width << 10 | height << 21 | + // tex0 is an EFB copy << 32. + uint64_t quadSignature = 0; }; constexpr uint32_t GXPipelineConfigVersion = 22;