diff --git a/pcsx2-qt/Settings/GraphicsDisplaySettingsTab.ui b/pcsx2-qt/Settings/GraphicsDisplaySettingsTab.ui
index 87d9f91727..7c74bd006f 100644
--- a/pcsx2-qt/Settings/GraphicsDisplaySettingsTab.ui
+++ b/pcsx2-qt/Settings/GraphicsDisplaySettingsTab.ui
@@ -84,6 +84,16 @@
+ -
+
+
+ Attempts to pre-round sprite texel coordinates to resolve rounding issues. Helpful for games such as Beyond Good and Evil, and Manhunt
+
+
+ Pre-Round Sprites
+
+
+
-
diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp
index de7bce69ab..9cb24b4263 100644
--- a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp
+++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp
@@ -95,12 +95,14 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsWindow* settings_dialog,
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_display.integerScaling, "EmuCore/GS", "IntegerScaling", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_display.PCRTCOffsets, "EmuCore/GS", "pcrtc_offsets", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_display.PCRTCOverscan, "EmuCore/GS", "pcrtc_overscan", false);
+ SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_display.PreRoundSprites, "EmuCore/GS", "preround_sprites", false);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_display.PCRTCAntiBlur, "EmuCore/GS", "pcrtc_antiblur", true);
SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_display.DisableInterlaceOffset, "EmuCore/GS", "disable_interlace_offset", false);
SettingWidgetBinder::BindWidgetToIntSetting(
sif, m_capture.screenshotSize, "EmuCore/GS", "ScreenshotSize", static_cast(GSScreenshotSize::WindowResolution));
SettingWidgetBinder::BindWidgetToIntSetting(
sif, m_capture.screenshotFormat, "EmuCore/GS", "ScreenshotFormat", static_cast(GSScreenshotFormat::PNG));
+
SettingWidgetBinder::BindWidgetToFloatSetting(sif, m_display.stretchY, "EmuCore/GS", "StretchY", 100.0f);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_display.cropLeft, "EmuCore/GS", "CropLeft", 0);
SettingWidgetBinder::BindWidgetToIntSetting(sif, m_display.cropTop, "EmuCore/GS", "CropTop", 0);
diff --git a/pcsx2/Config.h b/pcsx2/Config.h
index 786843f2da..9024084340 100644
--- a/pcsx2/Config.h
+++ b/pcsx2/Config.h
@@ -753,6 +753,7 @@ struct Pcsx2Config
PreloadFrameWithGSData : 1,
Mipmap : 1,
HWMipmap : 1,
+ PreRoundSprites : 1,
ManualUserHacks : 1,
UserHacks_AlignSpriteX : 1,
UserHacks_CPUFBConversion : 1,
diff --git a/pcsx2/GS/GSState.cpp b/pcsx2/GS/GSState.cpp
index 69546d0686..8f4b69e856 100644
--- a/pcsx2/GS/GSState.cpp
+++ b/pcsx2/GS/GSState.cpp
@@ -1729,7 +1729,96 @@ void GSState::FlushPrim()
// Sometimes hardware doesn't get affected, likely due to the difference in how GPU's handle textures (Persona minimap).
if (PRIM->TME && (GSUtil::GetPrimClass(PRIM->PRIM) == GS_PRIM_CLASS::GS_SPRITE_CLASS || m_vt.m_eq.z))
{
- if (!PRIM->FST) // STQ's
+ if (PRIM->FST && GSConfig.PreRoundSprites) // UV's
+ {
+ // UV's on sprites only alter the final UV, I believe the problem is we're drawing too much (drawing stops earlier than we do)
+ // But this fixes Beyond Good adn Evil water and Dark Cloud 2's UI
+ if (GSUtil::GetPrimClass(PRIM->PRIM) == GS_PRIM_CLASS::GS_SPRITE_CLASS)
+ {
+ for (u32 i = 0; i < m_index.tail; i += 2)
+ {
+ GSVertex* v1 = &m_vertex.buff[m_index.buff[i]];
+ GSVertex* v2 = &m_vertex.buff[m_index.buff[i + 1]];
+
+ GSVertex* vu1;
+ GSVertex* vu2;
+ GSVertex* vv1;
+ GSVertex* vv2;
+ if (v1->U > v2->U)
+ {
+ vu2 = v1;
+ vu1 = v2;
+ }
+ else
+ {
+ vu2 = v2;
+ vu1 = v1;
+ }
+
+ if (v1->V > v2->V)
+ {
+ vv2 = v1;
+ vv1 = v2;
+ }
+ else
+ {
+ vv2 = v2;
+ vv1 = v1;
+ }
+
+ GSVector2 pos_range(vu2->XYZ.X - vu1->XYZ.X, vv2->XYZ.Y - vv1->XYZ.Y);
+ const GSVector2 uv_range(vu2->U - vu1->U, vv2->V - vv1->V);
+ if (pos_range.x == 0)
+ pos_range.x = 1;
+ if (pos_range.y == 0)
+ pos_range.y = 1;
+ const GSVector2 grad(uv_range / pos_range);
+ bool isclamp_w = m_context->CLAMP.WMS > 0 && m_context->CLAMP.WMS < 3;
+ bool isclamp_h = m_context->CLAMP.WMT > 0 && m_context->CLAMP.WMT < 3;
+ int max_w = (m_context->CLAMP.WMS == 2) ? m_context->CLAMP.MAXU : ((1 << m_context->TEX0.TW) - 1);
+ int max_h = (m_context->CLAMP.WMT == 2) ? m_context->CLAMP.MAXV : ((1 << m_context->TEX0.TH) - 1);
+ int width = vu2->U >> 4;
+ int height = vv2->V >> 4;
+
+ if (m_context->CLAMP.WMS == 3)
+ width = (width & m_context->CLAMP.MINU) | m_context->CLAMP.MAXU;
+
+ if (m_context->CLAMP.WMT == 3)
+ height = (height & m_context->CLAMP.MINV) | m_context->CLAMP.MAXV;
+
+ if ((isclamp_w && width <= max_w && grad.x != 1.0f) || !isclamp_w)
+ {
+ if (vu2->U >= 0x1 && (!(vu2->U & 0xf) || grad.x <= 1.0f))
+ {
+ vu2->U = (vu2->U - 1);
+ /*if (!isclamp_w && ((vu2->XYZ.X - m_context->XYOFFSET.OFX) >> 4) < m_context->scissor.in.z)
+ vu2->XYZ.X -= 1;*/
+ }
+ }
+ else
+ vu2->U &= ~0xf;
+
+ // Dark Cloud 2 tries to address wider than the TBW when in CLAMP mode (maybe some weird behaviour in HW?)
+ if ((vu2->U >> 4) > (int)(m_context->TEX0.TBW * 64) && isclamp_w && (vu2->U >> 4) - 8 <= (int)(m_context->TEX0.TBW * 64))
+ {
+ vu2->U = (m_context->TEX0.TBW * 64) << 4;
+ }
+
+ if ((isclamp_h && height <= max_h && grad.y != 1.0f) || !isclamp_h)
+ {
+ if (vv2->V >= 0x1 && (!(vv2->V & 0xf) || grad.y <= 1.0f))
+ {
+ vv2->V = (vv2->V - 1);
+ /*if (!isclamp_h && ((vv2->XYZ.Y - m_context->XYOFFSET.OFY) >> 4) < m_context->scissor.in.w)
+ vv2->XYZ.Y -= 1;*/
+ }
+ }
+ else
+ vv2->V &= ~0xf;
+ }
+ }
+ }
+ else if (!PRIM->FST) // STQ's
{
const bool is_sprite = GSUtil::GetPrimClass(PRIM->PRIM) == GS_PRIM_CLASS::GS_SPRITE_CLASS;
// ST's have the lowest 9 bits (or greater depending on exponent difference) rounding down (from hardware tests).
@@ -1783,7 +1872,6 @@ void GSState::FlushPrim()
if (unused > 0)
{
memcpy(m_vertex.buff, buff, sizeof(GSVertex) * unused);
-
m_vertex.tail = unused;
m_vertex.next = next > head ? next - head : 0;
@@ -3727,7 +3815,6 @@ __forceinline void GSState::VertexKick(u32 skip)
const GSVector4i new_v1(m_v.m[1]);
GSVector4i* RESTRICT tailptr = (GSVector4i*)&m_vertex.buff[tail];
-
tailptr[0] = new_v0;
tailptr[1] = new_v1;
diff --git a/pcsx2/GameDatabase.cpp b/pcsx2/GameDatabase.cpp
index bdb28e3c9d..9f1113394d 100644
--- a/pcsx2/GameDatabase.cpp
+++ b/pcsx2/GameDatabase.cpp
@@ -377,6 +377,7 @@ static const char* s_gs_hw_fix_names[] = {
"estimateTextureRegion",
"PCRTCOffsets",
"PCRTCOverscan",
+ "preRoundSprites",
"trilinearFiltering",
"skipDrawStart",
"skipDrawEnd",
@@ -423,6 +424,7 @@ bool GameDatabaseSchema::isUserHackHWFix(GSHWFixId id)
case GSHWFixId::Deinterlace:
case GSHWFixId::Mipmap:
case GSHWFixId::TexturePreloading:
+ case GSHWFixId::PreRoundSprites:
case GSHWFixId::TrilinearFiltering:
case GSHWFixId::MinimumBlendingLevel:
case GSHWFixId::MaximumBlendingLevel:
@@ -779,6 +781,10 @@ void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions&
config.PCRTCOverscan = (value > 0);
break;
+ case GSHWFixId::PreRoundSprites:
+ config.PreRoundSprites = (value > 0);
+ break;
+
case GSHWFixId::Mipmap:
config.HWMipmap = (value > 0);
break;
diff --git a/pcsx2/GameDatabase.h b/pcsx2/GameDatabase.h
index 19d4790359..7d7defc408 100644
--- a/pcsx2/GameDatabase.h
+++ b/pcsx2/GameDatabase.h
@@ -60,6 +60,7 @@ namespace GameDatabaseSchema
EstimateTextureRegion,
PCRTCOffsets,
PCRTCOverscan,
+ PreRoundSprites,
// integer settings
TrilinearFiltering,
diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp
index 91346dabab..f9201877cc 100644
--- a/pcsx2/Pcsx2Config.cpp
+++ b/pcsx2/Pcsx2Config.cpp
@@ -748,6 +748,7 @@ Pcsx2Config::GSOptions::GSOptions()
PreloadFrameWithGSData = false;
Mipmap = true;
HWMipmap = true;
+ PreRoundSprites = false;
ManualUserHacks = false;
UserHacks_AlignSpriteX = false;
@@ -956,6 +957,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap)
SettingsWrapBitBool(OsdShowHardwareInfo);
SettingsWrapBitBool(OsdShowVideoCapture);
SettingsWrapBitBool(OsdShowInputRec);
+ SettingsWrapBitBoolEx(PreRoundSprites, "preround_sprites");
SettingsWrapBitBool(HWSpinGPUForReadbacks);
SettingsWrapBitBool(HWSpinCPUForReadbacks);