From e89988f92e8a1075e3dac38b7a5afe84c169448b Mon Sep 17 00:00:00 2001 From: iChris4 Date: Sat, 26 Sep 2026 05:51:43 +0200 Subject: [PATCH] Refactor composite source handling: unify link_composite_source logic and update related pipeline references --- OPENXR.md | 7 +- aurora-main/lib/gfx/common.cpp | 142 +++++++++++------------ aurora-main/lib/gx/command_processor.cpp | 91 ++++++++------- aurora-main/lib/gx/pipeline.hpp | 17 +-- aurora-main/lib/gx/shader_info.hpp | 9 +- aurora-main/tests/gx_fifo_test.cpp | 83 ++++++++++++- aurora-main/tests/gx_test_stubs.cpp | 3 +- 7 files changed, 220 insertions(+), 132 deletions(-) diff --git a/OPENXR.md b/OPENXR.md index ebeffda..1b123d4 100644 --- a/OPENXR.md +++ b/OPENXR.md @@ -761,7 +761,12 @@ off by the eye's own ground. An eye therefore skips the perspective draws of the 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 +in stereo, translucent, where the game put it. The desktop image is unchanged. The link is made as +the composite is recorded, so frames without a composite do no work for it, and a draw only +costs one comparison. The constant-alpha pipeline variants exist only for pipelines seen among +a composite's source draws: such a pipeline gets them from its next draw, and until then the +eye leaves that draw out, so a ghost first drawn in a session is missing from the eyes for that +one frame. The link is logged once (`Immersive replay: pass N ... is re-issued by each eye`). A reprojected 2D draw diff --git a/aurora-main/lib/gfx/common.cpp b/aurora-main/lib/gfx/common.cpp index 494182f..78b7a9e 100644 --- a/aurora-main/lib/gfx/common.cpp +++ b/aurora-main/lib/gfx/common.cpp @@ -209,7 +209,7 @@ struct RenderPass { 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. + // depth copy (link_composite_source), and skipped where they were recorded. bool compositeSource = false; std::vector paletteConvs; }; @@ -954,8 +954,57 @@ void end_offscreen() { push_command(CommandType::SetScissor, Command::Data{.setScissor = g_cachedScissor}); } +// 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 resolved the +// depth copy where they were recorded and re-issues them in the composite's place, blended +// with kCompositeSourceAlpha and depth-tested against its own world: the ghost in stereo, +// translucent, where the game put it. +// +// Linked as the composite is recorded, so a frame without one does no work for it. The source +// pass is complete by then, and only the current pass ever moves (offscreen suspension), so it +// is found by the copy it resolved. A source draw without constant-alpha siblings asks for them +// here; its pipeline's next draw brings them, and until then the eye leaves that draw out. +static void link_composite_source(const gx::DrawData& composite) noexcept { + if (g_currentRenderPass >= g_renderPasses.size()) { + return; + } + static bool linkLogged = false; + for (size_t p = g_currentRenderPass; p-- > 0;) { + auto& source = g_renderPasses[p]; + if (!source.efbTarget || source.resolveTarget.get() != composite.uniformReplayLayout.compositeDepthCopy) { + continue; + } + size_t draws = 0; + for (const auto& command : source.commands) { + if (command.type != CommandType::Draw || command.data.draw.type != ShaderType::GX || + !command.data.draw.gx.uniformReplayLayout.perspective) { + continue; + } + ++draws; + if (command.data.draw.gx.stereoCompositeSourcePipeline == 0) { + gx::note_composite_source_pipeline(command.data.draw.gx.pipeline); + } + } + if (draws > 0) { + source.compositeSource = true; + if (!linkLogged) { + linkLogged = true; + Log.info("Immersive replay: pass {} ({} perspective draws) is re-issued by each eye in place of a " + "composite that samples its depth copy", + p, draws); + } + } + return; + } +} + template <> void push_draw_command(gx::DrawData data) { + if (data.uniformReplayLayout.compositeDepthCopy != nullptr && stereo_frame_provider_active()) + UNLIKELY { link_composite_source(data); } push_draw_command(ShaderDrawCommand{.type = ShaderType::GX, .gx = data}); } @@ -1650,66 +1699,6 @@ 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; @@ -1729,9 +1718,6 @@ 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 { @@ -2697,9 +2683,20 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec static_cast(top), static_cast(targetSize.height))); return std::array{left, top, right - left, bottom - top}; }; + // Composites serve one camera; a multiplayer eye keeps every draw where it was recorded. + const bool compositeReplay = overrideTarget && !multiplayer; + const bool skipCompositeSource = compositeReplay && renderPasses[idx].compositeSource; + const auto find_composite_source = [&](const TextureRef* depthCopy) -> const RenderPass* { + for (u32 p = 0; p < idx; ++p) { + if (renderPasses[p].compositeSource && renderPasses[p].resolveTarget.get() == depthCopy) { + return &renderPasses[p]; + } + } + return nullptr; + }; // 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). + // depth-tested against the world the eye has drawn by now (see link_composite_source). const auto replay_composite_source = [&](const RenderPass& source) { const wgpu::Color alpha{stereo_replay::kCompositeSourceAlpha, stereo_replay::kCompositeSourceAlpha, stereo_replay::kCompositeSourceAlpha, stereo_replay::kCompositeSourceAlpha}; @@ -2721,7 +2718,7 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec if (!draw.uniformReplayLayout.perspective) { continue; } - const gfx::PipelineRef pipeline = stencilTarget ? draw.stereoGhostStencilPipeline : draw.stereoGhostPipeline; + const gfx::PipelineRef pipeline = stencilTarget ? draw.stereoCompositeSourcePipeline : draw.compositeSourcePipeline; if (pipeline == 0) { continue; } @@ -2795,14 +2792,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) { + // A composite's source pass is drawn at the composite instead (link_composite_source). + if (skipCompositeSource) { 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; + if (compositeReplay && draw.gx.uniformReplayLayout.compositeDepthCopy != nullptr) { + if (const RenderPass* source = find_composite_source(draw.gx.uniformReplayLayout.compositeDepthCopy)) { + replay_composite_source(*source); + break; + } } const gfx::Range* uniformOverride = nullptr; // Only a 2D draw the virtual screen actually claimed carries a stereo diff --git a/aurora-main/lib/gx/command_processor.cpp b/aurora-main/lib/gx/command_processor.cpp index 8c27403..64ab6c3 100644 --- a/aurora-main/lib/gx/command_processor.cpp +++ b/aurora-main/lib/gx/command_processor.cpp @@ -12,6 +12,7 @@ #include "../internal.hpp" #include +#include #include #include @@ -2210,6 +2211,11 @@ struct CachedPipelineState { mutable gfx::PipelineRef stereoRef = 0; mutable gfx::PipelineRef screenRef = 0; mutable gfx::PipelineRef stereoScreenRef = 0; + // Constant-alpha siblings, while ref is in s_compositeSourcePipelines. Checked again only when + // compositeSourceGeneration falls behind that set (resolve_composite_source_pipelines). + mutable gfx::PipelineRef compositeSourceRef = 0; + mutable gfx::PipelineRef stereoCompositeSourceRef = 0; + mutable u32 compositeSourceGeneration = 0; HashType configHash = 0; // Carried here so the draw can be recorded without keeping the PipelineConfig that produced it alive; it is the only // field of the config the draw itself still needs. @@ -2278,47 +2284,45 @@ static const CachedPipelineState& resolve_pipeline_state(GXPrimitive prim, GXVtx return state; } +// The pipelines an eye re-issues at a composite (gfx::link_composite_source). Only the draw +// paths touch it, under the renderer lock they hold. +static absl::flat_hash_set s_compositeSourcePipelines; +// Advanced whenever that set changes, so each cached pipeline state checks its membership once. +static u32 s_compositeSourceGeneration = 1; +constexpr size_t kMaxCompositeSourcePipelines = 1024; + +static void note_composite_source_pipeline_impl(gfx::PipelineRef pipeline) noexcept { + if (s_compositeSourcePipelines.size() >= kMaxCompositeSourcePipelines) { + s_compositeSourcePipelines.clear(); + } + if (s_compositeSourcePipelines.insert(pipeline).second && ++s_compositeSourceGeneration == 0) { + s_compositeSourceGeneration = 1; + } +} + +// A perspective draw pays one compare here. Its pipeline state builds the constant-alpha siblings +// from its own config, which is live now, once it is in the set; the config never has to be kept. +static void resolve_composite_source_pipelines(const CachedPipelineState& state) { + if (state.compositeSourceGeneration == s_compositeSourceGeneration) + LIKELY { return; } + state.compositeSourceGeneration = s_compositeSourceGeneration; + if (!s_compositeSourcePipelines.contains(state.ref)) { + state.compositeSourceRef = 0; + state.stereoCompositeSourceRef = 0; + return; + } + if (state.compositeSourceRef == 0) { + PipelineConfig config = *state.config; + config.stereoStencil = kCompositeSourceBlend; + state.compositeSourceRef = gfx::pipeline_ref(config); + config.stereoStencil = kCompositeSourceBlend | kStereoStencilFormat; + state.stereoCompositeSourceRef = gfx::pipeline_ref(config); + } +} + // 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; @@ -2537,7 +2541,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); + if (stereo && replayLayout.perspective) resolve_composite_source_pipelines(pipelineState); s_lastDrawRecordedInterpolation = interpolationIdentityActive; uint32_t instanceCount = 1; @@ -2553,6 +2557,9 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g .exactScreenDepthPipeline = stereo && screen ? pipelineState.screenRef : 0, .stereoPipeline = stereo ? pipelineState.stereoRef : 0, .stereoScreenPipeline = stereo && screen ? pipelineState.stereoScreenRef : 0, + .compositeSourcePipeline = stereo && replayLayout.perspective ? pipelineState.compositeSourceRef : 0, + .stereoCompositeSourcePipeline = + stereo && replayLayout.perspective ? pipelineState.stereoCompositeSourceRef : 0, .vertRange = vertRange, .idxRange = idxRange, .uniformRange = uniformRanges.current, @@ -2560,7 +2567,6 @@ 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, @@ -2738,7 +2744,6 @@ 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); +void aurora::gx::note_composite_source_pipeline(gfx::PipelineRef pipeline) noexcept { + fifo::note_composite_source_pipeline_impl(pipeline); } diff --git a/aurora-main/lib/gx/pipeline.hpp b/aurora-main/lib/gx/pipeline.hpp index cfe2d53..d52fe92 100644 --- a/aurora-main/lib/gx/pipeline.hpp +++ b/aurora-main/lib/gx/pipeline.hpp @@ -14,11 +14,11 @@ struct DrawData { // Eye depth/stencil format siblings. Shader modules are shared with mono. gfx::PipelineRef stereoPipeline = 0; gfx::PipelineRef stereoScreenPipeline = 0; - // Constant-alpha siblings, resolved only for the draws of a composite's source pass - // (link_composite_sources) from the config remembered under configHash. - gfx::PipelineRef stereoGhostPipeline = 0; - gfx::PipelineRef stereoGhostStencilPipeline = 0; - HashType configHash = 0; + // Constant-alpha (kCompositeSourceBlend) siblings of pipeline and stereoPipeline, set only + // on the perspective draws of a pipeline an eye re-issues at a composite + // (note_composite_source_pipeline). + gfx::PipelineRef compositeSourcePipeline = 0; + gfx::PipelineRef stereoCompositeSourcePipeline = 0; gfx::Range vertRange; gfx::Range idxRange; gfx::Range uniformRange; @@ -101,9 +101,10 @@ void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncod bool requireReadyPipeline, const gfx::Range* uniformRangeOverride = nullptr, gfx::PipelineRef pipelineOverride = 0, const wgpu::Color* blendConstantOverride = nullptr); -// The constant-alpha (kCompositeSourceBlend) siblings of the pipeline recorded under configHash, -// for a stencil-less and a stencil eye target. False when the config is no longer remembered. -bool resolve_composite_source_pipelines(HashType configHash, gfx::PipelineRef& plain, gfx::PipelineRef& stencil); +// Gives the draws of `pipeline` their constant-alpha siblings from its next draw on: an eye +// re-issues them at a composite (gfx::link_composite_source). Called with the renderer lock +// the draw paths hold, as the composite is recorded. +void note_composite_source_pipeline(gfx::PipelineRef pipeline) noexcept; void queue_surface(const u8* dlStart, uint32_t dlSize, bool bigEndian) noexcept; } // namespace aurora::gx diff --git a/aurora-main/lib/gx/shader_info.hpp b/aurora-main/lib/gx/shader_info.hpp index 877d876..1d035a5 100644 --- a/aurora-main/lib/gx/shader_info.hpp +++ b/aurora-main/lib/gx/shader_info.hpp @@ -21,13 +21,10 @@ struct UniformReplayLayout { // 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. + // such quad. The pass that resolved the copy holds the ghost's own perspective draws, + // which an eye re-issues in this quad's place (gfx::link_composite_source) instead of + // stamping the desktop's flat image of the ghost over its own view. 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 c3b2fd5..e7fd397 100644 --- a/aurora-main/tests/gx_fifo_test.cpp +++ b/aurora-main/tests/gx_fifo_test.cpp @@ -5308,7 +5308,6 @@ TEST_F(GXFifoTest, NativeCompositeRecordsTheFrameSizedDepthCopyItSamples) { 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(); @@ -5316,3 +5315,85 @@ TEST_F(GXFifoTest, NativeCompositeRecordsTheFrameSizedDepthCopyItSamples) { aurora::gx::FrameInterpolationDrawIdentity{}, true); EXPECT_EQ(world.replayLayout.compositeDepthCopy, nullptr); } + +static void draw_quad_with_projection(GXProjectionType type) { + aurora::gfx::testing::use_real_vertex_format_helpers(true); + aurora::gfx::testing::use_draw_command_tracking(true); + + aurora::Mat4x4 proj{}; + if (type == GX_PERSPECTIVE) { + proj.m0[0] = 1.0f; + proj.m1[1] = 1.0f; + proj.m2[2] = -1.0f; + proj.m2[3] = -1.0f; + proj.m3[2] = -1.0f; + } else { + proj.m0[0] = 2.0f / 608.0f; + proj.m0[3] = -1.0f; + proj.m1[1] = -2.0f / 456.0f; + proj.m1[3] = 1.0f; + proj.m2[2] = -1.0f; + proj.m3[3] = 1.0f; + } + GXSetProjection(&proj, type); + GXSetViewport(0.0f, 0.0f, 608.0f, 456.0f, 0.0f, 1.0f); + GXSetScissor(0, 0, 608, 456); + aurora::Mat3x4 identity{}; + identity.m0[0] = identity.m1[1] = identity.m2[2] = 1.0f; + GXLoadPosMtxImm(&identity, GX_PNMTX0); + GXSetCurrentMtx(GX_PNMTX0); + + GXClearVtxDesc(); + GXSetVtxDesc(GX_VA_POS, GX_DIRECT); + GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0); + GXSetNumChans(0); + GXSetNumTexGens(0); + GXSetNumTevStages(1); + GXSetTevOrder(GX_TEVSTAGE0, GX_TEXCOORD_NULL, GX_TEXMAP_NULL, GX_COLOR_NULL); + GXSetTevColorIn(GX_TEVSTAGE0, GX_CC_ZERO, GX_CC_ZERO, GX_CC_ZERO, GX_CC_C0); + GXSetTevColorOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + GXSetTevAlphaIn(GX_TEVSTAGE0, GX_CA_ZERO, GX_CA_ZERO, GX_CA_ZERO, GX_CA_A0); + GXSetTevAlphaOp(GX_TEVSTAGE0, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE, GX_TEVPREV); + + const float corners[4][2]{{-1.0f, -1.0f}, {1.0f, -1.0f}, {1.0f, 1.0f}, {-1.0f, 1.0f}}; + GXBegin(GX_QUADS, GX_VTXFMT0, 4); + for (const auto& corner : corners) { + GXPosition3f32(corner[0], corner[1], -5.0f); + } + GXEnd(); +} + +TEST_F(GXFifoTest, CompositeSourcePipelineGetsConstantAlphaSiblingsFromItsNextDraw) { + // An eye re-issues MKW's ghost kart draws at the composite with constant-alpha siblings of + // their pipelines. Only a pipeline an eye asked for gets them, from its next draw on, and + // only in perspective draws. + draw_quad_with_projection(GX_PERSPECTIVE); + decode_fifo(flush_and_capture()); + const auto* first = aurora::gfx::get_last_draw_command(); + ASSERT_NE(first, nullptr); + ASSERT_TRUE(first->uniformReplayLayout.perspective); + ASSERT_NE(first->pipeline, 0u); + EXPECT_EQ(first->compositeSourcePipeline, 0u); + EXPECT_EQ(first->stereoCompositeSourcePipeline, 0u); + const auto pipeline = first->pipeline; + + aurora::gx::note_composite_source_pipeline(pipeline); + draw_quad_with_projection(GX_PERSPECTIVE); + decode_fifo(flush_and_capture()); + const auto* next = aurora::gfx::get_last_draw_command(); + ASSERT_NE(next, nullptr); + ASSERT_EQ(next->pipeline, pipeline); + EXPECT_NE(next->compositeSourcePipeline, 0u); + EXPECT_NE(next->stereoCompositeSourcePipeline, 0u); + EXPECT_NE(next->compositeSourcePipeline, next->pipeline); + EXPECT_NE(next->stereoCompositeSourcePipeline, next->stereoPipeline); + EXPECT_NE(next->compositeSourcePipeline, next->stereoCompositeSourcePipeline); + + draw_quad_with_projection(GX_ORTHOGRAPHIC); + decode_fifo(flush_and_capture()); + const auto* flat = aurora::gfx::get_last_draw_command(); + ASSERT_NE(flat, nullptr); + ASSERT_EQ(flat->pipeline, pipeline); + EXPECT_EQ(flat->compositeSourcePipeline, 0u); + EXPECT_EQ(flat->stereoCompositeSourcePipeline, 0u); +} diff --git a/aurora-main/tests/gx_test_stubs.cpp b/aurora-main/tests/gx_test_stubs.cpp index 2da7beb..0ef5c76 100644 --- a/aurora-main/tests/gx_test_stubs.cpp +++ b/aurora-main/tests/gx_test_stubs.cpp @@ -357,7 +357,8 @@ void push_draw_command(clear::DrawData data) { } template <> PipelineRef pipeline_ref(const gx::PipelineConfig& config) { - return 0; + // Distinct configs get distinct refs, as the real cache's config hash does. + return xxh3_hash(config, static_cast(ShaderType::GX)); } template <> void push_draw_command(gx::DrawData data) {