From 834e617cc15b198c53ff7895b1ca0dffb2efb106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 3 Oct 2026 09:08:21 -0600 Subject: [PATCH] SoftGPU: Carry UVs and normals into formats without them as the GE does gpu/vertices/carry, recorded on a PSP: a draw whose format has no UVs uses the last ones read, by any earlier draw in draw order, textured or not. They're kept as read and get the scale and offset of the draw that uses them. Normals carry over the same way. We kept the UVs after scaling, only read them with texturing on, and after a cached indexed draw carried the highest vertex instead of the last one drawn. The carried values move from statics in ReadVertex to TransformUnit. Co-Authored-By: Claude Opus 5.5 (1M context) --- GPU/Software/TransformUnit.cpp | 52 +++++++++++++++++++--------------- GPU/Software/TransformUnit.h | 8 +++++- test.py | 1 + 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/GPU/Software/TransformUnit.cpp b/GPU/Software/TransformUnit.cpp index 9f9ca1c6c2..8282ec4ca7 100644 --- a/GPU/Software/TransformUnit.cpp +++ b/GPU/Software/TransformUnit.cpp @@ -274,7 +274,6 @@ struct TransformState { bool enableTransform : 1; bool enableLighting : 1; bool enableFog : 1; - bool readUV : 1; bool negateNormals : 1; uint8_t uvGenMode : 2; uint8_t matrixMode : 2; @@ -295,8 +294,7 @@ void ComputeTransformState(TransformState *state, const VertexReader &vreader) { state->enableTransform = !vreader.isThrough(); state->enableLighting = gstate.isLightingEnabled(); state->enableFog = gstate.isFogEnabled(); - state->readUV = !gstate.isModeClear() && gstate.isTextureMapEnabled() && vreader.hasUV(); - state->geUVScale = !vreader.isThrough() && gstate.getUVGenMode() == GE_TEXMAP_TEXTURE_COORDS; + state->geUVScale = !vreader.isThrough() && gstate.getUVGenMode() == GE_TEXMAP_TEXTURE_COORDS && !gstate.isModeClear() && gstate.isTextureMapEnabled(); state->uvScale[0] = TruncateToFloat24(getFloat24(gstate.texscaleu)); state->uvScale[1] = TruncateToFloat24(getFloat24(gstate.texscalev)); state->uvOffset[0] = TruncateToFloat24(getFloat24(gstate.texoffsetu)); @@ -412,34 +410,32 @@ static inline float Dot43(const Vec4f &a, const Vec3f &b) { return Dot(a, Vec4f(b, 1.0f)); } -ClipVertexData TransformUnit::ReadVertex(const VertexReader &vreader, const TransformState &state) { +ClipVertexData TransformUnit::ReadVertex(const VertexReader &vreader, const TransformState &state, VertexCarry &carry) { PROFILE_THIS_SCOPE("read_vert"); - // If we ever thread this, we'll have to change this. ClipVertexData vertex; ModelCoords pos; vreader.ReadPosThrough(pos.AsArray()); - static Vec3Packedf lastTC; - if (state.readUV) { - vreader.ReadUV(vertex.v.texturecoords.AsArray()); - vertex.v.texturecoords.q() = 0.0f; - if (state.geUVScale) { - // The decoder only normalized them (8 and 16 bit UVs are unsigned). - for (int i = 0; i < 2; ++i) { - const float scaled = ProductToFloat24((double)TruncateToFloat24(vertex.v.texturecoords[i]) * state.uvScale[i]); - vertex.v.texturecoords[i] = TruncateToFloat24(GEAdd(scaled, state.uvOffset[i])); - } + // A format without UVs uses the last ones read, by any draw, textured or not. They're kept as read, and + // scaled with the scale and offset of the draw using them (gpu/vertices/carry). + if (vreader.hasUV()) { + vreader.ReadUV(carry.tc.AsArray()); + carry.tc.q() = 0.0f; + } + vertex.v.texturecoords = carry.tc; + if (state.geUVScale) { + // The decoder only normalized them (8 and 16 bit UVs are unsigned). + for (int i = 0; i < 2; ++i) { + const float scaled = ProductToFloat24((double)TruncateToFloat24(vertex.v.texturecoords[i]) * state.uvScale[i]); + vertex.v.texturecoords[i] = TruncateToFloat24(GEAdd(scaled, state.uvOffset[i])); } - lastTC = vertex.v.texturecoords; - } else { - vertex.v.texturecoords = lastTC; } - static Vec3f lastnormal; + // Carried the same way (gpu/vertices/carry). if (vreader.hasNormal()) - vreader.ReadNrm(lastnormal.AsArray()); - Vec3f normal = lastnormal; + vreader.ReadNrm(carry.normal); + Vec3f normal(carry.normal[0], carry.normal[1], carry.normal[2]); if (state.negateNormals) normal = -normal; @@ -711,6 +707,7 @@ public: SoftwareVertexReader(u8 *base, VertexDecoder &vdecoder, u32 vertex_type, int vertex_count, const void *vertices, const void *indices, const TransformState &transformState, TransformUnit &transform) : vreader_(base, vdecoder.GetDecVtxFmt(), vertex_type), conv_(vertex_type, indices), transformState_(transformState), transform_(transform) { useIndices_ = indices != nullptr; + vertexCount_ = vertex_count; lowerBound_ = 0; upperBound_ = vertex_count == 0 ? 0 : vertex_count - 1; @@ -746,9 +743,17 @@ public: if (!useCache_) return; + // Within a draw, the format has UVs and a normal for every vertex or for none, so the order the + // cache is filled in doesn't matter. + TransformUnit::VertexCarry carry = transform_.carry_; for (int i = 0; i < upperBound_ - lowerBound_ + 1; ++i) { vreader_.Goto(i); - cached_[i] = transform_.ReadVertex(vreader_, transformState_); + cached_[i] = transform_.ReadVertex(vreader_, transformState_, carry); + } + // What the next draw carries is the last vertex in draw order (gpu/vertices/carry). + if (vertexCount_ != 0) { + vreader_.Goto(useIndices_ ? conv_(vertexCount_ - 1) - lowerBound_ : vertexCount_ - 1); + transform_.ReadVertex(vreader_, transformState_, transform_.carry_); } } @@ -762,7 +767,7 @@ public: vreader_.Goto(vtx); } - return transform_.ReadVertex(vreader_, transformState_); + return transform_.ReadVertex(vreader_, transformState_, transform_.carry_); }; protected: @@ -770,6 +775,7 @@ protected: const IndexConverter conv_; const TransformState &transformState_; TransformUnit &transform_; + int vertexCount_ = 0; uint16_t lowerBound_; uint16_t upperBound_; static std::vector cached_; diff --git a/GPU/Software/TransformUnit.h b/GPU/Software/TransformUnit.h index b9f2696649..cffec21f19 100644 --- a/GPU/Software/TransformUnit.h +++ b/GPU/Software/TransformUnit.h @@ -149,11 +149,17 @@ public: SoftDirty GetDirty(); private: - ClipVertexData ReadVertex(const VertexReader &vreader, const TransformState &state); + // What a vertex format without UVs or a normal reads: the last ones read before it. + struct VertexCarry { + Vec3Packedf tc{}; + float normal[3]{}; + }; + ClipVertexData ReadVertex(const VertexReader &vreader, const TransformState &state, VertexCarry &carry); // orderReversed: verts are in the opposite order of how the GE takes the triangle (matters for clipping). void SendTriangle(CullType cullType, const ClipVertexData *verts, int provoking = 2, bool orderReversed = false); u8 *decoded_ = nullptr; + VertexCarry carry_; BinManager *binner_ = nullptr; // Normally max verts per prim is 3, but we temporarily need 4 to detect rectangles from strips. diff --git a/test.py b/test.py index 509a527656..63dcc02751 100755 --- a/test.py +++ b/test.py @@ -298,6 +298,7 @@ tests_good = [ "gpu/transfer/mirrors", "gpu/transfer/overlap", "gpu/triangle/triangle", + "gpu/vertices/carry", "gpu/vertices/colors", "gpu/vertices/morph", # "gpu/vertices/texcoords", # See issue #19093