mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 00:00:17 +02:00
Implement native composite source handling for Mario Kart Wii ghost kart effect
This commit is contained in:
1 parent
7f9de4c702
commit
f2f7c5007a
10 files changed
+370
-59
No files matched your search
@@ -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
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user