mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 04:04:18 +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
|
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
|
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
|
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
|
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.
|
ended up; its original viewport is folded into the projection instead.
|
||||||
|
|
||||||
|
|||||||
+173
-46
@@ -208,6 +208,9 @@ struct RenderPass {
|
|||||||
bool postCopyClear = false;
|
bool postCopyClear = false;
|
||||||
bool snapshotColorResolveSource = false;
|
bool snapshotColorResolveSource = false;
|
||||||
bool efbTarget = 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;
|
std::vector<tex_palette_conv::ConvRequest> paletteConvs;
|
||||||
};
|
};
|
||||||
static std::vector<RenderPass> g_renderPasses;
|
static std::vector<RenderPass> g_renderPasses;
|
||||||
@@ -1647,6 +1650,66 @@ static bool prepare_stereo_replay_uniforms(const StereoReplayFrame& stereoFrame,
|
|||||||
return true;
|
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,
|
static bool end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame,
|
||||||
const StereoReplayFrame* stereoFrame = nullptr) {
|
const StereoReplayFrame* stereoFrame = nullptr) {
|
||||||
ZoneScoped;
|
ZoneScoped;
|
||||||
@@ -1666,6 +1729,9 @@ static bool end_batch_impl(const wgpu::CommandEncoder& cmd, bool advanceFrame,
|
|||||||
const bool stereoPrepared = (stereoFrame == nullptr && !captureStereo) ||
|
const bool stereoPrepared = (stereoFrame == nullptr && !captureStereo) ||
|
||||||
prepare_stereo_replay_uniforms(stereoFrame != nullptr ? *stereoFrame : placeholder,
|
prepare_stereo_replay_uniforms(stereoFrame != nullptr ? *stereoFrame : placeholder,
|
||||||
captureStereo ? &g_pendingLateStereo : nullptr);
|
captureStereo ? &g_pendingLateStereo : nullptr);
|
||||||
|
if (stereoPrepared && (stereoFrame != nullptr || captureStereo)) {
|
||||||
|
link_composite_sources();
|
||||||
|
}
|
||||||
if (captureStereo && stereoPrepared) {
|
if (captureStereo && stereoPrepared) {
|
||||||
g_pendingLateStereo.generation = g_replayBufferGeneration.load(std::memory_order_acquire);
|
g_pendingLateStereo.generation = g_replayBufferGeneration.load(std::memory_order_acquire);
|
||||||
} else {
|
} else {
|
||||||
@@ -2581,6 +2647,102 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
|
|||||||
recordedViewport.znear, recordedViewport.zfar);
|
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) {
|
for (const auto& cmd : renderPasses[idx].commands) {
|
||||||
#ifdef AURORA_GFX_DEBUG_GROUPS
|
#ifdef AURORA_GFX_DEBUG_GROUPS
|
||||||
{
|
{
|
||||||
@@ -2604,35 +2766,7 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
|
|||||||
case CommandType::SetViewport: {
|
case CommandType::SetViewport: {
|
||||||
const auto& vp = cmd.data.setViewport;
|
const auto& vp = cmd.data.setViewport;
|
||||||
sourceViewport = {vp.left, vp.top, vp.width, vp.height};
|
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
|
recordedViewport = map_viewport(vp);
|
||||||
// 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,
|
|
||||||
};
|
|
||||||
hudScreenViewport = false;
|
hudScreenViewport = false;
|
||||||
viewportStateKnown = true;
|
viewportStateKnown = true;
|
||||||
apply_viewport(false);
|
apply_viewport(false);
|
||||||
@@ -2640,23 +2774,7 @@ static void render_pass_impl(const wgpu::RenderPassEncoder& pass, const std::vec
|
|||||||
case CommandType::SetScissor: {
|
case CommandType::SetScissor: {
|
||||||
const auto& sc = cmd.data.setScissor;
|
const auto& sc = cmd.data.setScissor;
|
||||||
sourceScissor = {float(sc.x), float(sc.y), float(sc.width), float(sc.height)};
|
sourceScissor = {float(sc.x), float(sc.y), float(sc.width), float(sc.height)};
|
||||||
const auto sourceLeft = std::clamp(sc.x, sourceRegionLeft, sourceRegionRight);
|
recordedScissor = map_scissor(sc);
|
||||||
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};
|
|
||||||
hudScreenScissor = false;
|
hudScreenScissor = false;
|
||||||
scissorStateKnown = true;
|
scissorStateKnown = true;
|
||||||
apply_scissor(recordedScissor);
|
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))) {
|
!stereo_replay::subviews_overlap(sourceScissor, playerRegion))) {
|
||||||
break;
|
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;
|
const gfx::Range* uniformOverride = nullptr;
|
||||||
// Only a 2D draw the virtual screen actually claimed carries a stereo
|
// Only a 2D draw the virtual screen actually claimed carries a stereo
|
||||||
// uniform range without being perspective.
|
// uniform range without being perspective.
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
|
|
||||||
namespace aurora::gfx::stereo_replay {
|
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 {
|
struct SubviewRect {
|
||||||
float left = 0.f;
|
float left = 0.f;
|
||||||
float top = 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
|
// 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.
|
// draws only read the refs: no extra config population/hashing on the Quest CPU.
|
||||||
// Shader modules are shared by the depth-format variants.
|
// 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) {
|
static void resolve_replay_pipelines(const CachedPipelineState& state, bool screen) {
|
||||||
if (state.stereoRef && (!screen || state.stereoScreenRef)) return;
|
if (state.stereoRef && (!screen || state.stereoScreenRef)) return;
|
||||||
PipelineConfig config = *state.config;
|
PipelineConfig config = *state.config;
|
||||||
config.stereoStencil = 1;
|
config.stereoStencil = kStereoStencilFormat;
|
||||||
if (!state.stereoRef) state.stereoRef = gfx::pipeline_ref(config);
|
if (!state.stereoRef) state.stereoRef = gfx::pipeline_ref(config);
|
||||||
if (screen && !state.stereoScreenRef) {
|
if (screen && !state.stereoScreenRef) {
|
||||||
config.shaderConfig.exactScreenDepth = 1;
|
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 stereo = aurora::stereo_frame_provider_active();
|
||||||
const bool screen = !replayLayout.perspective && !replayLayout.nativeEfbEffect;
|
const bool screen = !replayLayout.perspective && !replayLayout.nativeEfbEffect;
|
||||||
if (stereo) resolve_replay_pipelines(pipelineState, screen);
|
if (stereo) resolve_replay_pipelines(pipelineState, screen);
|
||||||
|
if (stereo && replayLayout.perspective) remember_composite_source_config(pipelineState);
|
||||||
s_lastDrawRecordedInterpolation = interpolationIdentityActive;
|
s_lastDrawRecordedInterpolation = interpolationIdentityActive;
|
||||||
|
|
||||||
uint32_t instanceCount = 1;
|
uint32_t instanceCount = 1;
|
||||||
@@ -2521,6 +2560,7 @@ static void handle_draw_unmerged(GXPrimitive prim, GXVtxFmt fmt, u16 vtxCount, g
|
|||||||
.stereoUniformRanges = {},
|
.stereoUniformRanges = {},
|
||||||
.previousUniformRange = uniformRanges.previous,
|
.previousUniformRange = uniformRanges.previous,
|
||||||
.uniformReplayLayout = uniformRanges.replayLayout,
|
.uniformReplayLayout = uniformRanges.replayLayout,
|
||||||
|
.configHash = pipelineState.configHash,
|
||||||
.vtxCount = vtxCount,
|
.vtxCount = vtxCount,
|
||||||
.indexCount = numIndices,
|
.indexCount = numIndices,
|
||||||
.instanceCount = instanceCount,
|
.instanceCount = instanceCount,
|
||||||
@@ -2697,3 +2737,8 @@ bool handle_aurora(const u8* data, u32& pos, u32 size, bool bigEndian) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace aurora::gx::fifo
|
} // 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::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef<wgpu::VertexBufferLayout> vtxBuffers,
|
||||||
wgpu::ShaderModule shader, const char* label) noexcept {
|
wgpu::ShaderModule shader, const char* label) noexcept {
|
||||||
ZoneScoped;
|
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{
|
const wgpu::StencilFaceState stencil{
|
||||||
.compare = maskCockpit ? wgpu::CompareFunction::Equal : wgpu::CompareFunction::Always,
|
.compare = maskCockpit ? wgpu::CompareFunction::Equal : wgpu::CompareFunction::Always,
|
||||||
};
|
};
|
||||||
const wgpu::DepthStencilState depthStencil{
|
const wgpu::DepthStencilState depthStencil{
|
||||||
.format = config.stereoStencil ? wgpu::TextureFormat::Depth24PlusStencil8 : g_graphicsConfig.depthFormat,
|
.format = stencilTarget ? wgpu::TextureFormat::Depth24PlusStencil8 : g_graphicsConfig.depthFormat,
|
||||||
.depthWriteEnabled = config.depthUpdate,
|
.depthWriteEnabled = config.depthUpdate,
|
||||||
.depthCompare = config.depthCompare ? to_compare_function(config.depthFunc) : wgpu::CompareFunction::Always,
|
.depthCompare = config.depthCompare ? to_compare_function(config.depthFunc) : wgpu::CompareFunction::Always,
|
||||||
.stencilFront = stencil,
|
.stencilFront = stencil,
|
||||||
@@ -1622,8 +1623,22 @@ wgpu::RenderPipeline build_pipeline(const PipelineConfig& config, ArrayRef<wgpu:
|
|||||||
.stencilReadMask = 1,
|
.stencilReadMask = 1,
|
||||||
.stencilWriteMask = 0,
|
.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);
|
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{
|
const std::array colorTargets{wgpu::ColorTargetState{
|
||||||
.format = g_graphicsConfig.surfaceConfiguration.format,
|
.format = g_graphicsConfig.surfaceConfiguration.format,
|
||||||
.blend = &blendState,
|
.blend = &blendState,
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ void clear_shader_module_cache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncodeState& state,
|
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;
|
const gfx::PipelineRef pipeline = pipelineOverride != 0 ? pipelineOverride : data.pipeline;
|
||||||
if (!gfx::bind_pipeline(pipeline, pass, state.currentPipeline, requireReadyPipeline)) {
|
if (!gfx::bind_pipeline(pipeline, pass, state.currentPipeline, requireReadyPipeline)) {
|
||||||
return;
|
return;
|
||||||
@@ -83,7 +84,9 @@ void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncod
|
|||||||
wgpuRenderPassEncoderSetBindGroup(pass.Get(), 2, data.bindGroups.resolvedTextureBindGroup, 0, nullptr);
|
wgpuRenderPassEncoderSetBindGroup(pass.Get(), 2, data.bindGroups.resolvedTextureBindGroup, 0, nullptr);
|
||||||
state.boundTextureBindGroup = data.bindGroups.resolvedTextureBindGroup;
|
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};
|
const wgpu::Color color{0.f, 0.f, 0.f, data.dstAlpha / 255.f};
|
||||||
pass.SetBlendConstant(&color);
|
pass.SetBlendConstant(&color);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ struct DrawData {
|
|||||||
// Eye depth/stencil format siblings. Shader modules are shared with mono.
|
// Eye depth/stencil format siblings. Shader modules are shared with mono.
|
||||||
gfx::PipelineRef stereoPipeline = 0;
|
gfx::PipelineRef stereoPipeline = 0;
|
||||||
gfx::PipelineRef stereoScreenPipeline = 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 vertRange;
|
||||||
gfx::Range idxRange;
|
gfx::Range idxRange;
|
||||||
gfx::Range uniformRange;
|
gfx::Range uniformRange;
|
||||||
@@ -34,6 +39,11 @@ struct DrawData {
|
|||||||
|
|
||||||
constexpr uint32_t GXPipelineConfigVersion = 21;
|
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,
|
constexpr GXFogType effective_pipeline_fog_type(GXFogType fogType, GXZTexOp zTextureOp, bool zCompLocBeforeTex,
|
||||||
GXBlendMode blendMode, GXLogicOp logicOp) noexcept {
|
GXBlendMode blendMode, GXLogicOp logicOp) noexcept {
|
||||||
const bool usesLateZTexture = zTextureOp != GX_ZT_DISABLE && !zCompLocBeforeTex;
|
const bool usesLateZTexture = zTextureOp != GX_ZT_DISABLE && !zCompLocBeforeTex;
|
||||||
@@ -44,6 +54,8 @@ constexpr GXFogType effective_pipeline_fog_type(GXFogType fogType, GXZTexOp zTex
|
|||||||
struct PipelineConfig {
|
struct PipelineConfig {
|
||||||
uint32_t version = GXPipelineConfigVersion;
|
uint32_t version = GXPipelineConfigVersion;
|
||||||
uint32_t msaaSamples = 1;
|
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;
|
uint32_t stereoStencil = 0;
|
||||||
ShaderConfig shaderConfig;
|
ShaderConfig shaderConfig;
|
||||||
GXCompare depthFunc;
|
GXCompare depthFunc;
|
||||||
@@ -65,7 +77,7 @@ inline bool valid_pipeline_config(const PipelineConfig& config) noexcept {
|
|||||||
};
|
};
|
||||||
const bool validSamples =
|
const bool validSamples =
|
||||||
config.msaaSamples == 1 || config.msaaSamples == 2 || config.msaaSamples == 4 || config.msaaSamples == 8;
|
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.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.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);
|
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,
|
void render(const DrawData& data, const wgpu::RenderPassEncoder& pass, DrawEncodeState& state,
|
||||||
bool requireReadyPipeline, const gfx::Range* uniformRangeOverride = nullptr,
|
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;
|
void queue_surface(const u8* dlStart, uint32_t dlSize, bool bigEndian) noexcept;
|
||||||
} // namespace aurora::gx
|
} // 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.
|
// eligible for the virtual screen even when small and alpha blended.
|
||||||
bool samplesRecentEfbCopy = false;
|
bool samplesRecentEfbCopy = false;
|
||||||
bool samplesReducedEfbCopy = false;
|
bool samplesReducedEfbCopy = false;
|
||||||
|
const gfx::TextureRef* compositeDepthCopy = nullptr;
|
||||||
const u32 currentFrame = gfx::current_frame();
|
const u32 currentFrame = gfx::current_frame();
|
||||||
for (int i = 0; i < info.sampledTextures.size(); ++i) {
|
for (int i = 0; i < info.sampledTextures.size(); ++i) {
|
||||||
if (!info.sampledTextures.test(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);
|
// CHECK(tex, "unbound texture {}", i);
|
||||||
if (tex.ref && tex.ref->is_recent_efb_copy(currentFrame)) {
|
if (tex.ref && tex.ref->is_recent_efb_copy(currentFrame)) {
|
||||||
samplesRecentEfbCopy = true;
|
samplesRecentEfbCopy = true;
|
||||||
samplesReducedEfbCopy =
|
const bool reduced = tex.texObj.width() * 2 <= kEfbWidth || tex.texObj.height() * 2 <= kEfbHeight;
|
||||||
samplesReducedEfbCopy || 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));
|
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.width < static_cast<float>(fbWidth) * 0.9f ||
|
||||||
drawViewport.height < static_cast<float>(fbHeight) * 0.9f);
|
drawViewport.height < static_cast<float>(fbHeight) * 0.9f);
|
||||||
|
|
||||||
|
const bool nativeEfbEffect =
|
||||||
|
!perspective && (readsDstAlpha || (samplesRecentEfbCopy && (samplesReducedEfbCopy || blends || offscreenViewport)));
|
||||||
const UniformReplayLayout replayLayout{
|
const UniformReplayLayout replayLayout{
|
||||||
.projectionOffset = static_cast<uint32_t>(projectionOffset),
|
.projectionOffset = static_cast<uint32_t>(projectionOffset),
|
||||||
.positionOffset = static_cast<uint32_t>(positionOffset),
|
.positionOffset = static_cast<uint32_t>(positionOffset),
|
||||||
@@ -787,8 +793,8 @@ UniformRanges build_uniform(const ShaderInfo& info, u32 vtxStart, const BindGrou
|
|||||||
.normalMatrixCount = layout.nrmCount,
|
.normalMatrixCount = layout.nrmCount,
|
||||||
.perspective = perspective,
|
.perspective = perspective,
|
||||||
.indexedMatrices = info.indexAttr.test(GX_VA_PNMTXIDX),
|
.indexedMatrices = info.indexAttr.test(GX_VA_PNMTXIDX),
|
||||||
.nativeEfbEffect = !perspective && (readsDstAlpha || (samplesRecentEfbCopy &&
|
.nativeEfbEffect = nativeEfbEffect,
|
||||||
(samplesReducedEfbCopy || blends || offscreenViewport))),
|
.compositeDepthCopy = nativeEfbEffect ? compositeDepthCopy : nullptr,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!perspective || !frame_interpolation_active()) {
|
if (!perspective || !frame_interpolation_active()) {
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ struct UniformReplayLayout {
|
|||||||
// rest of the native post-processing chain. It belongs to the rendered image,
|
// 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.
|
// not to the game's 2D layer, so it must stay where the game aimed it.
|
||||||
bool nativeEfbEffect = false;
|
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 {
|
struct UniformRanges {
|
||||||
|
|||||||
@@ -5246,3 +5246,73 @@ TEST_F(GXFifoTest, OffscreenViewportPassOverFreshCopyIsNativeEfbEffect) {
|
|||||||
aurora::gx::FrameInterpolationDrawIdentity{}, false);
|
aurora::gx::FrameInterpolationDrawIdentity{}, false);
|
||||||
EXPECT_FALSE(frameLayout.replayLayout.nativeEfbEffect);
|
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