Files
thejerf--baijiSteamVR/Source/Core/VideoCommon/BPFunctions.cpp
T

555 lines
21 KiB
C++

// Copyright 2009 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VideoCommon/BPFunctions.h"
#include <algorithm>
#include <cmath>
#include <string_view>
#include "Common/Assert.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
#include "Common/SmallVector.h"
#include "Core/System.h"
#include "VideoCommon/AbstractGfx.h"
#include "VideoCommon/BPMemory.h"
#include "VideoCommon/EFBInterface.h"
#include "VideoCommon/FramebufferManager.h"
#include "VideoCommon/OpenXROpcodeReplay.h"
#include "VideoCommon/VertexManagerBase.h"
#include "VideoCommon/VertexShaderManager.h"
#include "VideoCommon/VideoCommon.h"
#include "VideoCommon/ShaderHunter.h"
#include "VideoCommon/VideoConfig.h"
#include "VideoCommon/XFMemory.h"
namespace BPFunctions
{
// ----------------------------------------------
// State translation lookup tables
// Reference: Yet Another GameCube Documentation
// ----------------------------------------------
void FlushPipeline()
{
g_vertex_manager->Flush();
}
void SetGenerationMode()
{
g_vertex_manager->SetRasterizationStateChanged();
}
int ScissorRect::GetArea() const
{
return rect.GetWidth() * rect.GetHeight();
}
int ScissorResult::GetViewportArea(const ScissorRect& rect) const
{
int x0 = std::clamp<int>(rect.rect.left + rect.x_off, viewport_left, viewport_right);
int x1 = std::clamp<int>(rect.rect.right + rect.x_off, viewport_left, viewport_right);
int y0 = std::clamp<int>(rect.rect.top + rect.y_off, viewport_top, viewport_bottom);
int y1 = std::clamp<int>(rect.rect.bottom + rect.y_off, viewport_top, viewport_bottom);
return (x1 - x0) * (y1 - y0);
}
// Compare so that a sorted collection of rectangles has the best one last, so that if they're drawn
// in order, the best one is the one that is drawn last (and thus over the rest).
// The exact iteration order on hardware hasn't been tested, but silly things can happen where a
// polygon can intersect with itself; this only applies outside of the viewport region (in areas
// that would normally be affected by clipping). No game is known to care about this.
bool ScissorResult::IsWorse(const ScissorRect& lhs, const ScissorRect& rhs) const
{
// First, penalize any rect that is not in the viewport
int lhs_area = GetViewportArea(lhs);
int rhs_area = GetViewportArea(rhs);
if (lhs_area != rhs_area)
return lhs_area < rhs_area;
// Now compare on total areas, without regard for the viewport
return lhs.GetArea() < rhs.GetArea();
}
namespace
{
using RangeList = Common::SmallVector<ScissorRange, 9>;
static RangeList ComputeScissorRanges(int start, int end, int offset, int efb_dim)
{
RangeList ranges;
for (int extra_off = -4096; extra_off <= 4096; extra_off += 1024)
{
int new_off = offset + extra_off;
int new_start = std::clamp(start - new_off, 0, efb_dim);
int new_end = std::clamp(end - new_off + 1, 0, efb_dim);
if (new_start < new_end)
{
ranges.emplace_back(new_off, new_start, new_end);
}
}
return ranges;
}
} // namespace
ScissorResult::ScissorResult(ScissorPos scissor_top_left, ScissorPos scissor_bottom_right,
ScissorOffset scissor_offset, const Viewport& viewport)
: ScissorResult(scissor_top_left, scissor_bottom_right, scissor_offset,
std::minmax(viewport.xOrig - viewport.wd, viewport.xOrig + viewport.wd),
std::minmax(viewport.yOrig - viewport.ht, viewport.yOrig + viewport.ht))
{
}
ScissorResult::ScissorResult(ScissorPos scissor_top_left, ScissorPos scissor_bottom_right,
ScissorOffset scissor_offset, std::pair<float, float> viewport_x,
std::pair<float, float> viewport_y)
: scissor_tl{.hex = scissor_top_left.hex}, scissor_br{.hex = scissor_bottom_right.hex},
scissor_off{.hex = scissor_offset.hex}, viewport_left(viewport_x.first),
viewport_right(viewport_x.second), viewport_top(viewport_y.first),
viewport_bottom(viewport_y.second)
{
// Range is [left, right] and [top, bottom] (closed intervals)
const int left = scissor_tl.x;
const int right = scissor_br.x;
const int top = scissor_tl.y;
const int bottom = scissor_br.y;
// When left > right or top > bottom, nothing renders (even with wrapping from the offsets)
if (left > right || top > bottom)
return;
// Note that both the offsets and the coordinates have 342 added to them internally by GX
// functions (for the offsets, this is before they are divided by 2/right shifted). This code
// could undo both sets of offsets, but it doesn't need to since they cancel out when subtracting
// (and those offsets actually matter for the left > right and top > bottom checks).
const int x_off = scissor_off.x << 1;
const int y_off = scissor_off.y << 1;
RangeList x_ranges = ComputeScissorRanges(left, right, x_off, EFB_WIDTH);
RangeList y_ranges = ComputeScissorRanges(top, bottom, y_off, EFB_HEIGHT);
rectangles.reserve(x_ranges.size() * y_ranges.size());
// Now we need to form actual rectangles from the x and y ranges,
// which is a simple Cartesian product of x_ranges_clamped and y_ranges_clamped.
// Each rectangle is also a Cartesian product of x_range and y_range, with
// the rectangles being half-open (of the form [x0, x1) X [y0, y1)).
for (const auto& x_range : x_ranges)
{
DEBUG_ASSERT(x_range.start < x_range.end);
DEBUG_ASSERT(static_cast<u32>(x_range.end) <= EFB_WIDTH);
for (const auto& y_range : y_ranges)
{
DEBUG_ASSERT(y_range.start < y_range.end);
DEBUG_ASSERT(static_cast<u32>(y_range.end) <= EFB_HEIGHT);
rectangles.emplace_back(x_range, y_range);
}
}
auto cmp = [&](const ScissorRect& lhs, const ScissorRect& rhs) { return IsWorse(lhs, rhs); };
std::ranges::sort(rectangles, cmp);
}
ScissorRect ScissorResult::Best() const
{
// For now, simply choose the best rectangle (see ScissorResult::IsWorse).
// This does mean we calculate all rectangles and only choose one, which is not optimal, but this
// is called infrequently. Eventually, all backends will support multiple scissor rects.
if (!rectangles.empty())
{
return rectangles.back();
}
else
{
// But if we have no rectangles, use a bogus one that's out of bounds.
// Ideally, all backends will support multiple scissor rects, in which case this won't be
// needed.
return ScissorRect(ScissorRange{0, 1000, 1001}, ScissorRange{0, 1000, 1001});
}
}
ScissorResult ComputeScissorRects(ScissorPos scissor_top_left, ScissorPos scissor_bottom_right,
ScissorOffset scissor_offset, const Viewport& viewport)
{
return ScissorResult{scissor_top_left, scissor_bottom_right, scissor_offset, viewport};
}
void SetScissorAndViewport(FramebufferManager* frame_buffer_manager, ScissorPos scissor_top_left,
ScissorPos scissor_bottom_right, ScissorOffset scissor_offset,
Viewport viewport)
{
// VR: Expand scissor and viewport to full active area for perspective draws.
// Games like Metroid Prime 3 use a restricted scissor rect during cinematics to create
// letterbox bars (e.g. Y=33-414 out of 448). Since the bars are just undrawn EFB clear
// color (not geometry), Shader Hunter can't find them. In VR, the restricted scissor also
// clips the GS-reprojected VR geometry. Fix: expand to full range for perspective draws.
if (g_ActiveConfig.stereo_mode == StereoMode::OpenXR &&
g_ActiveConfig.vr_remove_bars &&
xfmem.projection.type == ProjectionType::Perspective)
{
const int y_off = scissor_offset.y << 1;
const int efb_top = static_cast<int>(scissor_top_left.y) - y_off;
const int efb_bot = static_cast<int>(scissor_bottom_right.y) - y_off;
// Derive the game's active height from the viewport center: the viewport yOrig sits at
// the center of the active area, so active_height = 2 * center_y.
// This avoids using EFB_HEIGHT (528) when the game only uses 448 active lines.
const float center_y = viewport.yOrig - static_cast<float>(y_off);
const int active_height = static_cast<int>(center_y * 2.0f);
// Expand if the scissor Y doesn't cover the full active area (any trimming at all)
if (active_height > 0 && (efb_top > 0 || efb_bot < active_height - 1))
{
// Expand scissor Y to cover the full active area
scissor_top_left.y = static_cast<u32>(y_off);
scissor_bottom_right.y = static_cast<u32>(y_off + active_height - 1);
// Expand viewport ht so NDC [-1,+1] maps to the full active area.
// Keep yOrig (center) the same, just scale ht to half the active height.
const float sign = (viewport.ht < 0) ? -1.0f : 1.0f;
viewport.ht = sign * center_y;
}
}
// VR: Expand scissor for orthographic VR draws.
// The GS ortho VR path reprojects vertex positions from game ortho space to HMD clip space.
// Game-side scissor rects (e.g. roulette window clipping in Mario Kart DD) remain in EFB
// coordinates and will incorrectly clip the VR-reprojected content. Expand both X and Y
// to the full active area so all VR virtual-screen content is visible.
else if (g_ActiveConfig.stereo_mode == StereoMode::OpenXR &&
xfmem.projection.type == ProjectionType::Orthographic)
{
const int x_off = scissor_offset.x << 1;
const int y_off = scissor_offset.y << 1;
const float center_x = viewport.xOrig - static_cast<float>(x_off);
const float center_y = viewport.yOrig - static_cast<float>(y_off);
const int active_width = static_cast<int>(center_x * 2.0f);
const int active_height = static_cast<int>(center_y * 2.0f);
if (active_width > 0 && active_height > 0)
{
scissor_top_left.x = static_cast<u32>(x_off);
scissor_top_left.y = static_cast<u32>(y_off);
scissor_bottom_right.x = static_cast<u32>(x_off + active_width - 1);
scissor_bottom_right.y = static_cast<u32>(y_off + active_height - 1);
}
}
const auto result = BPFunctions::ComputeScissorRects(scissor_top_left, scissor_bottom_right,
scissor_offset, viewport);
auto native_rc = result.Best();
auto target_rc = frame_buffer_manager->ConvertEFBRectangle(native_rc.rect);
auto converted_rc = g_gfx->ConvertFramebufferRectangle(target_rc, g_gfx->GetCurrentFramebuffer());
g_gfx->SetScissorRect(converted_rc);
float raw_x = (viewport.xOrig - native_rc.x_off) - viewport.wd;
float raw_y = (viewport.yOrig - native_rc.y_off) + viewport.ht;
float raw_width = 2.0f * viewport.wd;
float raw_height = -2.0f * viewport.ht;
if (g_ActiveConfig.UseVertexRounding())
{
// Round the viewport to match full 1x IR pixels as well.
// This eliminates a line in the archery mode in Wii Sports Resort at 3x IR and higher.
//
// In OpenXR VR, rounding x/y can shift the viewport origin enough to clip stereo geometry
// and 2D passes. Keep size rounding but preserve origin there.
if (g_ActiveConfig.stereo_mode != StereoMode::OpenXR)
{
raw_x = std::round(raw_x);
raw_y = std::round(raw_y);
}
raw_width = std::round(raw_width);
raw_height = std::round(raw_height);
}
float x = frame_buffer_manager->EFBToScaledXf(raw_x);
float y = frame_buffer_manager->EFBToScaledYf(raw_y);
float width = frame_buffer_manager->EFBToScaledXf(raw_width);
float height = frame_buffer_manager->EFBToScaledYf(raw_height);
float min_depth = (viewport.farZ - viewport.zRange) / 16777216.0f;
float max_depth = viewport.farZ / 16777216.0f;
if (width < 0.f)
{
x += width;
width *= -1;
}
if (height < 0.f)
{
y += height;
height *= -1;
}
if (!g_backend_info.bSupportsDepthClamp)
{
// There's no way to support oversized depth ranges in this situation. Let's just clamp the
// range to the maximum value supported by the console GPU and hope for the best.
min_depth = std::clamp(min_depth, 0.0f, MAX_EFB_DEPTH);
max_depth = std::clamp(max_depth, 0.0f, MAX_EFB_DEPTH);
}
if (VertexShaderManager::UseVertexDepthRange())
{
// We need to ensure depth values are clamped the maximum value supported by the console GPU.
// Taking into account whether the depth range is inverted or not.
if (viewport.zRange < 0.0f && g_backend_info.bSupportsReversedDepthRange)
{
min_depth = MAX_EFB_DEPTH;
max_depth = 0.0f;
}
else
{
min_depth = 0.0f;
max_depth = MAX_EFB_DEPTH;
}
}
float near_depth, far_depth;
if (g_backend_info.bSupportsReversedDepthRange)
{
// Set the reversed depth range.
near_depth = max_depth;
far_depth = min_depth;
}
else
{
// We use an inverted depth range here to apply the Reverse Z trick.
// This trick makes sure we match the precision provided by the 1:0
// clipping depth range on the hardware.
near_depth = 1.0f - max_depth;
far_depth = 1.0f - min_depth;
}
// Lower-left flip.
if (g_backend_info.bUsesLowerLeftOrigin)
y = static_cast<float>(g_gfx->GetCurrentFramebuffer()->GetHeight()) - y - height;
g_gfx->SetViewport(x, y, width, height, near_depth, far_depth);
g_gfx->StoreViewportAndScissor(AbstractGfx::ViewportAndScissor{.scissor_rect = converted_rc,
.viewport_x = x,
.viewport_y = y,
.viewport_width = width,
.viewport_height = height,
.viewport_near_depth = near_depth,
.viewport_far_depth = far_depth});
}
void SetDepthMode()
{
g_vertex_manager->SetDepthStateChanged();
}
void SetBlendMode()
{
g_vertex_manager->SetBlendingStateChanged();
}
/* Explanation of the magic behind ClearScreen:
There's numerous possible formats for the pixel data in the EFB.
However, in the HW accelerated backends we're always using RGBA8
for the EFB format, which causes some problems:
- We're using an alpha channel although the game doesn't
- If the actual EFB format is RGBA6_Z24 or R5G6B5_Z16, we are using more bits per channel than the
native HW
To properly emulate the above points, we're doing the following:
(1)
- disable alpha channel writing of any kind of rendering if the actual EFB format doesn't use an
alpha channel
- NOTE: Always make sure that the EFB has been cleared to an alpha value of 0xFF in this case!
- Same for color channels, these need to be cleared to 0x00 though.
(2)
- convert the RGBA8 color to RGBA6/RGB8/RGB565 and convert it to RGBA8 again
- convert the Z24 depth value to Z16 and back to Z24
*/
bool ClearScreen(FramebufferManager* frame_buffer_manager, const MathUtil::Rectangle<int>& rc,
bool color_enable, bool alpha_enable, bool z_enable, PixelFormat pixel_format,
u32 clear_color_ar, u32 clear_color_gb, u32 clear_z_value,
bool frame_just_rendered)
{
// (1): Disable unused color channels
if (pixel_format == PixelFormat::RGB8_Z24 || pixel_format == PixelFormat::RGB565_Z16 ||
pixel_format == PixelFormat::Z24)
{
alpha_enable = false;
}
// VR AR mode: force alpha clears so the EFB background starts at alpha=0, allowing
// the OpenXR compositor to show the headset passthrough through cleared regions.
// Debug AR mode forces a clear to opaque magenta so we can see the same regions on
// any headset (composition stays OPAQUE; no passthrough required).
if (g_ActiveConfig.vr_ar_mode || g_ActiveConfig.vr_ar_mode_debug)
{
color_enable = true;
alpha_enable = true;
}
if (color_enable || alpha_enable || z_enable)
{
u32 color = (clear_color_ar << 16) | clear_color_gb;
u32 z = clear_z_value;
// VR AR debug: paint the would-be-transparent background opaque magenta so we can
// see exactly which pixels passthrough would reveal, on any headset.
if (g_ActiveConfig.vr_ar_mode_debug)
color = 0xFFFF00FF;
// VR AR mode: force the cleared alpha byte to 0 (transparent) regardless of the
// game's requested clear color, so the passthrough background shows through.
else if (g_ActiveConfig.vr_ar_mode)
color &= 0x00FFFFFF;
VideoCommon::OpenXROpcodeReplay::ApplyReplayClearState(frame_just_rendered, &color_enable,
&alpha_enable, &z_enable,
&pixel_format, &color, &z);
// (2) drop additional accuracy
if (pixel_format == PixelFormat::RGBA6_Z24)
{
color = RGBA8ToRGBA6ToRGBA8(color);
}
else if (pixel_format == PixelFormat::RGB565_Z16)
{
color = RGBA8ToRGB565ToRGBA8(color);
z = Z24ToZ16ToZ24(z);
}
// VR Draw Debug: log EFB clears so we can see if cinematic bars are ClearEFB operations
if (ShaderHunter::GetInstance().IsDebugLogging()) [[unlikely]]
{
INFO_LOG_FMT(VIDEO,
"VR_CLEAR: rect({},{} {}x{}) color={:08x} z={:06x} "
"col={} alpha={} z_en={}",
rc.left, rc.top, rc.GetWidth(), rc.GetHeight(), color, z, color_enable,
alpha_enable, z_enable);
}
frame_buffer_manager->ClearEFB(rc, color_enable, alpha_enable, z_enable, color, z,
pixel_format);
return true;
}
return false;
}
void OnPixelFormatChange(FramebufferManager* frame_buffer_manager, PixelFormat pixel_format,
DepthFormat z_format)
{
// TODO : Check for Z compression format change
// When using 16bit Z, the game may enable a special compression format which we might need to
// handle. Only a few games like RS2 and RS3 even use z compression but it looks like they
// always use ZFAR when using 16bit Z (on top of linear 24bit Z)
// Besides, we currently don't even emulate 16bit depth and force it to 24bit.
/*
* When changing the EFB format, the pixel data won't get converted to the new format but stays
* the same.
* Since we are always using an RGBA8 buffer though, this causes issues in some games.
* Thus, we reinterpret the old EFB data with the new format here.
*/
if (!g_ActiveConfig.bEFBEmulateFormatChanges)
return;
const auto old_format = frame_buffer_manager->GetPrevPixelFormat();
const auto new_format = pixel_format;
frame_buffer_manager->StorePixelFormat(new_format);
DEBUG_LOG_FMT(VIDEO, "pixelfmt: pixel={}, zc={}", new_format, z_format);
// no need to reinterpret pixel data in these cases
if (new_format == old_format || old_format == PixelFormat::INVALID_FMT)
return;
// Check for pixel format changes
switch (old_format)
{
case PixelFormat::RGB8_Z24:
case PixelFormat::Z24:
{
// Z24 and RGB8_Z24 are treated equal, so just return in this case
if (new_format == PixelFormat::RGB8_Z24 || new_format == PixelFormat::Z24)
return;
if (new_format == PixelFormat::RGBA6_Z24)
{
g_efb_interface->ReinterpretPixelData(EFBReinterpretType::RGB8ToRGBA6);
return;
}
else if (new_format == PixelFormat::RGB565_Z16)
{
g_efb_interface->ReinterpretPixelData(EFBReinterpretType::RGB8ToRGB565);
return;
}
}
break;
case PixelFormat::RGBA6_Z24:
{
if (new_format == PixelFormat::RGB8_Z24 || new_format == PixelFormat::Z24)
{
g_efb_interface->ReinterpretPixelData(EFBReinterpretType::RGBA6ToRGB8);
return;
}
else if (new_format == PixelFormat::RGB565_Z16)
{
g_efb_interface->ReinterpretPixelData(EFBReinterpretType::RGBA6ToRGB565);
return;
}
}
break;
case PixelFormat::RGB565_Z16:
{
if (new_format == PixelFormat::RGB8_Z24 || new_format == PixelFormat::Z24)
{
g_efb_interface->ReinterpretPixelData(EFBReinterpretType::RGB565ToRGB8);
return;
}
else if (new_format == PixelFormat::RGBA6_Z24)
{
g_efb_interface->ReinterpretPixelData(EFBReinterpretType::RGB565ToRGBA6);
return;
}
}
break;
default:
break;
}
ERROR_LOG_FMT(VIDEO, "Unhandled EFB format change: {} to {}", old_format, new_format);
}
void SetInterlacingMode(const BPCmd& bp)
{
// TODO
switch (bp.address)
{
case BPMEM_FIELDMODE:
{
// SDK always sets bpmem.lineptwidth.lineaspect via BPMEM_LINEPTWIDTH
// just before this cmd
DEBUG_LOG_FMT(VIDEO, "BPMEM_FIELDMODE texLOD:{} lineaspect:{}", bpmem.fieldmode.texLOD,
bpmem.lineptwidth.adjust_for_aspect_ratio);
}
break;
case BPMEM_FIELDMASK:
{
// Determines if fields will be written to EFB (always computed)
DEBUG_LOG_FMT(VIDEO, "BPMEM_FIELDMASK even:{} odd:{}", bpmem.fieldmask.even,
bpmem.fieldmask.odd);
}
break;
default:
ERROR_LOG_FMT(VIDEO, "SetInterlacingMode default");
break;
}
}
} // namespace BPFunctions