diff --git a/OPENXR.md b/OPENXR.md index dd1a8e6..ebeffda 100644 --- a/OPENXR.md +++ b/OPENXR.md @@ -750,7 +750,21 @@ full-screen orthographic quad then darkens the image by destination alpha. The e volumes, so that alpha exists in each eye, and the quad has to cover the whole eye: on the virtual screen it shaded only the screen's rectangle, cutting every shadow off at its edge. Retained one-shot EFB bakes such as Mario Kart Wii's minimap are treated as game art and remain eligible for the -screen. A reprojected 2D draw +screen. + +One native effect is replayed differently: a composite that samples a freshly produced, +frame-sized depth copy. Mario Kart Wii draws a ghost kart by rendering it alone into the cleared +EFB, copying the frame's colour and depth out, drawing the race, and blending the copies back +with one orthographic quad whose depth comes from the depth copy. Left on its recorded transforms, +that quad stamps the desktop's flat image of the ghost over each eye, following the head and cut +off by the eye's own ground. An eye therefore skips the perspective draws of the pass that +produced the depth copy where they were recorded and re-issues them in the composite's place, +with this eye's transforms, a constant-alpha blend (`kCompositeSourceAlpha`, about the strength of +the game's own composite) and a depth test against the world the eye has drawn by then: the ghost +in stereo, translucent, where the game put it. The desktop image is unchanged. The link is logged +once (`Immersive replay: pass N ... is re-issued by each eye`). + +A reprojected 2D draw uses the full eye viewport and scissor because its recorded rectangle no longer describes where it ended up; its original viewport is folded into the projection instead. diff --git a/aurora-main/lib/gfx/common.cpp b/aurora-main/lib/gfx/common.cpp index 43dfbb0..494182f 100644 --- a/aurora-main/lib/gfx/common.cpp +++ b/aurora-main/lib/gfx/common.cpp @@ -208,6 +208,9 @@ struct RenderPass { bool postCopyClear = false; bool snapshotColorResolveSource = false; bool efbTarget = false; + // This pass's perspective draws are re-issued by an eye at the composite that samples its + // depth copy (link_composite_sources), and skipped where they were recorded. + bool compositeSource = false; std::vector paletteConvs; }; static std::vector g_renderPasses; @@ -1647,6 +1650,66 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame, return true; } +// Mario Kart Wii draws a ghost kart by rendering it alone into the cleared EFB, copying the +// frame's colour and depth out, drawing the race, and blending the copies back with one +// orthographic quad whose depth comes from the depth copy. That quad is a native effect, so an +// eye would stamp the desktop's flat image of the ghost over its own view, following the head +// and cut by the eye's ground. Instead the eye skips the draws of the pass that produced the +// depth copy where they were recorded and re-issues them in the composite's place, blended +// with kCompositeSourceAlpha and depth-tested against the eye's own world: the ghost in stereo, +// translucent, where the game put it. Runs once the frame's passes are complete, on the +// producer, before the frame is sealed. +static void link_composite_sources() noexcept { + if (g_stereoLocalPlayerCount > 1) { + return; + } + static bool linkLogged = false; + for (size_t i = 0; i < g_renderPasses.size(); ++i) { + if (!g_renderPasses[i].efbTarget) { + continue; + } + for (auto& command : g_renderPasses[i].commands) { + if (command.type != CommandType::Draw || command.data.draw.type != ShaderType::GX) { + continue; + } + auto& layout = command.data.draw.gx.uniformReplayLayout; + layout.compositeSourcePass = -1; + if (!layout.nativeEfbEffect || layout.perspective || layout.compositeDepthCopy == nullptr) { + continue; + } + for (size_t p = i; p-- > 0;) { + auto& source = g_renderPasses[p]; + if (!source.efbTarget || source.resolveTarget.get() != layout.compositeDepthCopy) { + continue; + } + size_t draws = 0; + for (auto& sourceCommand : source.commands) { + if (sourceCommand.type != CommandType::Draw || sourceCommand.data.draw.type != ShaderType::GX) { + continue; + } + auto& sourceDraw = sourceCommand.data.draw.gx; + if (sourceDraw.uniformReplayLayout.perspective && + gx::resolve_composite_source_pipelines(sourceDraw.configHash, sourceDraw.stereoGhostPipeline, + sourceDraw.stereoGhostStencilPipeline)) { + ++draws; + } + } + if (draws > 0) { + source.compositeSource = true; + layout.compositeSourcePass = static_cast(p); + if (!linkLogged) { + linkLogged = true; + Log.info("Immersive replay: pass {} ({} perspective draws) is re-issued by each eye in place of the " + "composite in pass {} that samples its depth copy", + p, draws, i); + } + } + break; + } + } + } +} + static bool end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame, const StereoReplayFrame* stereoFrame = nullptr) { ZoneScoped; @@ -1666,6 +1729,9 @@ static bool end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame, const bool stereoPrepared = (stereoFrame == nullptr && !captureStereo) || prepare_stereo_replay_uniforms(stereoFrame != nullptr ? *stereoFrame : placeholder, captureStereo ? &g_pendingLateStereo : nullptr); + if (stereoPrepared && (stereoFrame != nullptr || captureStereo)) { + link_composite_sources(); + } if (captureStereo && stereoPrepared) { g_pendingLateStereo.generation = g_replayBufferGeneration.load(std::memory_order_acquire); } else { @@ -2581,6 +2647,102 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec recordedViewport.znear, recordedViewport.zfar); }; + // WebGPU requires 0 <= minDepth <= maxDepth <= 1. vp.znear/vp.zfar are in GX's own distance + // terms (0 = near); under UseReversedZ the host depth-buffer storage direction is flipped + // (near = 1, far = 0), so this range has to be remapped through 1-x the same way the + // projection matrix, depth compare function, and clear value all are - a plain min/max clamp + // (the previous code here) maps a *restricted* range (e.g. a viewport deliberately narrowed + // to force something to draw "in front of everything") to the wrong end of the buffer: what + // should land near the near-storage-extreme (1.0) instead lands near the far-storage-extreme + // (0.0), so anything else drawn afterward at its true depth wins the compare test and the + // "in front" geometry silently vanishes. A full [0,1] viewport is unaffected either way, + // which is why this only broke specific elements, not the whole scene. Matches upstream + // aurora's apply_viewport (lib/gfx/encoding.cpp). + // + // The remapped pair is then ordered and clamped before it reaches WebGPU. For any ordered + // guest range this is a no-op (znear <= zfar implies 1-zfar <= 1-znear), so upstream's fix is + // reproduced exactly; it only guards the swapped pair MKW is known to emit, which the raw + // remap would hand to SetViewport as minDepth > maxDepth and fail validation. The VR eye + // replay below reuses these recorded values, so the guard covers that path too. + const auto map_viewport = [&](const Viewport& vp) noexcept { + const float remappedNear = gx::UseReversedZ ? 1.0f - vp.zfar : vp.znear; + const float remappedFar = gx::UseReversedZ ? 1.0f - vp.znear : vp.zfar; + const float minDepth = std::clamp(std::min(remappedNear, remappedFar), 0.0f, 1.0f); + const float maxDepth = std::clamp(std::max(remappedNear, remappedFar), 0.0f, 1.0f); + return Viewport{ + .left = (vp.left - static_cast(sourceRegionLeft)) * scaleX, + .top = (vp.top - static_cast(sourceRegionTop)) * scaleY, + .width = vp.width * scaleX, + .height = vp.height * scaleY, + .znear = minDepth, + .zfar = maxDepth, + }; + }; + const auto map_scissor = [&](const ClipRect& sc) noexcept { + const auto sourceLeft = std::clamp(sc.x, sourceRegionLeft, sourceRegionRight); + const auto sourceTop = std::clamp(sc.y, sourceRegionTop, sourceRegionBottom); + const auto sourceRight = std::clamp(sc.x + sc.width, sourceLeft, sourceRegionRight); + const auto sourceBottom = std::clamp(sc.y + sc.height, sourceTop, sourceRegionBottom); + const auto left = static_cast( + std::clamp(static_cast(std::floor(static_cast(sourceLeft - sourceRegionLeft) * scaleX)), 0, + static_cast(targetSize.width))); + const auto top = static_cast( + std::clamp(static_cast(std::floor(static_cast(sourceTop - sourceRegionTop) * scaleY)), 0, + static_cast(targetSize.height))); + const auto right = static_cast( + std::clamp(static_cast(std::ceil(static_cast(sourceRight - sourceRegionLeft) * scaleX)), + static_cast(left), static_cast(targetSize.width))); + const auto bottom = static_cast( + std::clamp(static_cast(std::ceil(static_cast(sourceBottom - sourceRegionTop) * scaleY)), + static_cast(top), static_cast(targetSize.height))); + return std::array{left, top, right - left, bottom - top}; + }; + // The perspective draws of a composite's source pass, in the composite's place: their own + // recorded viewport and scissor, this eye's uniforms, the constant-alpha pipeline sibling, + // depth-tested against the world the eye has drawn by now (see link_composite_sources). + const auto replay_composite_source = [&](const RenderPass& source) { + const wgpu::Color alpha{stereo_replay::kCompositeSourceAlpha, stereo_replay::kCompositeSourceAlpha, + stereo_replay::kCompositeSourceAlpha, stereo_replay::kCompositeSourceAlpha}; + const bool stencilTarget = invocation.target->depthFormat == wgpu::TextureFormat::Depth24PlusStencil8; + for (const auto& command : source.commands) { + if (command.type == CommandType::SetViewport) { + const Viewport vp = map_viewport(command.data.setViewport); + pass.SetViewport(vp.left, vp.top, vp.width, vp.height, vp.znear, vp.zfar); + continue; + } + if (command.type == CommandType::SetScissor) { + apply_scissor(map_scissor(command.data.setScissor)); + continue; + } + if (command.type != CommandType::Draw || command.data.draw.type != ShaderType::GX) { + continue; + } + const auto& draw = command.data.draw.gx; + if (!draw.uniformReplayLayout.perspective) { + continue; + } + const gfx::PipelineRef pipeline = stencilTarget ? draw.stereoGhostStencilPipeline : draw.stereoGhostPipeline; + if (pipeline == 0) { + continue; + } + const gfx::Range* uniform = nullptr; + if (invocation.stereoEye < draw.stereoUniformRanges.size() && + draw.stereoUniformRanges[invocation.stereoEye].size != 0) { + uniform = &draw.stereoUniformRanges[invocation.stereoEye]; + } else if (invocation.interpolatedFrame >= 0 && + static_cast(invocation.interpolatedFrame) < draw.interpolatedUniformRanges.size() && + draw.interpolatedUniformRanges[invocation.interpolatedFrame].size != 0) { + uniform = &draw.interpolatedUniformRanges[invocation.interpolatedFrame]; + } + gx::render(draw, pass, encodeState, source.requireReadyPipelines, uniform, pipeline, &alpha); + } + if (invocation.sceneDrawn != nullptr) { + *invocation.sceneDrawn = true; + } + // The source pass's own viewport and scissor were applied; the next draw restores its own. + viewportStateKnown = false; + scissorStateKnown = false; + }; for (const auto& cmd : renderPasses[idx].commands) { #ifdef AURORA_GFX_DEBUG_GROUPS { @@ -2604,35 +2766,7 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec case CommandType::SetViewport: { const auto& vp = cmd.data.setViewport; sourceViewport = {vp.left, vp.top, vp.width, vp.height}; - // WebGPU requires 0 <= minDepth <= maxDepth <= 1. vp.znear/vp.zfar are in GX's own distance - // terms (0 = near); under UseReversedZ the host depth-buffer storage direction is flipped - // (near = 1, far = 0), so this range has to be remapped through 1-x the same way the - // projection matrix, depth compare function, and clear value all are - a plain min/max clamp - // (the previous code here) maps a *restricted* range (e.g. a viewport deliberately narrowed - // to force something to draw "in front of everything") to the wrong end of the buffer: what - // should land near the near-storage-extreme (1.0) instead lands near the far-storage-extreme - // (0.0), so anything else drawn afterward at its true depth wins the compare test and the - // "in front" geometry silently vanishes. A full [0,1] viewport is unaffected either way, - // which is why this only broke specific elements, not the whole scene. Matches upstream - // aurora's apply_viewport (lib/gfx/encoding.cpp). - // - // The remapped pair is then ordered and clamped before it reaches WebGPU. For any ordered - // guest range this is a no-op (znear <= zfar implies 1-zfar <= 1-znear), so upstream's fix is - // reproduced exactly; it only guards the swapped pair MKW is known to emit, which the raw - // remap would hand to SetViewport as minDepth > maxDepth and fail validation. The VR eye - // replay below reuses these recorded values, so the guard covers that path too. - const float remappedNear = gx::UseReversedZ ? 1.0f - vp.zfar : vp.znear; - const float remappedFar = gx::UseReversedZ ? 1.0f - vp.znear : vp.zfar; - const float minDepth = std::clamp(std::min(remappedNear, remappedFar), 0.0f, 1.0f); - const float maxDepth = std::clamp(std::max(remappedNear, remappedFar), 0.0f, 1.0f); - recordedViewport = { - .left = (vp.left - static_cast(sourceRegionLeft)) * scaleX, - .top = (vp.top - static_cast(sourceRegionTop)) * scaleY, - .width = vp.width * scaleX, - .height = vp.height * scaleY, - .znear = minDepth, - .zfar = maxDepth, - }; + recordedViewport = map_viewport(vp); hudScreenViewport = false; viewportStateKnown = true; apply_viewport(false); @@ -2640,23 +2774,7 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec case CommandType::SetScissor: { const auto& sc = cmd.data.setScissor; sourceScissor = {float(sc.x), float(sc.y), float(sc.width), float(sc.height)}; - const auto sourceLeft = std::clamp(sc.x, sourceRegionLeft, sourceRegionRight); - const auto sourceTop = std::clamp(sc.y, sourceRegionTop, sourceRegionBottom); - const auto sourceRight = std::clamp(sc.x + sc.width, sourceLeft, sourceRegionRight); - const auto sourceBottom = std::clamp(sc.y + sc.height, sourceTop, sourceRegionBottom); - const auto left = static_cast( - std::clamp(static_cast(std::floor(static_cast(sourceLeft - sourceRegionLeft) * scaleX)), 0, - static_cast(targetSize.width))); - const auto top = static_cast( - std::clamp(static_cast(std::floor(static_cast(sourceTop - sourceRegionTop) * scaleY)), 0, - static_cast(targetSize.height))); - const auto right = static_cast( - std::clamp(static_cast(std::ceil(static_cast(sourceRight - sourceRegionLeft) * scaleX)), - static_cast(left), static_cast(targetSize.width))); - const auto bottom = static_cast( - std::clamp(static_cast(std::ceil(static_cast(sourceBottom - sourceRegionTop) * scaleY)), - static_cast(top), static_cast(targetSize.height))); - recordedScissor = {left, top, right - left, bottom - top}; + recordedScissor = map_scissor(sc); hudScreenScissor = false; scissorStateKnown = true; apply_scissor(recordedScissor); @@ -2677,6 +2795,15 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec !stereo_replay::subviews_overlap(sourceScissor, playerRegion))) { break; } + // A composite's source pass is drawn at the composite instead (link_composite_sources). + if (overrideTarget && renderPasses[idx].compositeSource) { + break; + } + if (overrideTarget && draw.gx.uniformReplayLayout.compositeSourcePass >= 0 && + static_cast(draw.gx.uniformReplayLayout.compositeSourcePass) < renderPasses.size()) { + replay_composite_source(renderPasses[static_cast(draw.gx.uniformReplayLayout.compositeSourcePass)]); + break; + } const gfx::Range* uniformOverride = nullptr; // Only a 2D draw the virtual screen actually claimed carries a stereo // uniform range without being perspective. diff --git a/aurora-main/lib/gfx/stereo_replay.hpp b/aurora-main/lib/gfx/stereo_replay.hpp index 84e7662..27ec4c0 100644 --- a/aurora-main/lib/gfx/stereo_replay.hpp +++ b/aurora-main/lib/gfx/stereo_replay.hpp @@ -6,6 +6,11 @@ namespace aurora::gfx::stereo_replay { +// The alpha an eye re-issues a composite's source draws with (Mario Kart Wii's ghost kart). +// The game's own composite blends its copy of the ghost at about half strength; the constant +// stands in for the alpha that quad carries in its vertex colour. +constexpr float kCompositeSourceAlpha = 0.5f; + struct SubviewRect { float left = 0.f; float top = 0.f; diff --git a/aurora-main/lib/gx/command_processor.cpp b/aurora-main/lib/gx/command_processor.cpp index b8eb13a..8c27403 100644 --- a/aurora-main/lib/gx/command_processor.cpp +++ b/aurora-main/lib/gx/command_processor.cpp @@ -2281,10 +2281,48 @@ static const CachedPipelineState& resolve_pipeline_state(GXPrimitive prim, GXVtx // Lazily cache eye-format siblings alongside the ordinary pipeline. Steady-state // draws only read the refs: no extra config population/hashing on the Quest CPU. // Shader modules are shared by the depth-format variants. +// The configs of the perspective draws recorded while a stereo frame provider is active, so a +// pass found later to be a composite's source (link_composite_sources) can have constant-alpha +// siblings built for exactly its draws. Nothing is compiled here; cached_pipeline_state's slot +// can be overwritten by the time the frame is sealed, which is why the config is kept by value. +struct CompositeSourceConfig { + PipelineConfig config{}; + gfx::PipelineRef plain = 0; + gfx::PipelineRef stencil = 0; +}; +static absl::flat_hash_map s_compositeSourceConfigs; +static std::mutex s_compositeSourceConfigsMutex; +constexpr size_t kMaxCompositeSourceConfigs = 4096; + +static void remember_composite_source_config(const CachedPipelineState& state) { + std::lock_guard lock{s_compositeSourceConfigsMutex}; + if (s_compositeSourceConfigs.contains(state.configHash)) return; + if (s_compositeSourceConfigs.size() >= kMaxCompositeSourceConfigs) s_compositeSourceConfigs.clear(); + s_compositeSourceConfigs.emplace(state.configHash, CompositeSourceConfig{.config = *state.config}); +} + +static bool resolve_composite_source_pipelines_impl(HashType configHash, gfx::PipelineRef& plain, + gfx::PipelineRef& stencil) { + std::lock_guard lock{s_compositeSourceConfigsMutex}; + const auto it = s_compositeSourceConfigs.find(configHash); + if (it == s_compositeSourceConfigs.end()) return false; + auto& entry = it->second; + if (entry.plain == 0) { + PipelineConfig config = entry.config; + config.stereoStencil = kCompositeSourceBlend; + entry.plain = gfx::pipeline_ref(config); + config.stereoStencil = kCompositeSourceBlend | kStereoStencilFormat; + entry.stencil = gfx::pipeline_ref(config); + } + plain = entry.plain; + stencil = entry.stencil; + return true; +} + static void resolve_replay_pipelines(const CachedPipelineState& state, bool screen) { if (state.stereoRef && (!screen || state.stereoScreenRef)) return; PipelineConfig config = *state.config; - config.stereoStencil = 1; + config.stereoStencil = kStereoStencilFormat; if (!state.stereoRef) state.stereoRef = gfx::pipeline_ref(config); if (screen && !state.stereoScreenRef) { config.shaderConfig.exactScreenDepth = 1; @@ -2499,6 +2537,7 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g const bool stereo = aurora::stereo_frame_provider_active(); const bool screen = !replayLayout.perspective && !replayLayout.nativeEfbEffect; if (stereo) resolve_replay_pipelines(pipelineState, screen); + if (stereo && replayLayout.perspective) remember_composite_source_config(pipelineState); s_lastDrawRecordedInterpolation = interpolationIdentityActive; uint32_t instanceCount = 1; @@ -2521,6 +2560,7 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g .stereoUniformRanges = {}, .previousUniformRange = uniformRanges.previous, .uniformReplayLayout = uniformRanges.replayLayout, + .configHash = pipelineState.configHash, .vtxCount = vtxCount, .indexCount = numIndices, .instanceCount = instanceCount, @@ -2697,3 +2737,8 @@ bool handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) { } } // namespace aurora::gx::fifo + +bool aurora::gx::resolve_composite_source_pipelines(HashType configHash, gfx::PipelineRef& plain, + gfx::PipelineRef& stencil) { + return fifo::resolve_composite_source_pipelines_impl(configHash, plain, stencil); +} diff --git a/aurora-main/lib/gx/gx.cpp b/aurora-main/lib/gx/gx.cpp index c85a2cb..3c74cb6 100644 --- a/aurora-main/lib/gx/gx.cpp +++ b/aurora-main/lib/gx/gx.cpp @@ -1609,12 +1609,13 @@ static inline wgpu::PrimitiveState to_primitive_state(GXCullMode gx_cullMode) { wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef vtxBuffers, wgpu::ShaderModule shader, const char* label) noexcept { ZoneScoped; - const bool maskCockpit = config.stereoStencil && config.shaderConfig.exactScreenDepth; + const bool stencilTarget = (config.stereoStencil & kStereoStencilFormat) != 0; + const bool maskCockpit = stencilTarget && config.shaderConfig.exactScreenDepth; const wgpu::StencilFaceState stencil{ .compare = maskCockpit ? wgpu::CompareFunction::Equal : wgpu::CompareFunction::Always, }; const wgpu::DepthStencilState depthStencil{ - .format = config.stereoStencil ? wgpu::TextureFormat::Depth24PlusStencil8 : g_graphicsConfig.depthFormat, + .format = stencilTarget ? wgpu::TextureFormat::Depth24PlusStencil8 : g_graphicsConfig.depthFormat, .depthWriteEnabled = config.depthUpdate, .depthCompare = config.depthCompare ? to_compare_function(config.depthFunc) : wgpu::CompareFunction::Always, .stencilFront = stencil, @@ -1622,8 +1623,22 @@ wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRefis_recent_efb_copy(currentFrame)) { samplesRecentEfbCopy = true; - samplesReducedEfbCopy = - samplesReducedEfbCopy || tex.texObj.width() * 2 <= kEfbWidth || tex.texObj.height() * 2 <= kEfbHeight; + const bool reduced = tex.texObj.width() * 2 <= kEfbWidth || tex.texObj.height() * 2 <= kEfbHeight; + samplesReducedEfbCopy = samplesReducedEfbCopy || reduced; + if (!perspective && !reduced && is_depth_format(tex.texObj.format())) { + compositeDepthCopy = tex.ref.get(); + } } buf.append(texture_size_bias(tex)); } @@ -778,6 +782,8 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou (drawViewport.width < static_cast(fbWidth) * 0.9f || drawViewport.height < static_cast(fbHeight) * 0.9f); + const bool nativeEfbEffect = + !perspective && (readsDstAlpha || (samplesRecentEfbCopy && (samplesReducedEfbCopy || blends || offscreenViewport))); const UniformReplayLayout replayLayout{ .projectionOffset = static_cast(projectionOffset), .positionOffset = static_cast(positionOffset), @@ -787,8 +793,8 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou .normalMatrixCount = layout.nrmCount, .perspective = perspective, .indexedMatrices = info.indexAttr.test(GX_VA_PNMTXIDX), - .nativeEfbEffect = !perspective && (readsDstAlpha || (samplesRecentEfbCopy && - (samplesReducedEfbCopy || blends || offscreenViewport))), + .nativeEfbEffect = nativeEfbEffect, + .compositeDepthCopy = nativeEfbEffect ? compositeDepthCopy : nullptr, }; if (!perspective || !frame_interpolation_active()) { diff --git a/aurora-main/lib/gx/shader_info.hpp b/aurora-main/lib/gx/shader_info.hpp index 54fcb0a..877d876 100644 --- a/aurora-main/lib/gx/shader_info.hpp +++ b/aurora-main/lib/gx/shader_info.hpp @@ -18,6 +18,16 @@ struct UniformReplayLayout { // rest of the native post-processing chain. It belongs to the rendered image, // not to the game's 2D layer, so it must stay where the game aimed it. bool nativeEfbEffect = false; + // For a native effect that samples a freshly produced, frame-sized depth copy: that + // copy. Mario Kart Wii draws a ghost kart alone into the cleared EFB, copies the + // frame's colour and depth out, draws the race, then blends the copies back with one + // such quad. Set when the draw is recorded; the pass that produced the copy holds the + // ghost's own perspective draws. + const gfx::TextureRef* compositeDepthCopy = nullptr; + // Index of that pass once the frame is sealed (link_composite_sources), or -1. An eye + // re-issues its perspective draws in this composite's place instead of stamping the + // desktop's flat image of the ghost over its own view. + int32_t compositeSourcePass = -1; }; struct UniformRanges { diff --git a/aurora-main/tests/gx_fifo_test.cpp b/aurora-main/tests/gx_fifo_test.cpp index 45897ea..c3b2fd5 100644 --- a/aurora-main/tests/gx_fifo_test.cpp +++ b/aurora-main/tests/gx_fifo_test.cpp @@ -5246,3 +5246,73 @@ TEST_F(GXFifoTest, OffscreenViewportPassOverFreshCopyIsNativeEfbEffect) { aurora::gx::FrameInterpolationDrawIdentity{}, false); EXPECT_FALSE(frameLayout.replayLayout.nativeEfbEffect); } + +TEST(GXPipelineConfig, StereoStencilFieldCarriesTheCompositeSourceBlendBit) { + aurora::gx::PipelineConfig config{}; + config.stereoStencil = aurora::gx::kStereoStencilFormat | aurora::gx::kCompositeSourceBlend; + EXPECT_TRUE(aurora::gx::valid_pipeline_config(config)); + config.stereoStencil = (aurora::gx::kStereoStencilFormat | aurora::gx::kCompositeSourceBlend) + 1; + EXPECT_FALSE(aurora::gx::valid_pipeline_config(config)); +} + +TEST_F(GXFifoTest, NativeCompositeRecordsTheFrameSizedDepthCopyItSamples) { + // Mario Kart Wii's ghost kart: one orthographic quad blends a frame-sized colour copy back + // over the race with the depth of a frame-sized depth copy. The layout names that depth copy so + // the eye replay can find the pass whose draws produced it. + // Frame-sized buffers: static so two of them do not exhaust the test thread's stack. + static std::array colour{}; + static std::array depth{}; + gxState().pixelFmt = GX_PF_RGBA6_Z24; + aurora::gfx::testing::set_framebuffer_sizes(640, 528, 640, 528); + aurora::gfx::testing::set_current_frame(50); + + GXSetTexCopySrc(0, 0, 608, 456); + GXSetTexCopyDst(608, 456, GX_TF_Z24X8, GX_FALSE); + GXCopyTex(depth.data(), GX_TRUE); + GXSetTexCopyDst(608, 456, GX_TF_RGB565, GX_FALSE); + GXCopyTex(colour.data(), GX_TRUE); + const auto& records = aurora::gfx::testing::resolve_pass_records(); + ASSERT_EQ(records.size(), 2u); + ASSERT_TRUE(records[0].texture); + ASSERT_TRUE(records[1].texture); + + GXTexObj_ colourObj{}; + colourObj.mWidth = 608; + colourObj.mHeight = 456; + colourObj.mFormat = GX_TF_RGB565; + GXTexObj_ depthObj{}; + depthObj.mWidth = 608; + depthObj.mHeight = 456; + depthObj.mFormat = GX_TF_Z24X8; + gxState().textures[GX_TEXMAP0] = aurora::gfx::TextureBind{colourObj, records[1].texture}; + gxState().textures[GX_TEXMAP1] = aurora::gfx::TextureBind{depthObj, records[0].texture}; + gxState().blendMode = GX_BM_BLEND; + gxState().blendFacSrc = GX_BL_SRCALPHA; + gxState().blendFacDst = GX_BL_INVSRCALPHA; + gxState().logicalViewport = {0.0f, 0.0f, 640.0f, 528.0f, 0.0f, 1.0f}; + aurora::gx::ShaderConfig shader{}; + shader.numTexGens = 2; + shader.tevStageCount = 2; + shader.tevStages[0].texCoordId = GX_TEXCOORD0; + shader.tevStages[0].texMapId = GX_TEXMAP0; + shader.tevStages[0].colorPass.d = GX_CC_TEXC; + shader.tevStages[0].alphaPass.d = GX_CA_TEXA; + shader.tevStages[1].texCoordId = GX_TEXCOORD1; + shader.tevStages[1].texMapId = GX_TEXMAP1; + shader.tevStages[1].colorPass.d = GX_CC_TEXC; + shader.tevStages[1].alphaPass.d = GX_CA_TEXA; + const auto info = aurora::gx::build_shader_info(shader); + + aurora::gfx::testing::reset_uniform_allocations(); + const auto composite = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{}, + aurora::gx::FrameInterpolationDrawIdentity{}, false); + EXPECT_TRUE(composite.replayLayout.nativeEfbEffect); + EXPECT_EQ(composite.replayLayout.compositeDepthCopy, records[0].texture.get()); + EXPECT_EQ(composite.replayLayout.compositeSourcePass, -1); + + // The same draw with the game camera is world geometry, whatever it samples. + aurora::gfx::testing::reset_uniform_allocations(); + const auto world = aurora::gx::build_uniform(info, 0, aurora::gx::BindGroupRanges{}, + aurora::gx::FrameInterpolationDrawIdentity{}, true); + EXPECT_EQ(world.replayLayout.compositeDepthCopy, nullptr); +}