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

+15 -1
View File
@@ -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.
+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);
}