mirror of
https://github.com/simplysanatori/KartFrame.git
synced 2026-10-17 13:01:26 +02:00
Full-frame fades cover the whole eye in the race, not just the virtual screen (R1)
A blended 2D rectangle covering the whole frame, drawn after the pass's scene (the respawn's black filter, the race start/end wipes and fades, the scene fader, white flashes) is no longer claimed by the virtual screen: the eye replay maps it from the frame onto the whole eye, head-locked. Opaque and logic-op full-frame quads (backgrounds, a logic-op pass every frame) stay as they were; one shown across the eye blacked it out in a test. Not in the immersive window or split screen. KARTFRAME_FULL_VIEW_FADES=0 restores the old behaviour. Mock race start: the fade-in darkens the whole view instead of showing a black rectangle on the screen. The "Immersive replay" line counts full-view draws; "VR large 2D rectangle" lines (24 at most) and KARTFRAME_LOG_2D_QUADS help find other full-screen effects (the Blooper ink was not reproduced headless). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4b87fcfca7
commit
f127ea4538
3 files changed
+136
-5
No files matched your search
@@ -1610,6 +1610,45 @@ static void write_stereo_uniform(std::span<uint8_t> 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<n>: 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<float>(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<std::pair<uint64_t, uint64_t>> 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<uint64_t, uint64_t> 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<float>(displayRegion.width), h = static_cast<float>(displayRegion.height);
|
||||
const float x = static_cast<float>(displayRegion.x), y = static_cast<float>(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<double>(hudDrawsWindow) / static_cast<double>(std::max<uint64_t>(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,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -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<uint32_t>(tex.texObj.width(), 2047) : 0;
|
||||
const uint32_t h = tex.ref ? std::min<uint32_t>(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<uint64_t> 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<DrawData>();
|
||||
|
||||
@@ -30,6 +30,12 @@ struct DrawData {
|
||||
// Recorded before merging so VR can omit desktop split masks without
|
||||
// modifying GX.
|
||||
std::optional<gfx::stereo_replay::SubviewRect> 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;
|
||||
|
||||
Reference in new issue
Block a user