Implement native composite source handling for Mario Kart Wii ghost kart effect

This commit is contained in:
iChris4 committed 2026-09-26 05:17:49 +02:00
1 parent 7f9de4c702
commit f2f7c5007a
10 files changed
+370 -59

No files matched your search

+173 -46
View File
@@ -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<tex_palette_conv::ConvRequest> paletteConvs;
};
static std::vector<RenderPass> 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<int32_t>(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<float>(sourceRegionLeft)) * scaleX,
.top = (vp.top - static_cast<float>(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<uint32_t>(
std::clamp(static_cast<int32_t>(std::floor(static_cast<float>(sourceLeft - sourceRegionLeft) * scaleX)), 0,
static_cast<int32_t>(targetSize.width)));
const auto top = static_cast<uint32_t>(
std::clamp(static_cast<int32_t>(std::floor(static_cast<float>(sourceTop - sourceRegionTop) * scaleY)), 0,
static_cast<int32_t>(targetSize.height)));
const auto right = static_cast<uint32_t>(
std::clamp(static_cast<int32_t>(std::ceil(static_cast<float>(sourceRight - sourceRegionLeft) * scaleX)),
static_cast<int32_t>(left), static_cast<int32_t>(targetSize.width)));
const auto bottom = static_cast<uint32_t>(
std::clamp(static_cast<int32_t>(std::ceil(static_cast<float>(sourceBottom - sourceRegionTop) * scaleY)),
static_cast<int32_t>(top), static_cast<int32_t>(targetSize.height)));
return std::array<uint32_t, 4>{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<size_t>(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<float>(sourceRegionLeft)) * scaleX,
.top = (vp.top - static_cast<float>(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<uint32_t>(
std::clamp(static_cast<int32_t>(std::floor(static_cast<float>(sourceLeft - sourceRegionLeft) * scaleX)), 0,
static_cast<int32_t>(targetSize.width)));
const auto top = static_cast<uint32_t>(
std::clamp(static_cast<int32_t>(std::floor(static_cast<float>(sourceTop - sourceRegionTop) * scaleY)), 0,
static_cast<int32_t>(targetSize.height)));
const auto right = static_cast<uint32_t>(
std::clamp(static_cast<int32_t>(std::ceil(static_cast<float>(sourceRight - sourceRegionLeft) * scaleX)),
static_cast<int32_t>(left), static_cast<int32_t>(targetSize.width)));
const auto bottom = static_cast<uint32_t>(
std::clamp(static_cast<int32_t>(std::ceil(static_cast<float>(sourceBottom - sourceRegionTop) * scaleY)),
static_cast<int32_t>(top), static_cast<int32_t>(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<size_t>(draw.gx.uniformReplayLayout.compositeSourcePass) < renderPasses.size()) {
replay_composite_source(renderPasses[static_cast<size_t>(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.
+5
View File
@@ -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;
+46 -1
View File
@@ -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<HashType, CompositeSourceConfig> 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);
}
+18 -3
View File
@@ -1609,12 +1609,13 @@ static inline wgpu::PrimitiveState to_primitive_state(GXCullMode gx_cullMode) {
wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef<wgpu::VertexBufferLayout> 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, ArrayRef<wgpu:
.stencilReadMask = 1,
.stencilWriteMask = 0,
};
const auto blendState = to_blend_state(config.blendMode, config.blendFacSrc, config.blendFacDst, config.blendOp,
auto blendState = to_blend_state(config.blendMode, config.blendFacSrc, config.blendFacDst, config.blendOp,
config.pixelFmt, config.dstAlpha);
if ((config.stereoStencil & kCompositeSourceBlend) != 0) {
// A composite's source draw re-issued by an eye: opaque geometry laid over the world at
// the blend constant's alpha, the alpha the game's own composite quad would have used.
blendState.color = wgpu::BlendComponent{
.operation = wgpu::BlendOperation::Add,
.srcFactor = wgpu::BlendFactor::Constant,
.dstFactor = wgpu::BlendFactor::OneMinusConstant,
};
blendState.alpha = wgpu::BlendComponent{
.operation = wgpu::BlendOperation::Add,
.srcFactor = wgpu::BlendFactor::One,
.dstFactor = wgpu::BlendFactor::Zero,
};
}
const std::array colorTargets{wgpu::ColorTargetState{
.format = g_graphicsConfig.surfaceConfiguration.format,
.blend = &blendState,
+5 -2
View File
@@ -66,7 +66,8 @@ void clear_shader_module_cache() {
}
void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncodeState& state,
bool requireReadyPipeline, const gfx::Range* uniformRangeOverride, gfx::PipelineRef pipelineOverride) {
bool requireReadyPipeline, const gfx::Range* uniformRangeOverride, gfx::PipelineRef pipelineOverride,
const wgpu::Color* blendConstantOverride) {
const gfx::PipelineRef pipeline = pipelineOverride != 0 ? pipelineOverride : data.pipeline;
if (!gfx::bind_pipeline(pipeline, pass, state.currentPipeline, requireReadyPipeline)) {
return;
@@ -83,7 +84,9 @@ void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncod
wgpuRenderPassEncoderSetBindGroup(pass.Get(), 2, data.bindGroups.resolvedTextureBindGroup, 0, nullptr);
state.boundTextureBindGroup = data.bindGroups.resolvedTextureBindGroup;
}
if (data.dstAlpha != UINT32_MAX) {
if (blendConstantOverride != nullptr) {
pass.SetBlendConstant(blendConstantOverride);
} else if (data.dstAlpha != UINT32_MAX) {
const wgpu::Color color{0.f, 0.f, 0.f, data.dstAlpha / 255.f};
pass.SetBlendConstant(&color);
}
+18 -2
View File
@@ -14,6 +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;
gfx::Range vertRange;
gfx::Range idxRange;
gfx::Range uniformRange;
@@ -34,6 +39,11 @@ struct DrawData {
constexpr uint32_t GXPipelineConfigVersion = 21;
// PipelineConfig::stereoStencil: the eye's Depth24PlusStencil8 target, and the constant-alpha
// blend an eye uses to re-issue a composite's source draws (see UniformReplayLayout).
constexpr uint32_t kStereoStencilFormat = 1u;
constexpr uint32_t kCompositeSourceBlend = 2u;
constexpr GXFogType effective_pipeline_fog_type(GXFogType fogType, GXZTexOp zTextureOp, bool zCompLocBeforeTex,
GXBlendMode blendMode, GXLogicOp logicOp) noexcept {
const bool usesLateZTexture = zTextureOp != GX_ZT_DISABLE && !zCompLocBeforeTex;
@@ -44,6 +54,8 @@ constexpr GXFogType effective_pipeline_fog_type(GXFogType fogType, GXZTexOp zTex
struct PipelineConfig {
uint32_t version = GXPipelineConfigVersion;
uint32_t msaaSamples = 1;
// Bits of kStereoStencilFormat and kCompositeSourceBlend. One field, so the on-disk
// pipeline recipes recorded before the second bit existed stay valid.
uint32_t stereoStencil = 0;
ShaderConfig shaderConfig;
GXCompare depthFunc;
@@ -65,7 +77,7 @@ inline bool valid_pipeline_config(const PipelineConfig& config) noexcept {
};
const bool validSamples =
config.msaaSamples == 1 || config.msaaSamples == 2 || config.msaaSamples == 4 || config.msaaSamples == 8;
return config.version == GXPipelineConfigVersion && validSamples && config.stereoStencil <= 1 && in_range(config.depthFunc, GX_ALWAYS) &&
return config.version == GXPipelineConfigVersion && validSamples && config.stereoStencil <= (kStereoStencilFormat | kCompositeSourceBlend) && in_range(config.depthFunc, GX_ALWAYS) &&
in_range(config.cullMode, GX_CULL_ALL) && in_range(config.blendMode, GX_BM_SUBTRACT) &&
in_range(config.blendFacSrc, GX_BL_INVDSTALPHA) && in_range(config.blendFacDst, GX_BL_INVDSTALPHA) &&
in_range(config.blendOp, GX_LO_SET) && in_range(config.pixelFmt, GX_PF_YUV420);
@@ -87,7 +99,11 @@ struct DrawEncodeState {
void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncodeState& state,
bool requireReadyPipeline, const gfx::Range* uniformRangeOverride = nullptr,
gfx::PipelineRef pipelineOverride = 0);
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);
void queue_surface(const u8* dlStart, uint32_t dlSize, bool bigEndian) noexcept;
} // namespace aurora::gx
+10 -4
View File
@@ -743,6 +743,7 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
// eligible for the virtual screen even when small and alpha blended.
bool samplesRecentEfbCopy = false;
bool samplesReducedEfbCopy = false;
const gfx::TextureRef* compositeDepthCopy = nullptr;
const u32 currentFrame = gfx::current_frame();
for (int i = 0; i < info.sampledTextures.size(); ++i) {
if (!info.sampledTextures.test(i)) {
@@ -752,8 +753,11 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
// CHECK(tex, "unbound texture {}", i);
if (tex.ref && tex.ref->is_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<float>(fbWidth) * 0.9f ||
drawViewport.height < static_cast<float>(fbHeight) * 0.9f);
const bool nativeEfbEffect =
!perspective && (readsDstAlpha || (samplesRecentEfbCopy && (samplesReducedEfbCopy || blends || offscreenViewport)));
const UniformReplayLayout replayLayout{
.projectionOffset = static_cast<uint32_t>(projectionOffset),
.positionOffset = static_cast<uint32_t>(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()) {
+10
View File
@@ -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 {
+70
View File
@@ -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<u8, 608 * 456 * 4> colour{};
static std::array<u8, 608 * 456 * 4> 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);
}