// Copyright 2009 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "VideoCommon/BPFunctions.h" #include #include #include #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/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(rect.rect.left + rect.x_off, viewport_left, viewport_right); int x1 = std::clamp(rect.rect.right + rect.x_off, viewport_left, viewport_right); int y0 = std::clamp(rect.rect.top + rect.y_off, viewport_top, viewport_bottom); int y1 = std::clamp(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; 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 viewport_x, std::pair 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(x_range.end) <= EFB_WIDTH); for (const auto& y_range : y_ranges) { DEBUG_ASSERT(y_range.start < y_range.end); DEBUG_ASSERT(static_cast(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 auto original_scissor_top_left = scissor_top_left; const auto original_scissor_bottom_right = scissor_bottom_right; const auto original_viewport = viewport; const int x_off = scissor_offset.x << 1; const int y_off = scissor_offset.y << 1; const float center_x = viewport.xOrig - static_cast(x_off); const int efb_top = static_cast(scissor_top_left.y) - y_off; const int efb_bot = static_cast(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(y_off); const int active_width = static_cast(center_x * 2.0f); const int active_height = static_cast(center_y * 2.0f); // Heuristic: only treat perspective scissor trimming as cinematic letterbox when the viewport // is still close to full-screen size. This avoids distorting perspective sub-viewports (e.g. // menu/character-select model windows) where expanding scissor/viewport would skew geometry. const float vp_w = std::abs(viewport.wd); const float vp_h = std::abs(viewport.ht); const bool likely_fullscreen_width = active_width > 0 && vp_w >= center_x * 0.90f; const bool likely_fullscreen_height = active_height > 0 && vp_h >= center_y * 0.75f; const bool likely_letterbox_pass = likely_fullscreen_width && likely_fullscreen_height; const bool scissor_trimmed = active_height > 0 && (efb_top > 0 || efb_bot < active_height - 1); const bool debug_logging = ShaderHunter::GetInstance().IsDebugLogging(); // Expand if the scissor Y doesn't cover the full active area (any trimming at all) if (debug_logging) [[unlikely]] { INFO_LOG_FMT( VIDEO, "VR_BARS_CHECK: VP({:.0f},{:.0f} {:.0f}x{:.0f}) SC({},{} {},{}) OFF({}, {}) " "center=({:.1f},{:.1f}) active={}x{} efb_y={}..{} full_w={} full_h={} trimmed={}", original_viewport.xOrig, original_viewport.yOrig, original_viewport.wd, original_viewport.ht, original_scissor_top_left.x, original_scissor_top_left.y, original_scissor_bottom_right.x, original_scissor_bottom_right.y, x_off, y_off, center_x, center_y, active_width, active_height, efb_top, efb_bot, likely_fullscreen_width, likely_fullscreen_height, scissor_trimmed); } if (likely_letterbox_pass && scissor_trimmed) { // Expand scissor Y to cover the full active area scissor_top_left.y = static_cast(y_off); scissor_bottom_right.y = static_cast(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; if (debug_logging) [[unlikely]] { INFO_LOG_FMT( VIDEO, "VR_BARS_APPLY: VP({:.0f},{:.0f} {:.0f}x{:.0f}) -> ({:.0f},{:.0f} {:.0f}x{:.0f}) " "SC({},{} {},{}) -> ({},{} {},{})", original_viewport.xOrig, original_viewport.yOrig, original_viewport.wd, original_viewport.ht, viewport.xOrig, viewport.yOrig, viewport.wd, viewport.ht, original_scissor_top_left.x, original_scissor_top_left.y, original_scissor_bottom_right.x, original_scissor_bottom_right.y, scissor_top_left.x, scissor_top_left.y, scissor_bottom_right.x, scissor_bottom_right.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(x_off); const float center_y = viewport.yOrig - static_cast(y_off); const int active_width = static_cast(center_x * 2.0f); const int active_height = static_cast(center_y * 2.0f); if (active_width > 0 && active_height > 0) { scissor_top_left.x = static_cast(x_off); scissor_top_left.y = static_cast(y_off); scissor_bottom_right.x = static_cast(x_off + active_width - 1); scissor_bottom_right.y = static_cast(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(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& 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) { // (1): Disable unused color channels if (pixel_format == PixelFormat::RGB8_Z24 || pixel_format == PixelFormat::RGB565_Z16 || pixel_format == PixelFormat::Z24) { alpha_enable = false; } if (color_enable || alpha_enable || z_enable) { u32 color = (clear_color_ar << 16) | clear_color_gb; u32 z = clear_z_value; // (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