diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index 05fc764a8..60bb37230 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -581,6 +581,7 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) uint32_t decompressed[4] = {lanes[0], lanes[1], lanes[2], lanes[3]}; bool decoded = false; + const uint32_t sourceVectorIndex = srcIndex; const uint8_t *srcVec = nullptr; if (sourceAvailable && srcIndex < sourceVectorCount) { @@ -687,6 +688,74 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) handledFormat = false; } + // The VIF expands V2 to XYXY. V3's hardware-defined W lane depends + // on both the packed format and the source packet phase. + if (handledFormat && components == 2) + { + decompressed[2] = decompressed[0]; + decompressed[3] = decompressed[1]; + } + else if (handledFormat && components == 3) + { + const size_t dataStartOffset = + static_cast(srcBase - data); + const uint32_t dataStartWord = + static_cast((dataStartOffset >> 2u) & 0x3u); + const uint32_t startAlignment = + (dataStartWord == 0u) ? 4u : dataStartWord; + const uint32_t unpackIteration = sourceVectorIndex + 1u; + bool keepFourthComponent = false; + + if (vl == 0u) + { + // V3-32 advances its internal phase after each vector. + keepFourthComponent = + ((sourceVectorIndex & 1u) == (startAlignment & 1u)); + } + else if (vl == 1u) + { + // V3-16 has a four-word phase tested against real hardware. + const uint32_t phase = + ((unpackIteration / 4u) + 1u + + (4u - startAlignment)) & 0x3u; + keepFourthComponent = + !((unpackIteration & 1u) == 0u && phase == 0u); + } + else if (vl == 2u) + { + // V3-8 only exposes W at its matching initial packet phase. + keepFourthComponent = + (unpackIteration == (startAlignment & 1u)); + } + + const size_t fourthComponentOffset = + static_cast(srcVec - data) + + 3u * static_cast(bitsPerComponent / 8); + const size_t fourthComponentBytes = + static_cast(bitsPerComponent / 8); + decompressed[3] = 0u; + if (keepFourthComponent && + 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..ee59af0a7 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -531,6 +531,146 @@ 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-32 W follows source packet phase", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + for (uint32_t prefixWords = 0u; prefixWords < 4u; ++prefixWords) + { + std::memset(mem.getVU1Data(), 0, PS2_VU1_DATA_SIZE); + + std::vector packet; + for (uint32_t i = 0u; i < prefixWords; ++i) + appendU32(packet, makeVifCmd(0x00u, 0u, 0u)); + + appendU32(packet, makeVifCmd(0x68u, 2u, 0u)); // UNPACK V3-32 + for (uint32_t component = 1u; component <= 6u; ++component) + appendU32(packet, 0x10000000u + component); + appendU32(packet, makeVifCmd(0x07u, 0u, 0xBEEFu)); + + mem.processVIF1Data(packet.data(), static_cast(packet.size())); + + const uint32_t dataStartWord = (prefixWords + 1u) & 0x3u; + const uint32_t startAlignment = + (dataStartWord == 0u) ? 4u : dataStartWord; + for (uint32_t vector = 0u; vector < 2u; ++vector) + { + uint32_t actualW = 0u; + std::memcpy(&actualW, + mem.getVU1Data() + vector * 16u + 12u, + sizeof(actualW)); + const bool keepW = + ((vector & 1u) == (startAlignment & 1u)); + const uint32_t expectedW = keepW + ? ((vector == 0u) ? 0x10000004u : 0x0700BEEFu) + : 0u; + t.Equals(actualW, expectedW, + "V3-32 W should follow the hardware-tested packet phase"); + } + } + }); + + tc.Run("VIF UNPACK V3-16 W follows four-word packet phase", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + std::memset(mem.getVU1Data(), 0, PS2_VU1_DATA_SIZE); + + std::vector packet; + appendU32(packet, makeVifCmd(0x00u, 0u, 0u)); + appendU32(packet, makeVifCmd(0x69u, 6u, 0u)); // data begins at word phase 2 + for (uint16_t component = 1u; component <= 18u; ++component) + { + const size_t offset = packet.size(); + packet.resize(offset + sizeof(component)); + std::memcpy(packet.data() + offset, &component, sizeof(component)); + } + appendU32(packet, makeVifCmd(0x07u, 0u, 0xBEEFu)); + + mem.processVIF1Data(packet.data(), static_cast(packet.size())); + + const uint8_t *vu = mem.getVU1Data(); + const uint32_t expectedW[6] = { + 4u, 7u, 10u, 0u, 16u, 0u, + }; + for (uint32_t vector = 0u; vector < 6u; ++vector) + { + uint32_t actualW = 0u; + std::memcpy(&actualW, vu + vector * 16u + 12u, sizeof(actualW)); + t.Equals(actualW, expectedW[vector], + "V3-16 W should follow the hardware-tested four-word phase"); + } + }); + + tc.Run("VIF UNPACK V3-8 W follows its initial packet phase", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + for (uint32_t prefixWords = 0u; prefixWords < 4u; ++prefixWords) + { + std::memset(mem.getVU1Data(), 0, PS2_VU1_DATA_SIZE); + + std::vector packet; + for (uint32_t i = 0u; i < prefixWords; ++i) + appendU32(packet, makeVifCmd(0x00u, 0u, 0u)); + + appendU32(packet, makeVifCmd(0x6Au, 2u, 0u)); + packet.insert(packet.end(), { + 0x11u, 0x12u, 0x13u, + 0x21u, 0x22u, 0x23u, + 0xA5u, 0xA6u, + }); + + mem.processVIF1Data(packet.data(), static_cast(packet.size())); + + const uint32_t dataStartWord = (prefixWords + 1u) & 0x3u; + const uint32_t startAlignment = + (dataStartWord == 0u) ? 4u : dataStartWord; + uint32_t firstW = 0u; + uint32_t secondW = 0u; + std::memcpy(&firstW, mem.getVU1Data() + 12u, sizeof(firstW)); + std::memcpy(&secondW, mem.getVU1Data() + 28u, sizeof(secondW)); + const uint32_t expectedFirstW = + ((startAlignment & 1u) != 0u) ? 0x21u : 0u; + t.Equals(firstW, expectedFirstW, + "V3-8 first W should follow the hardware-tested packet phase"); + t.Equals(secondW, 0u, + "V3-8 should zero W after its initial matching phase"); + } + }); + tc.Run("VIF UNPACK bit15 adds TOPS to destination address", [](TestCase &t) { PS2Memory mem;