// 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/PixelShaderManager.h" #include "VideoCommon/VertexManagerBase.h" #include "VideoCommon/VertexShaderManager.h" #include "VideoCommon/VideoCommon.h" #include "VideoCommon/ShaderHunter.h" #include "VideoCommon/VideoConfig.h" #include "VideoCommon/VR/VRFrameRegion.h" #include "VideoCommon/XFMemory.h" namespace BPFunctions { // ---------------------------------------------- // State translation lookup tables // Reference: Yet Another GameCube Documentation // ---------------------------------------------- // VR (LEGACY path, "Frame Size from XFB Copy" OFF): real active frame height, captured from the // full-width EFB clear. The 3D scene's own viewport can't tell us this (Metroid Prime Trilogy // clears 640x480 but then renders the scene into a 640x448 viewport, leaving a permanent 32-line // bottom bar), so we read it from the frame clear instead. // Known flaw (why the XFB-copy path superseded it): many games clear MORE EFB lines than they // scan out (Mario Galaxy), so the clear height wrongly triggers expansion during normal gameplay. static int s_vr_frame_active_height = 0; 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 && xfmem.projection.type == ProjectionType::Perspective) { const int x_off = scissor_offset.x << 1; const int y_off = scissor_offset.y << 1; const VR::VRFrameRegion frame = VR::GetVRFrameRegion(); const VR::VRViewportClass vp_class = VR::ClassifyVRViewport(viewport, x_off, y_off); if (g_ActiveConfig.vr_panes_on_screen && g_ActiveConfig.vr_virtual_screen && frame.valid && vp_class == VR::VRViewportClass::HudElement) { // Sub-screen 3D pane (e.g. MKDD character select boxes): GeometryShaderManager routes // this draw onto the virtual screen with a pane->frame NDC remap, so its output spans // the whole frame. The game's pane-local viewport/scissor would squeeze and clip the // reprojected content — replace both with the full displayed frame region. scissor_top_left.x = static_cast(x_off + frame.left); scissor_top_left.y = static_cast(y_off + frame.top); scissor_bottom_right.x = static_cast(x_off + frame.left + frame.width - 1); scissor_bottom_right.y = static_cast(y_off + frame.top + frame.height - 1); // Standard GX orientation (wd > 0, ht < 0) — any pane flip is encoded in the remap. viewport.xOrig = static_cast(x_off + frame.left) + frame.width * 0.5f; viewport.yOrig = static_cast(y_off + frame.top) + frame.height * 0.5f; viewport.wd = frame.width * 0.5f; viewport.ht = -frame.height * 0.5f; } else if (g_ActiveConfig.vr_remove_bars && g_ActiveConfig.vr_frame_size_from_xfb) { // Preserve the game's viewport so EFB readbacks (Trilogy blur and visors) keep // their exact pixel contract. Scissor-only cinematic bars can safely be opened // to the original viewport. Viewport/display height mismatches are repaired later // while making the VRAM XFB presentation copy. if (frame.valid && (vp_class == VR::VRViewportClass::MainScene || vp_class == VR::VRViewportClass::Letterboxed)) { const int efb_top = static_cast(scissor_top_left.y) - y_off; const int efb_bot = static_cast(scissor_bottom_right.y) - y_off; const int viewport_top = static_cast( std::lround(viewport.yOrig - std::fabs(viewport.ht) - y_off)); const int viewport_bot = static_cast( std::lround(viewport.yOrig + std::fabs(viewport.ht) - y_off)) - 1; const int viewport_width = static_cast(std::lround(2.0f * std::fabs(viewport.wd))); const int viewport_height = viewport_bot - viewport_top + 1; const int scissor_width = static_cast(scissor_bottom_right.x) - static_cast(scissor_top_left.x) + 1; const int scissor_height = efb_bot - efb_top + 1; const bool plausible_bar_scissor = scissor_width >= viewport_width * 9 / 10 && scissor_height >= viewport_height / 2; // A small scissor inside a full-size viewport is a deliberate sub-window, not // cinematic bars. Expanding it makes preview/model geometry escape its pane. if (plausible_bar_scissor && (efb_top > viewport_top || efb_bot < viewport_bot)) { scissor_top_left.y = static_cast(y_off + std::max(viewport_top, 0)); scissor_bottom_right.y = static_cast( y_off + std::min(viewport_bot, static_cast(EFB_HEIGHT) - 1)); } } } else if (g_ActiveConfig.vr_remove_bars) { // LEGACY path ("Frame Size from XFB Copy" OFF) — kept verbatim for A/B testing. 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 area from the viewport center: the viewport origin sits at // the center of the active area, so active extent = 2 * center. // This avoids using EFB_HEIGHT (528) when the game only uses 448 active lines. 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); int active_height = static_cast(center_y * 2.0f); // Gate: only touch the main full-width scene draw. Perspective EFB-effect passes // (shadow/env/reflection maps) use smaller viewports and must be left untouched or they // distort under head tracking. const bool is_main_scene = active_width >= static_cast(EFB_WIDTH) * 9 / 10; // The scene viewport only knows its own (possibly short) height — Metroid Prime Trilogy // renders into 448 lines of a 480-line frame, so "448 already full" is wrong. Use the real // frame height captured from the full-screen clear to detect that case. if (is_main_scene && s_vr_frame_active_height > active_height) active_height = s_vr_frame_active_height; // Expand if the scissor Y doesn't cover the full active area (any trimming at all) if (is_main_scene && 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(y_off); scissor_bottom_right.y = static_cast(y_off + active_height - 1); // Expand the viewport DOWNWARD only: keep the top edge fixed where the game put it and // extend the bottom to the real frame height. Re-centering on the old origin would shift // the horizon and vertically stretch the scene. const float new_half = static_cast(active_height) * 0.5f; const float sign = (viewport.ht < 0) ? -1.0f : 1.0f; viewport.yOrig = static_cast(y_off) + new_half; viewport.ht = sign * new_half; } } } // 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 && g_ActiveConfig.vr_ortho_scissor_fix && xfmem.projection.type == ProjectionType::Orthographic) { const int x_off = scissor_offset.x << 1; const int y_off = scissor_offset.y << 1; // Ortho render-to-texture / offscreen passes keep the game's own scissor — their output // is read back as a texture, not shown on the virtual screen. const bool rtt_exempt = [&] { if (!g_ActiveConfig.vr_detect_render_targets) return false; const VR::VRViewportClass vp_class = VR::ClassifyVRViewport(viewport, x_off, y_off); return vp_class == VR::VRViewportClass::RenderToTexture || vp_class == VR::VRViewportClass::Offscreen; }(); 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 (!rtt_exempt && 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; #ifdef ENABLE_VR // Exact virtual-screen depth export: mirror the host depth range into the PS constants so // screen-handled draws can reproduce the fixed-function viewport transform bit-for-bit. if (g_ActiveConfig.stereo_mode == StereoMode::OpenXR) { Core::System::GetInstance().GetPixelShaderManager().SetVRScreenDepthRange(near_depth, far_depth); } #endif 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}); } bool SetVRPaneScreenViewport(FramebufferManager* frame_buffer_manager) { const VR::VRFrameRegion frame = VR::GetVRFrameRegion(); if (!frame.valid || frame.width <= 0 || frame.height <= 0) return false; const int x_off = bpmem.scissorOffset.x << 1; const int y_off = bpmem.scissorOffset.y << 1; ScissorPos scissor_top_left = bpmem.scissorTL; ScissorPos scissor_bottom_right = bpmem.scissorBR; scissor_top_left.x = static_cast(x_off + frame.left); scissor_top_left.y = static_cast(y_off + frame.top); scissor_bottom_right.x = static_cast(x_off + frame.left + frame.width - 1); scissor_bottom_right.y = static_cast(y_off + frame.top + frame.height - 1); Viewport viewport = xfmem.viewport; viewport.xOrig = static_cast(x_off + frame.left) + frame.width * 0.5f; viewport.yOrig = static_cast(y_off + frame.top) + frame.height * 0.5f; viewport.wd = frame.width * 0.5f; viewport.ht = -frame.height * 0.5f; SetScissorAndViewport(frame_buffer_manager, scissor_top_left, scissor_bottom_right, bpmem.scissorOffset, viewport); return true; } 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, bool frame_just_rendered) { // VR LEGACY capture ("Frame Size from XFB Copy" OFF): the real frame height for the // cinematic-bar fix. Only trust clears that span (nearly) the full EFB width and start at the // top — those are frame clears, not small EFB-effect clears. The last such clear each frame // wins, which is correct because the frame clear precedes the scene draws. if (g_ActiveConfig.stereo_mode == StereoMode::OpenXR && g_ActiveConfig.vr_remove_bars && !g_ActiveConfig.vr_frame_size_from_xfb && rc.left == 0 && rc.top == 0 && rc.GetWidth() >= static_cast(EFB_WIDTH) * 9 / 10 && rc.GetHeight() > 0 && rc.GetHeight() <= static_cast(EFB_HEIGHT)) { s_vr_frame_active_height = rc.GetHeight(); // = 480 for Metroid Prime Trilogy } // (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