From 48fd68ca87a89e1201760af8767bb62ef7f030d4 Mon Sep 17 00:00:00 2001 From: refractionpcsx2 Date: Sun, 10 Apr 2022 15:30:55 +0100 Subject: [PATCH] GS: Implement PCRTC Offsets This is off by default because people will complain about black borders, but I've tried to keep it to a minimum. Enabling this option (Screen Offsets in the graphics settings) will allow you to position the screen in games which allow you to do so, maintain correct aspect ratios, and screen shake effects which are done on PCRTC (WipEout Fusion for example) will work. --- pcsx2-qt/Settings/GraphicsSettingsWidget.cpp | 1 + pcsx2-qt/Settings/GraphicsSettingsWidget.ui | 29 ++- pcsx2/Config.h | 1 + pcsx2/GS/GS.cpp | 1 + pcsx2/GS/GSState.cpp | 140 ++++++++----- pcsx2/GS/GSState.h | 28 ++- pcsx2/GS/Renderers/Common/GSRenderer.cpp | 202 +++++++++++++++---- pcsx2/GS/Renderers/DX11/GSDevice11.cpp | 6 +- pcsx2/GS/Renderers/HW/GSRendererHW.cpp | 2 +- pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp | 10 +- pcsx2/GS/Renderers/SW/GSRendererSW.h | 2 +- pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp | 13 +- pcsx2/GS/Window/GSSetting.cpp | 4 + pcsx2/GS/Window/GSSetting.h | 1 + pcsx2/GS/Window/GSwxDialog.cpp | 9 + pcsx2/Pcsx2Config.cpp | 3 + 16 files changed, 323 insertions(+), 129 deletions(-) diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp index 05fe1ef3cc..c31dad2d89 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp @@ -95,6 +95,7 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.interlacing, "EmuCore/GS", "deinterlace", DEFAULT_INTERLACE_MODE); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.bilinearFiltering, "EmuCore/GS", "linear_present", true); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.integerScaling, "EmuCore/GS", "IntegerScaling", false); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.PCRTCOffsets, "EmuCore/GS", "pcrtc_offsets", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.internalResolutionScreenshots, "EmuCore/GS", "InternalResolutionScreenshots", false); SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.zoom, "EmuCore/GS", "Zoom", 100.0f); SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_ui.stretchY, "EmuCore/GS", "StretchY", 100.0f); diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui index 3c05868e2e..172fa3e983 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui @@ -6,7 +6,7 @@ 0 0 - 747 + 566 890 @@ -261,34 +261,41 @@ - - - - Integer Upscaling - - - - + Bilinear Filtering - + Sync To Host Refresh (VSync) - + Internal Resolution Screenshots + + + + Integer Upscaling + + + + + + + Screen Offsets + + + diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 37c289509e..fb27ceb360 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -422,6 +422,7 @@ struct Pcsx2Config struct { bool + PCRTCOffsets : 1, IntegerScaling : 1, LinearPresent : 1, UseDebugDevice : 1, diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp index c6e55a681c..89567aec87 100644 --- a/pcsx2/GS/GS.cpp +++ b/pcsx2/GS/GS.cpp @@ -1359,6 +1359,7 @@ void GSApp::Init() m_default_configuration["FMVSoftwareRendererSwitch"] = "0"; m_default_configuration["fxaa"] = "0"; m_default_configuration["HWDisableReadbacks"] = "0"; + m_default_configuration["pcrtc_offsets"] = "0"; m_default_configuration["IntegerScaling"] = "0"; m_default_configuration["deinterlace"] = "7"; m_default_configuration["conservative_framebuffer"] = "1"; diff --git a/pcsx2/GS/GSState.cpp b/pcsx2/GS/GSState.cpp index c2e16b78fd..fc1977ec83 100644 --- a/pcsx2/GS/GSState.cpp +++ b/pcsx2/GS/GSState.cpp @@ -368,63 +368,51 @@ GSVideoMode GSState::GetVideoMode() __assume(0); // unreachable } -// There are some cases where the PS2 seems to saturate the output circuit size when the developer requests for a higher -// unsupported value with respect to the current video mode via the DISP registers, the following function handles such cases. -// NOTE: This function is totally hacky as there are no documents related to saturation of output dimensions, function is -// generally just based on technical and intellectual guesses. -void GSState::SaturateOutputSize(GSVector4i& r) +bool GSState::IsAnalogue() { + return GetVideoMode() == GSVideoMode::NTSC || GetVideoMode() == GSVideoMode::PAL || GetVideoMode() == GSVideoMode::HDTV_1080I; +} + +GSVector4i GSState::GetDisplayRectSize(int i) +{ + GSVector4i rectangle = { 0, 0, 0, 0 }; + + if (!IsEnabled(0) && !IsEnabled(1)) + return rectangle; + const GSVideoMode videomode = GetVideoMode(); + const auto& DISP = m_regs->DISP[i].DISPLAY; - const bool is_ntsc = videomode == GSVideoMode::NTSC; - const bool is_pal = videomode == GSVideoMode::PAL; + const u32 DW = DISP.DW + 1; + const u32 DH = DISP.DH + 1; +; + // The number of sub pixels to draw are given in DH and DW, the MAGH/V relates to the size of the original square in the FB + // but the size it's drawn to uses the default size of the display mode (for PAL/NTSC this is a MAGH of 3) + // so for example a game will have a DW of 2559 and a MAGH of 4 to make it 512 (from the FB), but because it's drawing 2560 subpixels + // it will cover the entire 640 wide of the screen (2560 / (3+1)). + const int width = (DW / (VideoModeDividers[(int)videomode - 1].x + 1)); + const int height = (DH / (VideoModeDividers[(int)videomode - 1].y + 1)); - const auto& SMODE2 = m_regs->SMODE2; - const auto& PMODE = m_regs->PMODE; - const int res_multi = (SMODE2.INT + 1); - // Pixels only get drawn so quick so oversizing of the DISPLAY is ignored when rasterized. - // Non-Interlaced pictures can only do half the number of lines (double strike) - if (is_ntsc) - r.bottom = r.top + std::min(r.height(), 224 * res_multi); - else if (is_pal) - r.bottom = r.top + std::min(r.height(), 256 * res_multi); + // Set up the display rectangle based on the values obtained from DISPLAY registers + + rectangle.right = width; + rectangle.bottom = height; + //DevCon.Warning("Display Rect Size Returning w %d h %d", width, height); + return rectangle; } GSVector4i GSState::GetDisplayRect(int i) { - if (!IsEnabled(0) && !IsEnabled(1)) - return {}; + GSVector4i rectangle = { 0, 0, 0, 0 }; - // If no specific context is requested then pass the merged rectangle as return value if (i == -1) { - if (IsEnabled(0) && IsEnabled(1)) - { - const GSVector4i disp1_rect = GetDisplayRect(0); - const GSVector4i disp2_rect = GetDisplayRect(1); - - const GSVector4i intersect = disp1_rect.rintersect(disp2_rect); - const GSVector4i combined = disp1_rect.runion_ordered(disp2_rect); - - // If the conditions for passing the merged rectangle is unsatisfied, then - // pass the rectangle with the bigger size. - const bool can_be_merged = - intersect.width() == 0 || - intersect.height() == 0 || - intersect.xyxy().eq(combined.xyxy()); - - if (can_be_merged) - return combined; - - if (disp1_rect.rarea() > disp2_rect.rarea()) - return disp1_rect; - - return disp2_rect; - } - - i = m_regs->PMODE.EN2; + return GetDisplayRect(0).runion(GetDisplayRect(1)); } + if (!IsEnabled(i)) + return rectangle; + const auto& DISP = m_regs->DISP[i].DISPLAY; const u32 DW = DISP.DW + 1; @@ -441,18 +429,46 @@ GSVector4i GSState::GetDisplayRect(int i) const int height = DH / magnification.y; // Set up the display rectangle based on the values obtained from DISPLAY registers - GSVector4i rectangle; - - rectangle.left = DX / magnification.x; - rectangle.top = DY / magnification.y; + rectangle.left = DX; + rectangle.top = DY; rectangle.right = rectangle.left + width; rectangle.bottom = rectangle.top + height; - SaturateOutputSize(rectangle); - + //DevCon.Warning("left %d offset %d total %d top %d offset %d total %d", rectangle.left, VideoModeOffsets[(int)videomode - 1].z, (rectangle.left - VideoModeOffsets[(int)videomode - 1].z) / magnification.x, rectangle.top, (VideoModeOffsets[(int)videomode - 1].w / magnification.y) * (IsAnalogue() ? res_multi : 1), v_offset); + //DevCon.Warning("Get Display Rect Left %d Right %d Top %d Bottom %d", rectangle.left, rectangle.right, rectangle.top, rectangle.bottom); return rectangle; } +GSVector2i GSState::GetResolutionOffset(int i) +{ + const GSVideoMode videomode = GetVideoMode(); + const auto& DISP = m_regs->DISP[i].DISPLAY; + + const auto& SMODE2 = m_regs->SMODE2; + const int res_multi = (SMODE2.INT + 1); + GSVector2i offset; + + offset.x = (((int)DISP.DX - VideoModeOffsets[(int)videomode - 1].z) / (VideoModeDividers[(int)videomode - 1].x + 1)); + offset.y = (((int)DISP.DY - (VideoModeOffsets[(int)videomode - 1].w * (IsAnalogue() ? res_multi : 1))) / (VideoModeDividers[(int)videomode - 1].y + 1)); + //DevCon.Warning("Res Offset X %d Video mode %d Y %d Video mode %d", DISP.DX, VideoModeOffsets[(int)videomode - 1].z, DISP.DY, (VideoModeOffsets[(int)videomode - 1].w * (IsAnalogue() ? res_multi : 1))); + + return offset; +} + +GSVector2i GSState::GetResolution() +{ + const GSVideoMode videomode = GetVideoMode(); + const auto& SMODE2 = m_regs->SMODE2; + const int res_multi = (SMODE2.INT + 1); + + GSVector2i resolution; + + resolution.x = VideoModeOffsets[(int)videomode - 1].x; + resolution.y = VideoModeOffsets[(int)videomode - 1].y * ((IsAnalogue() && res_multi) ? res_multi : 1); + //DevCon.Warning("Getting Resolution X %d Y %d", resolution.x, resolution.y); + return resolution; +} + GSVector4i GSState::GetFrameRect(int i) { // If no specific context is requested then pass the merged rectangle as return value @@ -461,19 +477,31 @@ GSVector4i GSState::GetFrameRect(int i) GSVector4i rectangle = GetDisplayRect(i); - int w = rectangle.width(); - int h = rectangle.height(); + const auto& DISP = m_regs->DISP[i].DISPLAY; - if (isinterlaced() && m_regs->SMODE2.FFMD && h > 1) - h >>= 1; + const u32 DW = DISP.DW + 1; + const u32 DH = DISP.DH + 1; + const GSVector2i magnification(DISP.MAGH+1, DISP.MAGV + 1); const u32 DBX = m_regs->DISP[i].DISPFB.DBX; const u32 DBY = m_regs->DISP[i].DISPFB.DBY; + int w = DW / magnification.x; + int h = DH / magnification.y; + rectangle.left = DBX; rectangle.top = DBY; - rectangle.right = rectangle.left + w; - rectangle.bottom = rectangle.top + h; + + if (isinterlaced() && m_regs->SMODE2.FFMD && h > 1) + { + h >>= 1; + } + + // Round down as this is the reading rect, so we want 0-639 and 0-479 for an example + rectangle.right = rectangle.left + w - 1; + rectangle.bottom = rectangle.top + h - 1; + + //DevCon.Warning("Frame Rect left %d right %d top %d bottom %d DBX %d DBY %d", rectangle.left, rectangle.right, rectangle.top, rectangle.bottom, DBX, DBY); return rectangle; } diff --git a/pcsx2/GS/GSState.h b/pcsx2/GS/GSState.h index ce147db65b..a08b23c1c0 100644 --- a/pcsx2/GS/GSState.h +++ b/pcsx2/GS/GSState.h @@ -258,6 +258,29 @@ public: PRIM_OVERLAP m_prim_overlap; std::vector m_drawlist; + // The horizontal offset values (under z) for PAL and NTSC have been tweaked + // they should be apparently 632 and 652 respectively, but that causes a thick black line on the left + // these values leave a small black line on the right in a bunch of games, but it's not so bad. + // The only conclusion I can come to is there is horizontal overscan expected so there would normally + // be black borders either side anyway, or both sides slightly covered. + const GSVector4i VideoModeOffsets[6] = { + GSVector4i(640, 224, 642, 25), + GSVector4i(640, 256, 676, 36), + GSVector4i(640, 480, 276, 34), + GSVector4i(720, 480, 232, 35), + GSVector4i(1280, 720, 302, 24), + GSVector4i(1920, 540, 238, 40) + }; + + const GSVector4i VideoModeDividers[6] = { + GSVector4i(3, 0, 2559, 239), + GSVector4i(3, 0, 2559, 287), + GSVector4i(1, 0, 1279, 479), + GSVector4i(1, 0, 1439, 479), + GSVector4i(0, 0, 1279, 719), + GSVector4i(0, 0, 1919, 1079) + }; + public: GSState(); virtual ~GSState(); @@ -265,13 +288,16 @@ public: void ResetHandlers(); int GetFramebufferHeight(); - void SaturateOutputSize(GSVector4i& r); GSVector4i GetDisplayRect(int i = -1); + GSVector4i GetDisplayRectSize(int i = -1); + GSVector2i GetResolutionOffset(int i = -1); + GSVector2i GetResolution(); GSVector4i GetFrameRect(int i = -1); GSVideoMode GetVideoMode(); bool IsEnabled(int i); bool isinterlaced(); + bool IsAnalogue(); float GetTvRefreshRate(); diff --git a/pcsx2/GS/Renderers/Common/GSRenderer.cpp b/pcsx2/GS/Renderers/Common/GSRenderer.cpp index d12ed4a756..25ea20512b 100644 --- a/pcsx2/GS/Renderers/Common/GSRenderer.cpp +++ b/pcsx2/GS/Renderers/Common/GSRenderer.cpp @@ -77,25 +77,32 @@ bool GSRenderer::Merge(int field) GSVector2i display_baseline = {INT_MAX, INT_MAX}; GSVector2i frame_baseline = {INT_MAX, INT_MAX}; + GSVector2i display_combined = { 0, 0 }; + bool feedback_merge = m_regs->EXTWRITE.WRITE == 1; for (int i = 0; i < 2; i++) { - en[i] = IsEnabled(i); + en[i] = IsEnabled(i) || (m_regs->EXTBUF.FBIN == i && feedback_merge); if (en[i]) { fr[i] = GetFrameRect(i); dr[i] = GetDisplayRect(i); + display_combined.x = std::max(GetDisplayRectSize(i).right + abs(GetResolutionOffset(i).x), display_combined.x); + display_combined.y = std::max(GetDisplayRectSize(i).bottom + abs(GetResolutionOffset(i).y), display_combined.y); display_baseline.x = std::min(dr[i].left, display_baseline.x); display_baseline.y = std::min(dr[i].top, display_baseline.y); frame_baseline.x = std::min(fr[i].left, frame_baseline.x); frame_baseline.y = std::min(fr[i].top, frame_baseline.y); + //DevCon.Warning("Read offset was X %d(left %d) Y %d(top %d)", display_baseline.x, dr[i].left, display_baseline.y, dr[i].top); //printf("[%d]: %d %d %d %d, %d %d %d %d\n", i, fr[i].x,fr[i].y,fr[i].z,fr[i].w , dr[i].x,dr[i].y,dr[i].z,dr[i].w); } } + //display_combined.y = display_combined.y * (IsAnalogue() ? (isinterlaced() + 1) : 1); + if (!en[0] && !en[1]) { return false; @@ -162,8 +169,6 @@ bool GSRenderer::Merge(int field) s_n++; - bool feedback_merge = m_regs->EXTWRITE.WRITE == 1; - if (samesrc && fr[0].bottom == fr[1].bottom && !feedback_merge) { tex[0] = GetOutput(0, y_offset[0]); @@ -182,78 +187,187 @@ bool GSRenderer::Merge(int field) GSVector4 src[2]; GSVector4 src_hw[2]; - GSVector4 dst[2]; + GSVector4 dst[3]; const bool slbg = m_regs->PMODE.SLBG; - bool scanmsk_frame = true; for (int i = 0; i < 2; i++) { if (!en[i] || !tex[i]) continue; - GSVector4i r = fr[i]; + const GSVector2i resolution(GetResolution()); + GSVector4i r = GetDisplayRectSize(i); GSVector4 scale = GSVector4(tex[i]->GetScale()).xyxy(); - src[i] = GSVector4(r) * scale / GSVector4(tex[i]->GetSize()).xyxy(); - src_hw[i] = (GSVector4(r) + GSVector4(0, y_offset[i], 0, y_offset[i])) * scale / GSVector4(tex[i]->GetSize()).xyxy(); + bool ignore_offset = !GSConfig.PCRTCOffsets; - GSVector2 off(0); + GSVector2i off(ignore_offset ? 0 : GetResolutionOffset(i)); GSVector2i display_diff(dr[i].left - display_baseline.x, dr[i].top - display_baseline.y); GSVector2i frame_diff(fr[i].left - frame_baseline.x, fr[i].top - frame_baseline.y); - // Don't apply offset when blending with background color - // The Suffering - video monitors (feedback write) - if (!slbg) + const GSVideoMode videomode = GetVideoMode(); + + const int interlace_offset = display_diff.y & 1; + + if (m_scanmask_used && interlace_offset) { - // Time Crisis 2/3 uses two side by side images when in split screen mode. - // Though ignore cases where baseline and display rectangle offsets only differ by 1 pixel, causes blurring and wrong resolution output on FFXII - if (display_diff.x > 2) - { - off.x = tex[i]->GetScale().x * display_diff.x; - } - // If the DX offset is too small then consider the status of frame memory offsets, prevents blurring on Tenchu: Fatal Shadows, Worms 3D - else if (display_diff.x != frame_diff.x) - { - off.x = tex[i]->GetScale().x * frame_diff.x; - } + display_diff.y &= ~1; - if (m_scanmask_used && display_diff.y == 1) // Scanmask effect wouldn't look correct if we scale the offset - { - off.y = display_diff.y; - scanmsk_frame = false; // Scanmsk is used between the 2 merge circuits. - } - else if (display_diff.y >= 4) // Shouldn't this be >= 2? - { - off.y = tex[i]->GetScale().y * display_diff.y; + if (!ignore_offset) + off.y &= ~1; + } - if (m_regs->SMODE2.INT && m_regs->SMODE2.FFMD) + display_diff.x /= (VideoModeDividers[(int)videomode - 1].x + 1); + display_diff.y /= (VideoModeDividers[(int)videomode - 1].y + 1); + + if (!ignore_offset && display_combined.y < resolution.y && display_combined.x < resolution.x) + { + float difference[2]; + difference[0] = resolution.x / (float)display_combined.x; + difference[1] = resolution.y / (float)display_combined.y; + //DevCon.Warning("Difference x %f y %f old size x %d y %d res x %d y %d off x %d y %d", difference[0], difference[1], r.right, r.bottom, resolution.x, resolution.y, off.x, off.y); + if (difference[0] > 1.0f) + { + float difference_to_use = (difference[0] < difference[1]) ? difference[0] : difference[1]; + int width_change = (r.right * difference_to_use) - r.right; + + r.right += width_change; + off.x -= width_change >> 1; + + int height_change = (r.bottom * difference_to_use) - r.bottom; + if (height_change > 4) { - off.y /= 2; + r.bottom += height_change; + off.y -= height_change >> 1; } } - else if (display_diff.y != frame_diff.y) + // Anti blur hax + if (display_diff.x < 4) { - off.y = tex[i]->GetScale().y * frame_diff.y; + off.x -= display_diff.x; + } + + if (display_diff.y < 4) + { + off.y -= display_diff.y; } } - dst[i] = GSVector4(off).xyxy() + scale * GSVector4(r.rsize()); + else if(ignore_offset)// Stretch to fit the window + { + float difference[2]; - fs.x = std::max(fs.x, (int)(dst[i].z + 0.5f)); - fs.y = std::max(fs.y, (int)(dst[i].w + 0.5f)); + //If the picture is offset we want to make sure we don't make it bigger, so this is the only place we need to now about the offset! + difference[0] = resolution.x / (float)((display_combined.x - GetResolutionOffset(i).x) + display_diff.x); + difference[1] = resolution.y / (float)((display_combined.y - GetResolutionOffset(i).y) + display_diff.y); + + //DevCon.Warning("Difference x %f y %f old size x %d y %d res x %d y %d off x %d y %d disp diff x %d y %d comb x %d y %d res off x %d y %d", difference[0], difference[1], r.right, r.bottom, resolution.x, resolution.y, off.x, off.y, display_diff.x, display_diff.y, display_combined.x, display_combined.y, GetResolutionOffset(i).x, GetResolutionOffset(i).y); + if (difference[0] > 1.0f) + { + const float difference_to_use = difference[0]; + const int width_change = (r.right * difference_to_use) - r.right; + + r.right += width_change; + } + + const float difference_to_use = difference[1]; + const int height_change = (r.bottom * difference_to_use) - r.bottom; + + if (difference[1] > 1.0f && (!m_scanmask_used || height_change > 4)) + { + r.bottom += height_change; + } + // Anti blur hax + if (!slbg || !feedback_merge) + { + if (display_diff.x > 4) + off.x = display_diff.x; + + if (display_diff.y > 4) + off.y = display_diff.y; + } + + if (!slbg || !feedback_merge) + { + if (samesrc) + { + if (display_diff.x < 4 && off.x) + off.x = 0; + if (display_diff.y < 4) + off.y = 0; + + if (display_diff.x > 4) + off.x = display_diff.x; + + if (display_diff.y > 4) + off.y = display_diff.y; + + if (frame_diff.x == 1) + off.x += 1; + if (frame_diff.y == 1) + off.y += 1; + } + else + { + if (display_diff.x > 4) + off.x = display_diff.x; + + if (display_diff.y > 4) + off.y = display_diff.y; + } + } + } + // Anti blur hax + else if (samesrc) + { + if (display_diff.x < 4) + off.x -= display_diff.x; + if (display_diff.y < 4) + off.y -= display_diff.y; + + if (frame_diff.x == 1) + off.x += 1; + if (frame_diff.y == 1) + off.y += 1; + } + + // Src is the size for output + src[i] = GSVector4(r) * scale / GSVector4(tex[i]->GetSize()).xyxy(); + // Src_hw is the size which we're really reading + src_hw[i] = (GSVector4(fr[i]) + GSVector4(0, y_offset[i], 0, y_offset[i])) * scale / GSVector4(tex[i]->GetSize()).xyxy(); + + dst[i] = scale * (GSVector4(off).xyxy() + GSVector4(r.rsize())); + + if (m_scanmask_used && interlace_offset) + dst[i] += GSVector4(0.0f, 1.0f, 0.0f, 1.0f); + //DevCon.Warning("Offset final x %d y %d Display x %d y %d Frame x %d y %d rect x %d y %d z %d w %d", off.x, off.y, display_diff.x, display_diff.x, frame_diff.y, frame_diff.y, r.x, r.y, r.z, r.w); } + if (feedback_merge && tex[2]) + { + GSVector4 scale = GSVector4(tex[2]->GetScale()).xyxy(); + GSVector4i feedback_rect; + + feedback_rect.left = m_regs->EXTBUF.WDX; + feedback_rect.right = feedback_rect.left + ((m_regs->EXTDATA.WW + 1) / ((m_regs->EXTDATA.SMPH - m_regs->DISP[m_regs->EXTBUF.FBIN].DISPLAY.MAGH) + 1)); + feedback_rect.top = m_regs->EXTBUF.WDY; + feedback_rect.bottom = ((m_regs->EXTDATA.WH + 1) * (2 - m_regs->EXTBUF.WFFMD)) / ((m_regs->EXTDATA.SMPV - m_regs->DISP[m_regs->EXTBUF.FBIN].DISPLAY.MAGV) + 1); + + dst[2] = GSVector4(scale * GSVector4(feedback_rect.rsize())); + } + + fs = GetResolution() * GSVector2i(GetUpscaleMultiplier()); + //DevCon.Warning("Res x %d y %d", fs.x, fs.y); ds = fs; + m_real_size = ds; if (m_regs->SMODE2.INT && m_regs->SMODE2.FFMD) { ds.y *= 2; } - m_real_size = ds; - if (tex[0] || tex[1]) { - if (tex[0] == tex[1] && !slbg && (src[0] == src[1] & dst[0] == dst[1]).alltrue()) + if (tex[0] == tex[1] && !slbg && (src[0] == src[1] & dst[0] == dst[1]).alltrue() && !feedback_merge) { // the two outputs are identical, skip drawing one of them (the one that is alpha blended) @@ -266,7 +380,7 @@ bool GSRenderer::Merge(int field) if (m_regs->SMODE2.INT && GSConfig.InterlaceMode != GSInterlaceMode::Off) { - const bool scanmask = (m_scanmask_used && scanmsk_frame) && GSConfig.InterlaceMode == GSInterlaceMode::Automatic; + const bool scanmask = m_scanmask_used && GSConfig.InterlaceMode == GSInterlaceMode::Automatic; if (GSConfig.InterlaceMode == GSInterlaceMode::Automatic && (m_regs->SMODE2.FFMD)) // Auto interlace enabled / Odd frame interlace setting { @@ -276,9 +390,9 @@ bool GSRenderer::Merge(int field) } else { - const int field2 = scanmask ? 0 : 1 - ((static_cast(GSConfig.InterlaceMode) - 1) & 1); + const int field2 = scanmask ? 0 : 0 - ((static_cast(GSConfig.InterlaceMode) - 1) & 1); const int offset = tex[1] ? tex[1]->GetScale().y : tex[0]->GetScale().y; - int mode = scanmask ? 2 : (static_cast(GSConfig.InterlaceMode) - 1) >> 1; + int mode = scanmask ? 2 : ((static_cast(GSConfig.InterlaceMode) - 1) >> 1); g_gs_device->Interlace(ds, field ^ field2, mode, offset); } diff --git a/pcsx2/GS/Renderers/DX11/GSDevice11.cpp b/pcsx2/GS/Renderers/DX11/GSDevice11.cpp index b3d500af3c..79b7c39345 100644 --- a/pcsx2/GS/Renderers/DX11/GSDevice11.cpp +++ b/pcsx2/GS/Renderers/DX11/GSDevice11.cpp @@ -715,13 +715,13 @@ void GSDevice11::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, { // 2nd output is enabled and selected. Copy it to destination so we can blend it with 1st output // Note: value outside of dRect must contains the background color (c) - StretchRect(sTex[1], sRect[1], dTex, dRect[1], ShaderConvert::COPY); + StretchRect(sTex[1], sRect[1], dTex, PMODE.SLBG ? dRect[2] : dRect[1], ShaderConvert::COPY); } // Save 2nd output if (feedback_write_2) { - StretchRect(dTex, full_r, sTex[2], dRect[1], m_convert.ps[static_cast(ShaderConvert::YUV)].get(), + StretchRect(dTex, full_r, sTex[2], dRect[2], m_convert.ps[static_cast(ShaderConvert::YUV)].get(), m_merge.cb.get(), nullptr, true); } @@ -737,7 +737,7 @@ void GSDevice11::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, if (feedback_write_1) { - StretchRect(dTex, full_r, sTex[2], dRect[0], m_convert.ps[static_cast(ShaderConvert::YUV)].get(), + StretchRect(sTex[0], full_r, sTex[2], dRect[2], m_convert.ps[static_cast(ShaderConvert::YUV)].get(), m_merge.cb.get(), nullptr, true); } } diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp index b67bcc2620..23a6eb067c 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp @@ -256,7 +256,7 @@ GSTexture* GSRendererHW::GetFeedbackOutput() TEX0.TBW = m_regs->EXTBUF.EXBW; TEX0.PSM = m_regs->DISP[m_regs->EXTBUF.FBIN & 1].DISPFB.PSM; - GSTextureCache::Target* rt = m_tc->LookupTarget(TEX0, GetTargetSize(), /*GetFrameRect(i).bottom*/ 0); + GSTextureCache::Target* rt = m_tc->LookupTarget(TEX0, GetTargetSize(), /*GetFrameRect(i).bottom*/ m_regs->DISP[m_regs->EXTBUF.FBIN & 1].DISPLAY.DH); GSTexture* t = rt->m_texture; diff --git a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp index 6ffb4c87c3..dfb4e08472 100644 --- a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp +++ b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp @@ -1325,7 +1325,7 @@ void GSDeviceOGL::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, { // 2nd output is enabled and selected. Copy it to destination so we can blend it with 1st output // Note: value outside of dRect must contains the background color (c) - StretchRect(sTex[1], sRect[1], dTex, dRect[1], ShaderConvert::COPY); + StretchRect(sTex[1], sRect[1], dTex, PMODE.SLBG ? dRect[2] : dRect[1], ShaderConvert::COPY); } // Upload constant to select YUV algo @@ -1337,8 +1337,8 @@ void GSDeviceOGL::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, } // Save 2nd output - if (feedback_write_2) // FIXME I'm not sure dRect[1] is always correct - StretchRect(dTex, full_r, sTex[2], dRect[1], ShaderConvert::YUV); + if (feedback_write_2) + StretchRect(dTex, full_r, sTex[2], dRect[2], ShaderConvert::YUV); // Restore background color to process the normal merge if (feedback_write_2_but_blend_bg) @@ -1364,8 +1364,8 @@ void GSDeviceOGL::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, } } - if (feedback_write_1) // FIXME I'm not sure dRect[0] is always correct - StretchRect(dTex, full_r, sTex[2], dRect[0], ShaderConvert::YUV); + if (feedback_write_1) + StretchRect(dTex, full_r, sTex[2], dRect[2], ShaderConvert::YUV); } void GSDeviceOGL::DoInterlace(GSTexture* sTex, GSTexture* dTex, int shader, bool linear, float yoffset) diff --git a/pcsx2/GS/Renderers/SW/GSRendererSW.h b/pcsx2/GS/Renderers/SW/GSRendererSW.h index 7b60a9fe8e..1923af569f 100644 --- a/pcsx2/GS/Renderers/SW/GSRendererSW.h +++ b/pcsx2/GS/Renderers/SW/GSRendererSW.h @@ -71,7 +71,7 @@ protected: IRasterizer* m_rl; GSRingHeap m_vertex_heap; GSTextureCacheSW* m_tc; - GSTexture* m_texture[2]; + GSTexture* m_texture[3]; u8* m_output; GSPixelOffset4* m_fzb; GSVector4i m_fzb_bbox; diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp index 1bf6002155..9b84d939ef 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp @@ -763,7 +763,7 @@ void GSDeviceVK::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, SetUtilityTexture(sTex[1], m_linear_sampler); BeginClearRenderPass(m_utility_color_render_pass_clear, darea, c); SetPipeline(m_convert[static_cast(ShaderConvert::COPY)]); - DrawStretchRect(sRect[1], dRect[1], dsize); + DrawStretchRect(sRect[1], PMODE.SLBG ? dRect[2] : dRect[1], dsize); dTex->SetState(GSTexture::State::Dirty); dcleared = true; } @@ -772,20 +772,19 @@ void GSDeviceVK::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, // Upload constant to select YUV algo const GSVector2i fbsize(sTex[2] ? sTex[2]->GetSize() : GSVector2i(0, 0)); const GSVector4i fbarea(0, 0, fbsize.x, fbsize.y); - if (feedback_write_2)// FIXME I'm not sure dRect[1] is always correct + if (feedback_write_2) { EndRenderPass(); OMSetRenderTargets(sTex[2], nullptr, fbarea, false); if (dcleared) SetUtilityTexture(dTex, m_linear_sampler); - // sTex[2] can be sTex[0], in which case it might be cleared (e.g. Xenosaga). - BeginRenderPassForStretchRect(static_cast(sTex[2]), fbarea, GSVector4i(dRect[1])); + BeginRenderPassForStretchRect(static_cast(sTex[2]), fbarea, GSVector4i(dRect[2])); if (dcleared) { SetPipeline(m_convert[static_cast(ShaderConvert::YUV)]); SetUtilityPushConstants(yuv_constants, sizeof(yuv_constants)); - DrawStretchRect(full_r, dRect[1], fbsize); + DrawStretchRect(full_r, dRect[2], fbsize); } EndRenderPass(); @@ -818,7 +817,7 @@ void GSDeviceVK::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, DrawStretchRect(sRect[0], dRect[0], dTex->GetSize()); } - if (feedback_write_1) // FIXME I'm not sure dRect[0] is always correct + if (feedback_write_1) { EndRenderPass(); SetPipeline(m_convert[static_cast(ShaderConvert::YUV)]); @@ -826,7 +825,7 @@ void GSDeviceVK::DoMerge(GSTexture* sTex[3], GSVector4* sRect, GSTexture* dTex, SetUtilityPushConstants(yuv_constants, sizeof(yuv_constants)); OMSetRenderTargets(sTex[2], nullptr, fbarea, false); BeginRenderPass(m_utility_color_render_pass_load, fbarea); - DrawStretchRect(full_r, dRect[0], dsize); + DrawStretchRect(full_r, dRect[2], dsize); } EndRenderPass(); diff --git a/pcsx2/GS/Window/GSSetting.cpp b/pcsx2/GS/Window/GSSetting.cpp index b4213a3f65..6f2151631f 100644 --- a/pcsx2/GS/Window/GSSetting.cpp +++ b/pcsx2/GS/Window/GSSetting.cpp @@ -89,6 +89,10 @@ const char* dialog_message(int ID, bool* updateText) return cvtString("Enabled: GPU converts colormap-textures.\n" "Disabled: CPU converts colormap-textures.\n\n" "It is a trade-off between GPU and CPU."); + case IDC_PCRTC_OFFSETS: + return cvtString("Enable: Takes in to account offsets in the analogue circuits.\n" + "This will use the intended aspect ratios and screen offsets, may cause odd black borders.\n" + "Used for screen positioning and screen shake in Wipeout Fusion."); case IDC_ACCURATE_DATE: return cvtString("Implement a more accurate algorithm to compute GS destination alpha testing.\n" "It improves shadow and transparency rendering.\n\n" diff --git a/pcsx2/GS/Window/GSSetting.h b/pcsx2/GS/Window/GSSetting.h index 96c54133e0..0c2de522a7 100644 --- a/pcsx2/GS/Window/GSSetting.h +++ b/pcsx2/GS/Window/GSSetting.h @@ -42,6 +42,7 @@ enum { // Renderer IDC_FILTER, + IDC_PCRTC_OFFSETS, // Hardware Renderer IDC_PRELOAD_TEXTURES, IDC_ACCURATE_DATE, diff --git a/pcsx2/GS/Window/GSwxDialog.cpp b/pcsx2/GS/Window/GSwxDialog.cpp index 9c93288016..7534982121 100644 --- a/pcsx2/GS/Window/GSwxDialog.cpp +++ b/pcsx2/GS/Window/GSwxDialog.cpp @@ -276,6 +276,7 @@ RendererTab::RendererTab(wxWindow* parent) auto upscale_prereq = [this]{ return !m_is_native_res; }; PaddedBoxSizer tab_box(wxVERTICAL); + PaddedBoxSizer general_box(wxVERTICAL, this, "General GS Settings"); PaddedBoxSizer hardware_box(wxVERTICAL, this, "Hardware Mode"); PaddedBoxSizer software_box(wxVERTICAL, this, "Software Mode"); @@ -315,8 +316,16 @@ RendererTab::RendererTab(wxWindow* parent) m_ui.addSpinAndLabel(thread_box, "Extra Rendering threads:", "extrathreads", 0, 32, 2, IDC_SWTHREADS, sw_prereq); software_box->Add(thread_box, wxSizerFlags().Centre()); + // General GS Settings box + auto* pcrtc_checks_box = new wxWrapSizer(wxHORIZONTAL); + + m_ui.addCheckBox(pcrtc_checks_box, "Screen Offsets", "pcrtc_offsets", IDC_PCRTC_OFFSETS); + + general_box->Add(pcrtc_checks_box, wxSizerFlags().Center()); + tab_box->Add(hardware_box.outer, wxSizerFlags().Expand()); tab_box->Add(software_box.outer, wxSizerFlags().Expand()); + tab_box->Add(general_box.outer, wxSizerFlags().Expand()); SetSizerAndFit(tab_box.outer); } diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index a5c1fac5ea..f9c9185a2a 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -292,6 +292,7 @@ Pcsx2Config::GSOptions::GSOptions() { bitset = 0; + PCRTCOffsets = false; IntegerScaling = false; LinearPresent = true; UseDebugDevice = false; @@ -501,6 +502,8 @@ void Pcsx2Config::GSOptions::ReloadIniSettings() // Unfortunately, because code in the GS still reads the setting by key instead of // using these variables, we need to use the old names. Maybe post 2.0 we can change this. + + GSSettingBoolEx(PCRTCOffsets, "pcrtc_offsets"); GSSettingBool(IntegerScaling); GSSettingBoolEx(LinearPresent, "linear_present"); GSSettingBool(UseDebugDevice);