Compare commits

..
Author SHA1 Message Date
refractionpcsx2 5e947ac730 GS/HW/NS: Detect fake bilinear jiggle effect 2026-08-06 16:11:43 +01:00
refractionpcsx2 1198000ff3 GameDB: Add required GameDB Entries
S.L.A.I. - Steel Lancer Arena International
Syphon Filter - Logan's Shadow
2026-08-06 02:54:48 +01:00
refractionpcsx2 2855c0631d GS/HW: Improve "jiggle" blur effect detection and handling 2026-08-06 02:54:47 +01:00
refractionpcsx2 163edd643b GS/HW: Native Scaling detection of mild downscale and write back 2026-08-06 02:54:47 +01:00
8 changed files with 94 additions and 87 deletions
+6
View File
@@ -12928,6 +12928,7 @@ SCUS-97584:
autoFlush: 2
preloadFrameData: 1
nativeScaling: 2 # Fixes post-processing.
halfPixelOffset: 4 # Aligns post bloom.
SCUS-97589:
name: "NBA '08 featuring The Life Vol.3"
region: "NTSC-U"
@@ -21923,6 +21924,7 @@ SLES-52940:
region: "PAL-M3"
gsHWFixes:
halfPixelOffset: 5 # Fixes double image and aligns glow effects.
nativeScaling: 1 # Fixes depth blur.
SLES-52941:
name: "Gungrave - Overdose"
region: "PAL-M3"
@@ -45830,6 +45832,7 @@ SLPM-65791:
region: "NTSC-J"
gsHWFixes:
halfPixelOffset: 5 # Fixes double image and aligns glow effects.
nativeScaling: 1 # Fixes depth blur.
SLPM-65793:
name: "SSX3 [EA BEST HITS]"
name-sort: "SSX3 [EA BEST HITS]"
@@ -69376,6 +69379,7 @@ SLUS-20969:
compat: 5
gsHWFixes:
halfPixelOffset: 5 # Fixes double image and aligns glow effects.
nativeScaling: 1 # Fixes depth blur.
SLUS-20970:
name: "Rumble Roses"
region: "NTSC-U"
@@ -75906,6 +75910,7 @@ SLUS-29132:
region: "NTSC-U"
gsHWFixes:
halfPixelOffset: 5 # Fixes double image and aligns glow effects.
nativeScaling: 1 # Fixes depth blur.
SLUS-29133:
name: "Namco Transmission Demo Disc Vol.2"
region: "NTSC-U"
@@ -76708,6 +76713,7 @@ TLES-52940:
region: "PAL-E"
gsHWFixes:
halfPixelOffset: 5 # Fixes double image and aligns glow effects.
nativeScaling: 1 # Fixes depth blur.
TLES-53544:
name: "Pro Evolution Soccer 5 Beta Trial Code"
region: "PAL-E"
+3 -3
View File
@@ -12638,12 +12638,12 @@ This action cannot be undone.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp" line="2849"/>
<location filename="../../pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp" line="2847"/>
<source>Stencil buffers and texture barriers are both unavailable, this will break some graphical effects.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../../pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp" line="5312"/>
<location filename="../../pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp" line="5308"/>
<source>Spin GPU During Readbacks is enabled, but calibrated timestamps are unavailable. This might be really slow.</source>
<translation type="unfinished"></translation>
</message>
@@ -12738,7 +12738,7 @@ Please see our official documentation for more information.</source>
<context>
<name>GSDeviceVK</name>
<message>
<location filename="../../pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp" line="2179"/>
<location filename="../../pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp" line="2177"/>
<source>Your GPU does not support the required Vulkan features.</source>
<translation type="unfinished"></translation>
</message>
+1
View File
@@ -531,6 +531,7 @@ REG_END2
__forceinline bool IsOpaque() const { return ((A == B || (C == 2 && FIX == 0)) && D == 0) || (A == 0 && B == D && C == 2 && FIX == 0x80); }
__forceinline bool IsOpaque(int amin, int amax) const { return ((A == B || amax == 0) && D == 0) || (A == 0 && B == D && amin == 0x80 && amax == 0x80); }
__forceinline bool IsCd() const { return (A == B) && (D == 1); }
__forceinline bool IsAdditive() const { return (A == 0 && B == 2 && (C != 2 || FIX != 0) && D == 1); }
// output will be Cd, Cs is discarded
__forceinline bool IsCdOutput() const { return (C == 2 && D != 1 && FIX == 0x00); }
+62 -65
View File
@@ -373,7 +373,6 @@ void GSState::ResetDrawBufferIdx()
void GSState::ResetDrawBuffers()
{
m_used_buffers_idx = 1;
m_max_vertex_count = 0;
for (int i = 0; i < MAX_DRAW_BUFFERS; i++)
{
@@ -384,9 +383,9 @@ void GSState::ResetDrawBuffers()
m_index = &m_index_buffers[i];
m_vertex = &m_vertex_buffers[i];
m_vertex_buffers[i].head = m_vertex_buffers[i].tail = m_vertex_buffers[i].next = 0;
GrowVertexBuffer();
}
GrowVertexBuffer();
ResetDrawBufferIdx();
}
@@ -477,14 +476,12 @@ void GSState::PushBuffer()
GSIndexBuff& idx_buff = *m_index;
const u32 base = m_vertex_buffers[m_current_buffer_idx].head;
u32 copy_amt = m_vertex_buffers[m_current_buffer_idx].tail - base;
const u32 copy_amt = m_vertex_buffers[m_current_buffer_idx].tail - base;
m_vertex->tail = 0;
if (copy_amt && m_env.PRIM.PRIM == m_env_buffers[m_current_buffer_idx].m_env.PRIM.PRIM)
if (copy_amt)
memcpy(vtx_buff.buff, &m_vertex_buffers[m_current_buffer_idx].buff[base], sizeof(GSVertex) * copy_amt);
else
copy_amt = 0;
vtx_buff.head = 0;
vtx_buff.next = 0;
@@ -641,27 +638,20 @@ bool GSState::CanBufferNewDraw()
GSVertexBuff& vtx_buff = *m_vertex;
GSIndexBuff& idx_buff = *m_index;
const u32 copy_amt = m_vertex_buffers[m_current_buffer_idx].tail - m_vertex_buffers[m_current_buffer_idx].head;
m_recent_buffer_switch = vtx_buff.tail == vtx_buff.head;
if (idx_buff.tail)
vtx_buff.tail = std::max(idx_buff.buff[idx_buff.tail - 1], idx_buff.buff[std::max(static_cast<int>(idx_buff.tail) - 2, 0)]) + 1;
vtx_buff.tail = idx_buff.buff[idx_buff.tail - 1] + 1;
else
vtx_buff.tail = 0;
vtx_buff.head = vtx_buff.tail;
vtx_buff.next = vtx_buff.head;
u32 copy_amt = m_vertex_buffers[m_current_buffer_idx].tail - m_vertex_buffers[m_current_buffer_idx].head;
if (copy_amt && m_env_buffers[i].m_env.PRIM.PRIM == m_env_buffers[m_current_buffer_idx].m_env.PRIM.PRIM)
{
if (copy_amt)
memcpy(&vtx_buff.buff[vtx_buff.tail], &m_vertex_buffers[m_current_buffer_idx].buff[m_vertex_buffers[m_current_buffer_idx].head], sizeof(GSVertex) * copy_amt);
vtx_buff.tail += copy_amt;
}
else
copy_amt = 0;
m_recent_buffer_switch = vtx_buff.tail == vtx_buff.head;
vtx_buff.head = vtx_buff.tail;
vtx_buff.next = vtx_buff.head;
vtx_buff.tail += copy_amt;
m_backed_up_ctx = m_env_buffers[i].m_backed_up_ctx;
temp_draw_rect = m_env_buffers[i].draw_rect;
m_env_buffers[i].m_dirty_regs = 0;
@@ -688,6 +678,7 @@ bool GSState::CanBufferNewDraw()
vtx_buff.xy_tail = 0;
m_current_buffer_idx = i;
}
m_dirty_gs_regs = 0;
@@ -2536,8 +2527,7 @@ void GSState::FlushPrim()
if (tail > head)
{
// NOTE: Do not use PRIM->PRIM as the previous environment may have been restored and it may have changed from strip to fan, which messes up the backup.
switch (m_env.PRIM.PRIM)
switch (PRIM->PRIM)
{
case GS_POINTLIST:
pxAssert(0);
@@ -2677,7 +2667,7 @@ void GSState::FlushPrim()
// If it's a Triangle fan the XY buffer needs to be updated to point to the correct head vert
// Jak 3 shadows get spikey (with autoflush) if you don't.
if (m_env.PRIM.PRIM == GS_TRIANGLEFAN)
if (PRIM->PRIM == GS_TRIANGLEFAN)
{
for (u32 i = 0; i < unused; i++)
{
@@ -3827,49 +3817,56 @@ void GSState::UpdateVertexKick()
m_fpGIFPackedRegHandlersC[GIF_REG_STQRGBAXYZ2] = m_fpGIFPackedRegHandlerSTQRGBAXYZ2[prim];
}
static void GrowBuffer(void** pbuff, size_t old_size, size_t new_size, bool vertices)
{
void* new_buff = _aligned_malloc(new_size, 32);
if (!new_buff)
{
Console.Error("GS: failed to allocate %zu bytes for %s.", new_size, vertices ? "vertices" : "indices");
pxFailRel("Memory allocation failed");
}
if (*pbuff)
{
if (old_size)
{
std::memcpy(new_buff, *pbuff, old_size);
}
_aligned_free(*pbuff);
}
*pbuff = new_buff;
}
template <typename T>
static void GrowBuffer(T** pbuff, size_t old_count, size_t new_count)
{
static_assert(std::is_same_v<T, GSVertex> || std::is_same_v<T, u16>, "For use with indices or vertices");
GrowBuffer(reinterpret_cast<void**>(pbuff), old_count * sizeof(T), new_count * sizeof(T), std::is_same_v<T, GSVertex>);
}
void GSState::GrowVertexBuffer()
{
const u32 maxcount = std::max<u32>(m_max_vertex_count * 3 / 2, 10000);
const u32 new_vertex_count = maxcount;
const u32 new_index_count = maxcount * 6; // Worst case index list is a list of points with vs expansion, 6 indices per point
const u32 maxcount = std::max<u32>(m_vertex->maxcount * 3 / 2, 10000);
const u32 old_vertex_size = sizeof(GSVertex) * m_vertex->tail;
const u32 new_vertex_size = sizeof(GSVertex) * maxcount;
const u32 old_index_size = sizeof(u16) * m_index->tail;
const u32 new_index_size = sizeof(u16) * maxcount * 6; // Worst case index list is a list of points with vs expansion, 6 indices per point
for (GSVertexBuff& buf : m_vertex_buffers)
// Structure describing buffers to reallocate
struct AllocDesc
{
GrowBuffer(&buf.buff, buf.tail, new_vertex_count);
GrowBuffer(&buf.buff_copy, 0, new_vertex_count);
}
for (GSIndexBuff& buf : m_index_buffers)
GrowBuffer(&buf.buff, buf.tail, new_index_count);
GrowBuffer(&m_draw_vertex.buff, m_vertex->tail, new_vertex_count);
GrowBuffer(&m_draw_index.buff, m_index->tail, new_index_count);
void** pbuff;
u32 old_size;
u32 new_size;
};
const std::array<AllocDesc, 5> alloc_desc = {{
{reinterpret_cast<void**>(&m_vertex->buff), old_vertex_size, new_vertex_size},
// discard contents of buff_copy by setting old_size = 0
{reinterpret_cast<void**>(&m_vertex->buff_copy), 0, new_vertex_size},
{reinterpret_cast<void**>(&m_draw_vertex.buff), old_vertex_size, new_vertex_size},
{reinterpret_cast<void**>(&m_index->buff), old_index_size, new_index_size},
{reinterpret_cast<void**>(&m_draw_index.buff), old_index_size, new_index_size}
}};
m_max_vertex_count = maxcount - 3; // -3 to have some space at the end of the buffer before DrawingKick can grow it
// For logging
u32 total_size = 0;
for (const auto& desc : alloc_desc)
total_size += desc.new_size;
// Reallocate each of the needed buffers
for (const auto [pbuff, old_size, new_size] : alloc_desc)
{
void* new_buff = _aligned_malloc(new_size, 32);
if (!new_buff)
{
Console.Error("GS: failed to allocate %zu bytes for vertices and indices.", total_size);
pxFailRel("Memory allocation failed");
}
if (*pbuff)
{
if (old_size)
{
std::memcpy(new_buff, *pbuff, old_size);
}
_aligned_free(*pbuff);
}
*pbuff = new_buff;
}
m_vertex->maxcount = maxcount - 3; // -3 to have some space at the end of the buffer before DrawingKick can grow it
}
// For returning order of vertices to form a right triangle
@@ -5838,7 +5835,7 @@ __forceinline void GSState::VertexKick(u32 skip)
GSVertexBuff& vtx_buff = *m_vertex;
GSIndexBuff& idx_buff = *m_index;
static_assert(n > 0);
pxAssert(vtx_buff.tail < m_max_vertex_count + 3);
pxAssert(vtx_buff.tail < vtx_buff. maxcount + 3);
if constexpr (prim == GS_INVALID)
{
@@ -5975,7 +5972,7 @@ __forceinline void GSState::VertexKick(u32 skip)
vtx_buff.head = head + 1;
[[fallthrough]];
case GS_TRIANGLEFAN:
if (tail >= m_max_vertex_count)
if (tail >= vtx_buff.maxcount)
GrowVertexBuffer(); // in case too many vertices were skipped
break;
default:
@@ -5999,7 +5996,7 @@ __forceinline void GSState::VertexKick(u32 skip)
SetDrawBufferEnv();
}
if (tail >= m_max_vertex_count)
if (tail >= vtx_buff.maxcount)
GrowVertexBuffer();
u16* RESTRICT buff = &idx_buff.buff[idx_buff.tail];
+1 -2
View File
@@ -135,13 +135,12 @@ protected:
int m_used_buffers_idx = 0;
int m_current_buffer_idx = 0;
bool m_recent_buffer_switch = false;
u32 m_max_vertex_count;
struct GSVertexBuff
{
GSVertex* buff;
GSVertex* buff_copy; // same size buffer to copy/modify the original buffer
u32 head, tail, next; // head: first vertex, tail: last vertex + 1, next: last indexed + 1
u32 head, tail, next, maxcount; // head: first vertex, tail: last vertex + 1, next: last indexed + 1
u32 xy_tail;
GSVector4i xy[4];
GSVector4i xyhead;
+15 -7
View File
@@ -222,7 +222,9 @@ void GSRendererHW::Lines2Sprites()
// each sprite converted to quad needs twice the space
while (m_vertex->tail * 2 > m_max_vertex_count)
const int tail = m_vertex->tail * 2;
const int max_count = m_vertex->maxcount;
while (tail > max_count)
{
GrowVertexBuffer();
}
@@ -5382,7 +5384,7 @@ void GSRendererHW::HandleFlatShadedVertices()
return;
// De-index the vertices using the copy buffer
while (m_max_vertex_count < idx_buff.tail)
while (vtx_buff.maxcount < idx_buff.tail)
GrowVertexBuffer();
for (int i = static_cast<int>(idx_buff.tail) - 1; i >= 0; i--)
@@ -10707,8 +10709,10 @@ int GSRendererHW::IsScalingDraw(GSTextureCache::Source* src, bool no_gaps)
IsMipMapDraw() || GSLocalMemory::m_psm[m_cached_ctx.TEX0.PSM].trbpp <= 8)
return 0;
const bool is_smaller = draw_size.x <= (tex_size.x * 0.75f) && draw_size.y <= (tex_size.y * 0.75f);
const bool is_smaller_and_writing_back = draw_size.x < tex_size.x && draw_size.y < tex_size.y && next_ctx.FRAME.Block() == m_cached_ctx.TEX0.TBP0 && next_ctx.TEX0.TBP0 == m_cached_ctx.FRAME.Block();
// Should usually be 2x but some games like Monster House goes from 512x448 -> 128x128
const bool is_downscale = m_cached_ctx.TEX0.TBW >= m_cached_ctx.FRAME.FBW && draw_size.x <= (tex_size.x * 0.75f) && draw_size.y <= (tex_size.y * 0.75f);
const bool is_downscale = m_cached_ctx.TEX0.TBW >= m_cached_ctx.FRAME.FBW && (is_smaller || is_smaller_and_writing_back);
// Check we're getting most of the texture and not just stenciling a part of it.
// Only allow non-bilineared downscales if it's most of the target (misdetections of shadows in Naruto, Transformers etc), otherwise it's fine.
const GSVector4i src_valid = src->m_from_target ? src->m_from_target->m_valid : src->m_valid_rect;
@@ -10732,7 +10736,8 @@ int GSRendererHW::IsScalingDraw(GSTextureCache::Source* src, bool no_gaps)
}
// Last ditched check if it's doing a lot of small draws exactly the same which could be recursive lighting bloom.
if (m_vt.m_primclass == GS_SPRITE_CLASS && m_index->tail > 2 && !no_gaps_or_single_sprite && m_context->TEX1.MMAG == 1 && !m_context->ALPHA.IsOpaque())
if (m_vt.m_primclass == GS_SPRITE_CLASS && m_index->tail > 2 && !no_gaps_or_single_sprite && m_context->TEX1.MMAG == 1 && is_target_src && (m_cached_ctx.TEST.ZTST == ZTST_ALWAYS || m_cached_ctx.TEST.ZTE == 0) &&
!m_context->ALPHA.IsOpaque() && GSLocalMemory::m_psm[m_cached_ctx.FRAME.PSM].bpp == GSLocalMemory::m_psm[m_cached_ctx.TEX0.PSM].bpp)
{
GSVertex* v = &m_vertex->buff[0];
float tw = 1 << src->m_TEX0.TW;
@@ -10743,7 +10748,10 @@ int GSRendererHW::IsScalingDraw(GSTextureCache::Source* src, bool no_gaps)
const int first_x = (v[1].XYZ.X - v[0].XYZ.X) >> 4;
const int first_y = (v[1].XYZ.Y - v[0].XYZ.Y) >> 4;
if (first_x > first_u && first_y > first_v && !no_resize && std::abs(draw_size.x - first_x) <= 4 && std::abs(draw_size.y - first_y) <= 4)
const u32 half_color_mask = (m_vt.m_min.c == GSVector4i(64, 64, 64, 64)).mask() & 0xfff;
const bool possible_fake_bilinear = no_resize && PRIM->ABE && m_context->ALPHA.IsAdditive() && m_cached_ctx.TEX0.TFX == TFX_MODULATE && m_vt.m_eq.rgba && half_color_mask == 0xfff;
if (first_x >= first_u && first_y >= first_v && (!no_resize || possible_fake_bilinear) && ((draw_size.x != first_x && draw_size.y != first_y) || (tex_size.x != first_u && tex_size.y != first_v)) && std::abs(draw_size.x - first_x) <= 4 && std::abs(draw_size.y - first_y) <= 4)
{
for (u32 i = 2; i < m_index->tail; i += 2)
{
@@ -10752,14 +10760,14 @@ int GSRendererHW::IsScalingDraw(GSTextureCache::Source* src, bool no_gaps)
const int next_x = (v[i + 1].XYZ.X - v[i].XYZ.X) >> 4;
const int next_y = (v[i + 1].XYZ.Y - v[i].XYZ.Y) >> 4;
if (std::abs(draw_size.x - next_x) > 4 || std::abs(draw_size.y - next_y) > 4)
if (std::abs(draw_size.x - next_x) > 4 || std::abs(draw_size.y - next_y) > 4 || (draw_size.x == next_x && tex_size.x == next_u) || (draw_size.y == next_y && tex_size.y == next_v))
break;
if (next_u != first_u || next_v != first_v || next_x != first_x || next_y != first_y)
break;
if (i + 2 >= m_index->tail)
return 2;
return (is_target_src && src->m_from_target->m_downscaled) ? -1 : 2;
}
}
}
+1 -1
View File
@@ -320,7 +320,7 @@ void GSRendererSW::RewriteVerticesIfSTOverflow()
constexpr int n = GSUtil::GetClassVertexCount(primclass);
// Make sure the copy buffer is large enough.
while (m_max_vertex_count < m_index->tail)
while (m_vertex->maxcount < m_index->tail)
GrowVertexBuffer();
GSVertex* RESTRICT vertex = m_vertex->buff;
+5 -9
View File
@@ -1657,11 +1657,9 @@ VkRenderPass GSDeviceVK::CreateCachedRenderPass(RenderPassCacheKey key)
if (key.depth_format != VK_FORMAT_UNDEFINED)
{
const VkImageLayout layout =
key.depth_sampling ?
((m_features.depth_feedback && UseFeedbackLoopLayout()) ?
VK_IMAGE_LAYOUT_ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT :
VK_IMAGE_LAYOUT_GENERAL) :
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
key.depth_sampling ? (UseFeedbackLoopLayout() ? VK_IMAGE_LAYOUT_ATTACHMENT_FEEDBACK_LOOP_OPTIMAL_EXT :
VK_IMAGE_LAYOUT_GENERAL) :
VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
attachments[num_attachments] = {0, static_cast<VkFormat>(key.depth_format), VK_SAMPLE_COUNT_1_BIT,
static_cast<VkAttachmentLoadOp>(key.depth_load_op), static_cast<VkAttachmentStoreOp>(key.depth_store_op),
static_cast<VkAttachmentLoadOp>(key.stencil_load_op),
@@ -3759,14 +3757,12 @@ void GSDeviceVK::OMSetRenderTargets(
}
else if (feedback_loop & FeedbackLoopFlag_ReadDepth)
{
const GSTextureVK::Layout layout = m_features.depth_feedback ?
GSTextureVK::Layout::FeedbackLoop : GSTextureVK::Layout::General;
if (vkDs->GetLayout() != layout)
if (vkDs->GetLayout() != GSTextureVK::Layout::FeedbackLoop)
{
m_dirty_flags |= (DIRTY_FLAG_TFX_TEXTURE_0 << TFX_TEXTURE_TEXTURE);
if (m_tfx_textures[TFX_TEXTURE_TEXTURE] == vkDs)
m_dirty_flags |= DIRTY_FLAG_TFX_TEXTURE_0 << TFX_TEXTURE_TEXTURE;
vkDs->TransitionToLayout(layout);
vkDs->TransitionToLayout(GSTextureVK::Layout::FeedbackLoop);
}
}
else