From f072ed345b9cfaaa0640f04c460355a6018472ae Mon Sep 17 00:00:00 2001 From: GTTeancum Date: Sat, 29 Aug 2026 22:23:38 -0400 Subject: [PATCH 1/2] Expand VIF UNPACK V2 and V3 lanes --- ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp | 36 ++++++++++ ps2xTest/src/ps2_memory_tests.cpp | 71 ++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index 05fc764a8..c254109c2 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -687,6 +687,42 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) handledFormat = false; } + // The VIF expands V2 to XYXY. V3's otherwise-indeterminate W lane + // overlaps the next packed source component; games rely on both + // behaviors, so preserve them when the overlapping bytes are present. + if (handledFormat && components == 2) + { + decompressed[2] = decompressed[0]; + decompressed[3] = decompressed[1]; + } + else if (handledFormat && components == 3) + { + const size_t fourthComponentOffset = + static_cast(srcVec - data) + + 3u * static_cast(bitsPerComponent / 8); + const size_t fourthComponentBytes = + static_cast(bitsPerComponent / 8); + if (fourthComponentOffset + fourthComponentBytes <= sizeBytes) + { + if (vl == 0u) + { + std::memcpy(&decompressed[3], + data + fourthComponentOffset, + sizeof(decompressed[3])); + } + else if (vl == 1u) + { + uint16_t raw = 0u; + std::memcpy(&raw, data + fourthComponentOffset, sizeof(raw)); + decompressed[3] = extend16(raw); + } + else if (vl == 2u) + { + decompressed[3] = extend8(data[fourthComponentOffset]); + } + } + } + // Unknown compressed format fallback: preserve legacy raw-copy behavior. if (!handledFormat && decoded && !maskEnable && (vif1_regs.mode == 0u || vif1_regs.mode == 3u)) { diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 7c3e8ea5d..526051ee4 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -531,6 +531,77 @@ void register_ps2_memory_tests() t.Equals(sw, 0x00008001u, "zero-extend w"); }); + tc.Run("VIF UNPACK V2 duplicates XY into ZW", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + std::memset(mem.getVU1Data(), 0, PS2_VU1_DATA_SIZE); + + // UNPACK V2-8 (opcode 0x66), NUM=2, sign-extended. + std::vector packet; + appendU32(packet, makeVifCmd(0x66u, 2u, 0u)); + packet.insert(packet.end(), {0x01u, 0x02u, 0xFEu, 0x7Fu}); + + mem.processVIF1Data(packet.data(), static_cast(packet.size())); + + const uint8_t *vu = mem.getVU1Data(); + const uint32_t expected[2][4] = { + {1u, 2u, 1u, 2u}, + {0xFFFFFFFEu, 0x7Fu, 0xFFFFFFFEu, 0x7Fu}, + }; + for (uint32_t vector = 0u; vector < 2u; ++vector) + { + for (uint32_t lane = 0u; lane < 4u; ++lane) + { + uint32_t actual = 0u; + std::memcpy(&actual, vu + vector * 16u + lane * 4u, sizeof(actual)); + t.Equals(actual, expected[vector][lane], + "V2 lane should follow XYXY hardware expansion"); + } + } + }); + + tc.Run("VIF UNPACK V3 sources W from the next packed component", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + std::memset(mem.getVU1Data(), 0, PS2_VU1_DATA_SIZE); + + // UNPACK V3-16 (opcode 0x69), NUM=2. The first vector's W overlaps + // the second vector's X; the second overlaps the following VIF word. + std::vector packet; + appendU32(packet, makeVifCmd(0x69u, 2u, 0u)); + const uint16_t components[] = { + 0x1111u, 0x2222u, 0x3333u, + 0x4444u, 0x5555u, 0x6666u, + }; + for (uint16_t component : components) + { + const size_t offset = packet.size(); + packet.resize(offset + sizeof(component)); + std::memcpy(packet.data() + offset, &component, sizeof(component)); + } + appendU32(packet, 0x0000ABCDu); // NOP VIF word and final overlapping source. + + mem.processVIF1Data(packet.data(), static_cast(packet.size())); + + const uint8_t *vu = mem.getVU1Data(); + const uint32_t expected[2][4] = { + {0x1111u, 0x2222u, 0x3333u, 0x4444u}, + {0x4444u, 0x5555u, 0x6666u, 0xFFFFABCDu}, + }; + for (uint32_t vector = 0u; vector < 2u; ++vector) + { + for (uint32_t lane = 0u; lane < 4u; ++lane) + { + uint32_t actual = 0u; + std::memcpy(&actual, vu + vector * 16u + lane * 4u, sizeof(actual)); + t.Equals(actual, expected[vector][lane], + "V3 W should overlap the next packed source component"); + } + } + }); + tc.Run("VIF UNPACK bit15 adds TOPS to destination address", [](TestCase &t) { PS2Memory mem; From 2bd95c3037bc7c36d7c752dbc12408c54ccbbf13 Mon Sep 17 00:00:00 2001 From: GTTeancum Date: Mon, 31 Aug 2026 17:47:46 -0400 Subject: [PATCH 2/2] Correct VIF V3 quadword boundary behavior --- ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp | 9 +++++---- ps2xTest/src/ps2_memory_tests.cpp | 11 ++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index c254109c2..806954d73 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -687,9 +687,8 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) handledFormat = false; } - // The VIF expands V2 to XYXY. V3's otherwise-indeterminate W lane - // overlaps the next packed source component; games rely on both - // behaviors, so preserve them when the overlapping bytes are present. + // The VIF expands V2 to XYXY. V3 sources W from the next packed + // component unless XYZ ends on a quadword boundary, where W is zero. if (handledFormat && components == 2) { decompressed[2] = decompressed[0]; @@ -702,7 +701,9 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) 3u * static_cast(bitsPerComponent / 8); const size_t fourthComponentBytes = static_cast(bitsPerComponent / 8); - if (fourthComponentOffset + fourthComponentBytes <= sizeBytes) + decompressed[3] = 0u; + if ((fourthComponentOffset & 0xFu) != 0u && + fourthComponentOffset + fourthComponentBytes <= sizeBytes) { if (vl == 0u) { diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 526051ee4..29a32b5b4 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -561,14 +561,15 @@ void register_ps2_memory_tests() } }); - tc.Run("VIF UNPACK V3 sources W from the next packed component", [](TestCase &t) + tc.Run("VIF UNPACK V3 zeros W at a quadword boundary", [](TestCase &t) { PS2Memory mem; t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); std::memset(mem.getVU1Data(), 0, PS2_VU1_DATA_SIZE); // UNPACK V3-16 (opcode 0x69), NUM=2. The first vector's W overlaps - // the second vector's X; the second overlaps the following VIF word. + // the second vector's X. The second vector's XYZ ends at the source + // quadword boundary, so hardware supplies zero for W. std::vector packet; appendU32(packet, makeVifCmd(0x69u, 2u, 0u)); const uint16_t components[] = { @@ -581,14 +582,14 @@ void register_ps2_memory_tests() packet.resize(offset + sizeof(component)); std::memcpy(packet.data() + offset, &component, sizeof(component)); } - appendU32(packet, 0x0000ABCDu); // NOP VIF word and final overlapping source. + appendU32(packet, 0x0000ABCDu); // A following VIF word must not leak into W. mem.processVIF1Data(packet.data(), static_cast(packet.size())); const uint8_t *vu = mem.getVU1Data(); const uint32_t expected[2][4] = { {0x1111u, 0x2222u, 0x3333u, 0x4444u}, - {0x4444u, 0x5555u, 0x6666u, 0xFFFFABCDu}, + {0x4444u, 0x5555u, 0x6666u, 0u}, }; for (uint32_t vector = 0u; vector < 2u; ++vector) { @@ -597,7 +598,7 @@ void register_ps2_memory_tests() uint32_t actual = 0u; std::memcpy(&actual, vu + vector * 16u + lane * 4u, sizeof(actual)); t.Equals(actual, expected[vector][lane], - "V3 W should overlap the next packed source component"); + "V3 W should follow source quadword boundary semantics"); } } });