From 3e393d99e142863fc8d2877e583328cb888ce7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 14 Jul 2026 14:15:32 +0200 Subject: [PATCH 1/5] GeDebugger: Add some sanity checks in the vertex preview to avoid crashes --- GPU/Common/SoftwareTransformCommon.cpp | 2 +- GPU/Debugger/State.cpp | 2 + UI/ImDebugger/ImGe.cpp | 71 ++++++++++++++++++++------ 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/GPU/Common/SoftwareTransformCommon.cpp b/GPU/Common/SoftwareTransformCommon.cpp index a84bbcd793..605ba87481 100644 --- a/GPU/Common/SoftwareTransformCommon.cpp +++ b/GPU/Common/SoftwareTransformCommon.cpp @@ -1532,7 +1532,7 @@ bool GetCurrentDrawAsDebugVertices(DrawEngineCommon *drawEngine, GECommand cmd, indexLowerBound = 0; } - int verticesToDecode = indexUpperBound + 1 - indexLowerBound; + const int verticesToDecode = indexUpperBound + 1 - indexLowerBound; const u8 *verts = Memory::GetPointerUnchecked(gstate_c.vertexAddr); diff --git a/GPU/Debugger/State.cpp b/GPU/Debugger/State.cpp index 1ed9e6e390..32ab3746e1 100644 --- a/GPU/Debugger/State.cpp +++ b/GPU/Debugger/State.cpp @@ -815,6 +815,8 @@ bool GetPrimPreview(u32 op, GEPrimitiveType *prim, std::vector * int count = 0; + *lowerIndexBound = 0; + const GECommand cmd = static_cast(op >> 24); if (cmd == GE_CMD_PRIM) { *prim = static_cast((op >> 16) & 0x7); // irrelevant for bbox diff --git a/UI/ImDebugger/ImGe.cpp b/UI/ImDebugger/ImGe.cpp index 00b3fecd65..ad17c199a0 100644 --- a/UI/ImDebugger/ImGe.cpp +++ b/UI/ImDebugger/ImGe.cpp @@ -959,9 +959,16 @@ static void DrawPreviewPrimitive(ImDrawList *drawList, ImVec2 p0, GECommand cmd, case GE_PRIM_TRIANGLES: { for (int i = 0; i < count - 2; i += 3) { - const auto &v1 = indices.empty() ? verts[i + 0] : verts[indices[indexOffset + i + 0]]; - const auto &v2 = indices.empty() ? verts[i + 1] : verts[indices[indexOffset + i + 1]]; - const auto &v3 = indices.empty() ? verts[i + 2] : verts[indices[indexOffset + i + 2]]; + const int index0 = indices.empty() ? (i + 0) : indices[indexOffset + i + 0]; + const int index1 = indices.empty() ? (i + 1) : indices[indexOffset + i + 1]; + const int index2 = indices.empty() ? (i + 2) : indices[indexOffset + i + 2]; + + if (index0 >= (int)verts.size() || index1 >= (int)verts.size() || index2 >= (int)verts.size()) { + continue; + } + const GPUDebugVertex &v1 = verts[index0]; + const GPUDebugVertex &v2 = verts[index1]; + const GPUDebugVertex &v3 = verts[index2]; drawList->AddTriangleFilled( ImVec2(p0.x + x(v1), p0.y + y(v1)), ImVec2(p0.x + x(v2), p0.y + y(v2)), @@ -972,8 +979,14 @@ static void DrawPreviewPrimitive(ImDrawList *drawList, ImVec2 p0, GECommand cmd, case GE_PRIM_RECTANGLES: { for (int i = 0; i < count - 2; i += 3) { - const auto &tl = indices.empty() ? verts[i] : verts[indices[indexOffset + i]]; - const auto &br = indices.empty() ? verts[i + 1] : verts[indices[indexOffset + i + 1]]; + const int indexTL = indices.empty() ? (i + 0) : indices[indexOffset + i + 0]; + const int indexBR = indices.empty() ? (i + 1) : indices[indexOffset + i + 1]; + if (indexTL >= (int)verts.size() || indexBR >= (int)verts.size()) { + continue; + } + + const GPUDebugVertex &tl = verts[indexTL]; + const GPUDebugVertex & br = verts[indexBR]; drawList->AddRectFilled( ImVec2(p0.x + x(tl), p0.y + y(tl)), ImVec2(p0.x + x(br), p0.y + y(br)), ImColor(defaultColor)); @@ -983,9 +996,17 @@ static void DrawPreviewPrimitive(ImDrawList *drawList, ImVec2 p0, GECommand cmd, case GE_PRIM_TRIANGLE_FAN: { for (int i = 0; i < count - 2; i++) { - const auto &v1 = indices.empty() ? verts[0] : verts[indices[indexOffset + 0]]; - const auto &v2 = indices.empty() ? verts[i + 1] : verts[indices[indexOffset + i + 1]]; - const auto &v3 = indices.empty() ? verts[i + 2] : verts[indices[indexOffset + i + 2]]; + const int index0 = indices.empty() ? 0 : indices[indexOffset + 0]; + const int index1 = indices.empty() ? (i + 1) : indices[indexOffset + i + 1]; + const int index2 = indices.empty() ? (i + 2) : indices[indexOffset + i + 2]; + + if (index0 >= (int)verts.size() || index1 >= (int)verts.size() || index2 >= (int)verts.size()) { + continue; + } + + const GPUDebugVertex &v1 = verts[index0]; + const GPUDebugVertex &v2 = verts[index1]; + const GPUDebugVertex &v3 = verts[index2]; drawList->AddTriangleFilled( ImVec2(p0.x + x(v1), p0.y + y(v1)), ImVec2(p0.x + x(v2), p0.y + y(v2)), @@ -1000,9 +1021,15 @@ static void DrawPreviewPrimitive(ImDrawList *drawList, ImVec2 p0, GECommand cmd, int i0 = i; int i1 = i + t; int i2 = i + (t ^ 3); - const auto &v1 = indices.empty() ? verts[i0] : verts[indices[indexOffset + i0]]; - const auto &v2 = indices.empty() ? verts[i1] : verts[indices[indexOffset + i1]]; - const auto &v3 = indices.empty() ? verts[i2] : verts[indices[indexOffset + i2]]; + const int index0 = indices.empty() ? i0 : indices[indexOffset + i0]; + const int index1 = indices.empty() ? i1 : indices[indexOffset + i1]; + const int index2 = indices.empty() ? i2 : indices[indexOffset + i2]; + if (index0 >= (int)verts.size() || index1 >= (int)verts.size() || index2 >= (int)verts.size()) { + continue; + } + const GPUDebugVertex &v1 = verts[index0]; + const GPUDebugVertex &v2 = verts[index1]; + const GPUDebugVertex &v3 = verts[index2]; drawList->AddTriangleFilled( ImVec2(p0.x + x(v1), p0.y + y(v1)), ImVec2(p0.x + x(v2), p0.y + y(v2)), @@ -1014,8 +1041,13 @@ static void DrawPreviewPrimitive(ImDrawList *drawList, ImVec2 p0, GECommand cmd, case GE_PRIM_LINES: { for (int i = 0; i < count - 1; i += 2) { - const auto &v1 = indices.empty() ? verts[i] : verts[indices[indexOffset + i]]; - const auto &v2 = indices.empty() ? verts[i + 1] : verts[indices[indexOffset + i + 1]]; + const int index0 = indices.empty() ? (i + 0) : indices[indexOffset + i + 0]; + const int index1 = indices.empty() ? (i + 1) : indices[indexOffset + i + 1]; + if (index0 >= (int)verts.size() || index1 >= (int)verts.size()) { + continue; + } + const GPUDebugVertex &v1 = verts[index0]; + const GPUDebugVertex &v2 = verts[index1]; drawList->AddLine( ImVec2(p0.x + x(v1), p0.y + y(v1)), ImVec2(p0.x + x(v2), p0.y + y(v2)), ImColor(defaultColor)); @@ -1025,8 +1057,13 @@ static void DrawPreviewPrimitive(ImDrawList *drawList, ImVec2 p0, GECommand cmd, case GE_PRIM_LINE_STRIP: { for (int i = 0; i < count - 2; i++) { - const auto &v1 = indices.empty() ? verts[i] : verts[indices[indexOffset + i]]; - const auto &v2 = indices.empty() ? verts[i + 1] : verts[indices[indexOffset + i + 1]]; + const int index0 = indices.empty() ? (i + 0) : indices[indexOffset + i + 0]; + const int index1 = indices.empty() ? (i + 1) : indices[indexOffset + i + 1]; + if (index0 >= (int)verts.size() || index1 >= (int)verts.size()) { + continue; + } + const GPUDebugVertex &v1 = verts[index0]; + const GPUDebugVertex &v2 = verts[index1]; drawList->AddLine( ImVec2(p0.x + x(v1), p0.y + y(v1)), ImVec2(p0.x + x(v2), p0.y + y(v2)), ImColor(defaultColor)); @@ -1848,7 +1885,9 @@ void DrawImGeVertsWindow(ImConfig &cfg, ImControl &control, GPUCommon *gpu) { for (int column = 0; column < colCount; column++) { ImGui::TableNextColumn(); char temp[36]; - if (transformed) { + if ((size_t)index >= vertices.size()) { + snprintf(temp, sizeof(temp), "(%d: idx out of range)", index); + } else if (transformed) { FormatVertColTransformed(temp, sizeof(temp), vertices[index], (VertexListTransformedCol)column); } else { FormatVertColDecoded(temp, sizeof(temp), vertices[index], (VertexListDecodedCol)column); From 18a9de3eca2ffe31d19974c8849dbdada6b67fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 14 Jul 2026 15:28:21 +0200 Subject: [PATCH 2/5] Move AdvanceVerts to GPUStateCache --- GPU/GPUCommon.cpp | 2 +- GPU/GPUCommon.h | 9 --------- GPU/GPUCommonHW.cpp | 22 ++++++++++++---------- GPU/GPUState.h | 10 ++++++++++ GPU/Software/SoftGpu.cpp | 28 ++++++++++++++++------------ 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/GPU/GPUCommon.cpp b/GPU/GPUCommon.cpp index 7babc15025..4f748f885a 100644 --- a/GPU/GPUCommon.cpp +++ b/GPU/GPUCommon.cpp @@ -1243,7 +1243,7 @@ void GPUCommon::Execute_BoundingBox(u32 op, u32 diff) { // This is the normal case that pretty much always happens, the others are esoteric. currentList->bboxResult = drawEngineCommon_->TestBoundingBox(control_points, inds, count, dec, vertType); } - AdvanceVerts(gstate.vertType, count, bytesRead); + gstate_c.AdvanceVerts(vertType, count, bytesRead); } void GPUCommon::Execute_MorphWeight(u32 op, u32 diff) { diff --git a/GPU/GPUCommon.h b/GPU/GPUCommon.h index af1553be34..9372dcee0d 100644 --- a/GPU/GPUCommon.h +++ b/GPU/GPUCommon.h @@ -320,15 +320,6 @@ protected: // TODO: Unify this. Vulkan and OpenGL are different due to how they buffer data. virtual void FinishDeferred() {} - void AdvanceVerts(u32 vertType, int count, int bytesRead) { - if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { - const int indexShift = ((vertType & GE_VTYPE_IDX_MASK) >> GE_VTYPE_IDX_SHIFT) - 1; - gstate_c.indexAddr += count << indexShift; - } else { - gstate_c.vertexAddr += bytesRead; - } - } - virtual void BuildReportingInfo() = 0; virtual void UpdateMSAALevel(Draw::DrawContext *draw) {} diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index fa0f8c2545..9689295011 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -1051,7 +1051,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) { // After drawing, we advance the vertexAddr (when non indexed) or indexAddr (when indexed). // Some games rely on this, they don't bother reloading VADDR and IADDR. // The VADDR/IADDR registers are NOT updated. - AdvanceVerts(vertexType, count, bytesRead); + gstate_c.AdvanceVerts(vertexType, count, bytesRead); int totalVertCount = count; @@ -1132,7 +1132,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) { drawEngineCommon_->SkipPrim(newPrim, count, decoder, &bytesRead); canExtend = false; } - AdvanceVerts(vertexType, count, bytesRead); + gstate_c.AdvanceVerts(vertexType, count, bytesRead); totalVertCount += count; break; } @@ -1301,7 +1301,8 @@ void GPUCommonHW::Execute_Bezier(u32 op, u32 diff) { const void *control_points = Memory::GetPointerUnchecked(gstate_c.vertexAddr); const void *indices = NULL; - if ((gstate.vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { + const u32 vertType = gstate.vertType; + if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { if (!Memory::IsValidAddress(gstate_c.indexAddr)) { ERROR_LOG(Log::G3D, "Bad index address %08x!", gstate_c.indexAddr); return; @@ -1309,8 +1310,8 @@ void GPUCommonHW::Execute_Bezier(u32 op, u32 diff) { indices = Memory::GetPointerUnchecked(gstate_c.indexAddr); } - if (vertTypeIsSkinningEnabled(gstate.vertType)) { - DEBUG_LOG_REPORT(Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", gstate.vertType, (gstate.vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(gstate.vertType)); + if (vertTypeIsSkinningEnabled(vertType)) { + DEBUG_LOG_REPORT(Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", vertType, (vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(vertType)); } // Can't flush after setting gstate_c.submitType below since it'll be a mess - it must be done already. @@ -1341,7 +1342,7 @@ void GPUCommonHW::Execute_Bezier(u32 op, u32 diff) { // After drawing, we advance pointers - see SubmitPrim which does the same. const int count = surface.num_points_u * surface.num_points_v; - AdvanceVerts(gstate.vertType, count, bytesRead); + gstate_c.AdvanceVerts(vertType, count, bytesRead); } void GPUCommonHW::Execute_Spline(u32 op, u32 diff) { @@ -1370,7 +1371,8 @@ void GPUCommonHW::Execute_Spline(u32 op, u32 diff) { const void *control_points = Memory::GetPointerUnchecked(gstate_c.vertexAddr); const void *indices = NULL; - if ((gstate.vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { + const u32 vertType = gstate.vertType; + if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { if (!Memory::IsValidAddress(gstate_c.indexAddr)) { ERROR_LOG(Log::G3D, "Bad index address %08x!", gstate_c.indexAddr); return; @@ -1378,8 +1380,8 @@ void GPUCommonHW::Execute_Spline(u32 op, u32 diff) { indices = Memory::GetPointerUnchecked(gstate_c.indexAddr); } - if (vertTypeIsSkinningEnabled(gstate.vertType)) { - WARN_LOG_ONCE(unusualcurve, Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", gstate.vertType, (gstate.vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(gstate.vertType)); + if (vertTypeIsSkinningEnabled(vertType)) { + WARN_LOG_ONCE(unusualcurve, Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", vertType, (vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(vertType)); } // Can't flush after setting gstate_c.submitType below since it'll be a mess - it must be done already. @@ -1412,7 +1414,7 @@ void GPUCommonHW::Execute_Spline(u32 op, u32 diff) { // After drawing, we advance pointers - see SubmitPrim which does the same. int count = surface.num_points_u * surface.num_points_v; - AdvanceVerts(gstate.vertType, count, bytesRead); + gstate_c.AdvanceVerts(vertType, count, bytesRead); } void GPUCommonHW::Execute_BlockTransferStart(u32 op, u32 diff) { diff --git a/GPU/GPUState.h b/GPU/GPUState.h index 845579a69a..48959bdfe8 100644 --- a/GPU/GPUState.h +++ b/GPU/GPUState.h @@ -534,6 +534,16 @@ struct GPUStateCache { bool IsDirty(u64 what) const { return (dirty & what) != 0ULL; } + + void AdvanceVerts(u32 vertType, int count, int bytesRead) { + if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { + const int indexShift = ((vertType & GE_VTYPE_IDX_MASK) >> GE_VTYPE_IDX_SHIFT) - 1; + indexAddr += count << indexShift; + } else { + vertexAddr += bytesRead; + } + } + void SetTextureSolidAlpha(bool solidAlpha) { if (solidAlpha != textureSolidAlpha) { textureSolidAlpha = solidAlpha; diff --git a/GPU/Software/SoftGpu.cpp b/GPU/Software/SoftGpu.cpp index 336df8343c..14e8c9b448 100644 --- a/GPU/Software/SoftGpu.cpp +++ b/GPU/Software/SoftGpu.cpp @@ -853,7 +853,9 @@ void SoftGPU::Execute_Prim(u32 op, u32 diff) { const void *verts = Memory::GetPointerUnchecked(gstate_c.vertexAddr); const void *indices = NULL; - if ((gstate.vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { + + const u32 vertType = gstate.vertType; + if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { if (!Memory::IsValidAddress(gstate_c.indexAddr)) { ERROR_LOG_REPORT(Log::G3D, "Software: Bad index address %08x!", gstate_c.indexAddr); return; @@ -864,7 +866,7 @@ void SoftGPU::Execute_Prim(u32 op, u32 diff) { cyclesExecuted += EstimatePerVertexCost() * count; int bytesRead; drawEngine_->transformUnit.SetDirty(dirtyFlags_); - drawEngine_->transformUnit.SubmitPrimitive(verts, indices, prim, count, gstate.vertType, &bytesRead, drawEngine_); + drawEngine_->transformUnit.SubmitPrimitive(verts, indices, prim, count, vertType, &bytesRead, drawEngine_); dirtyFlags_ = drawEngine_->transformUnit.GetDirty(); SoftGPUVRAMDirty mark = (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) != 0 ? SoftGPUVRAMDirty::DIRTY : SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY; @@ -873,7 +875,7 @@ void SoftGPU::Execute_Prim(u32 op, u32 diff) { // After drawing, we advance the vertexAddr (when non indexed) or indexAddr (when indexed). // Some games rely on this, they don't bother reloading VADDR and IADDR. // The VADDR/IADDR registers are NOT updated. - AdvanceVerts(gstate.vertType, count, bytesRead); + gstate_c.AdvanceVerts(vertType, count, bytesRead); } void SoftGPU::Execute_Bezier(u32 op, u32 diff) { @@ -890,7 +892,8 @@ void SoftGPU::Execute_Bezier(u32 op, u32 diff) { const void *control_points = Memory::GetPointerUnchecked(gstate_c.vertexAddr); const void *indices = NULL; - if ((gstate.vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { + const u32 vertType = gstate.vertType; + if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { if (!Memory::IsValidAddress(gstate_c.indexAddr)) { ERROR_LOG_REPORT(Log::G3D, "Bad index address %08x!", gstate_c.indexAddr); return; @@ -898,8 +901,8 @@ void SoftGPU::Execute_Bezier(u32 op, u32 diff) { indices = Memory::GetPointerUnchecked(gstate_c.indexAddr); } - if ((gstate.vertType & GE_VTYPE_MORPHCOUNT_MASK) || vertTypeIsSkinningEnabled(gstate.vertType)) { - DEBUG_LOG_REPORT(Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", gstate.vertType, (gstate.vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(gstate.vertType)); + if ((vertType & GE_VTYPE_MORPHCOUNT_MASK) || vertTypeIsSkinningEnabled(vertType)) { + DEBUG_LOG_REPORT(Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", vertType, (vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(vertType)); } Spline::BezierSurface surface; @@ -924,7 +927,7 @@ void SoftGPU::Execute_Bezier(u32 op, u32 diff) { // After drawing, we advance pointers - see SubmitPrim which does the same. int count = surface.num_points_u * surface.num_points_v; - AdvanceVerts(gstate.vertType, count, bytesRead); + gstate_c.AdvanceVerts(vertType, count, bytesRead); } void SoftGPU::Execute_Spline(u32 op, u32 diff) { @@ -941,7 +944,8 @@ void SoftGPU::Execute_Spline(u32 op, u32 diff) { const void *control_points = Memory::GetPointerUnchecked(gstate_c.vertexAddr); const void *indices = NULL; - if ((gstate.vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { + const u32 vertType = gstate.vertType; + if ((vertType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) { if (!Memory::IsValidAddress(gstate_c.indexAddr)) { ERROR_LOG_REPORT(Log::G3D, "Bad index address %08x!", gstate_c.indexAddr); return; @@ -949,8 +953,8 @@ void SoftGPU::Execute_Spline(u32 op, u32 diff) { indices = Memory::GetPointerUnchecked(gstate_c.indexAddr); } - if ((gstate.vertType & GE_VTYPE_MORPHCOUNT_MASK) || vertTypeIsSkinningEnabled(gstate.vertType)) { - DEBUG_LOG_REPORT(Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", gstate.vertType, (gstate.vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(gstate.vertType)); + if ((vertType & GE_VTYPE_MORPHCOUNT_MASK) || vertTypeIsSkinningEnabled(vertType)) { + DEBUG_LOG_REPORT(Log::G3D, "Unusual bezier/spline vtype: %08x, morph: %d, bones: %d", vertType, (vertType & GE_VTYPE_MORPHCOUNT_MASK) >> GE_VTYPE_MORPHCOUNT_SHIFT, vertTypeGetNumBoneWeights(vertType)); } Spline::SplineSurface surface; @@ -969,7 +973,7 @@ void SoftGPU::Execute_Spline(u32 op, u32 diff) { int bytesRead = 0; drawEngine_->transformUnit.SetDirty(dirtyFlags_); - drawEngineCommon_->SubmitCurve(control_points, indices, surface, gstate.vertType, &bytesRead, "spline"); + drawEngineCommon_->SubmitCurve(control_points, indices, surface, vertType, &bytesRead, "spline"); dirtyFlags_ = drawEngine_->transformUnit.GetDirty(); SoftGPUVRAMDirty mark = (gstate_c.skipDrawReason & SKIPDRAW_SKIPFRAME) != 0 ? SoftGPUVRAMDirty::DIRTY : SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY; @@ -977,7 +981,7 @@ void SoftGPU::Execute_Spline(u32 op, u32 diff) { // After drawing, we advance pointers - see SubmitPrim which does the same. int count = surface.num_points_u * surface.num_points_v; - AdvanceVerts(gstate.vertType, count, bytesRead); + gstate_c.AdvanceVerts(vertType, count, bytesRead); } void SoftGPU::Execute_LoadClut(u32 op, u32 diff) { From a655278dc8d9801bd923bb8e9304b66fa9263c53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 14 Jul 2026 15:29:55 +0200 Subject: [PATCH 3/5] Fix vertex/index advance with through-mode 2D culling. Fixes Naruto 3 Fixes #21919 --- GPU/GPUCommonHW.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index 9689295011..1330f1aff7 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -1002,8 +1002,8 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) { int cycles = vertexCost_ * count; gpuStats.perFrame.vertexGPUCycles += cycles; cyclesExecuted += cycles; - // NOTE! We still have to advance vertex pointers! - gstate_c.vertexAddr += bytesRead; // We know from the above check that it's not an indexed draw. + // We still have to advance vertex/index pointers! + gstate_c.AdvanceVerts(vertexType, count, bytesRead); return; } } From 219c97c53188078bb3d4fa6ed53a7a9a9bb17cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 14 Jul 2026 15:32:25 +0200 Subject: [PATCH 4/5] Apply sprite fix in Naturo Shippuden: UNH 3 --- assets/compat.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/compat.ini b/assets/compat.ini index 6349174115..83ff247ea7 100644 --- a/assets/compat.ini +++ b/assets/compat.ini @@ -2161,6 +2161,9 @@ ULUS10495 = -0.5 # This works in the provided dump, but needs more testing. NPJH50878 = -0.4 +# Naruto Shippuden: Ultimate Ninja Heroes 3 +ULES01407 = -0.5 + [TextureCLUTInShader] # Fushigi no Dungeon 4. See #15251. This drastically reduces the amount of textures decoded by the game, # but due to lots of tile modifications the number is still pretty high. From 4d018cd7e5ccb0e0bbcf9e53ab5ea9145ffabc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 14 Jul 2026 15:44:17 +0200 Subject: [PATCH 5/5] Fix back behavior in GameScreen Fixes #21884 --- UI/GameScreen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/GameScreen.cpp b/UI/GameScreen.cpp index 068c603c84..d7c55d218c 100644 --- a/UI/GameScreen.cpp +++ b/UI/GameScreen.cpp @@ -181,7 +181,7 @@ template std::string int2hexstr(I w, size_t hex_len = sizeof(I) << } void GameScreen::update() { - UIScreen::update(); + UITwoPaneBaseDialogScreen::update(); GameInfoFlags hasFlags; g_gameInfoCache->GetInfo(NULL, gamePath_, g_desiredFlags, &hasFlags);