diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index 05fc764a8..b30991e97 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -674,19 +674,55 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) } else if (vl == 3u && vn == 3u) { - // V4-5: packed color-like format in a single 16-bit value. + // V4-5 expands 5-bit RGB and 1-bit alpha into the upper bits of each lane. uint16_t packed = 0; std::memcpy(&packed, srcVec, sizeof(packed)); - decompressed[0] = packed & 0x1Fu; - decompressed[1] = (packed >> 5) & 0x1Fu; - decompressed[2] = (packed >> 10) & 0x1Fu; - decompressed[3] = (packed >> 15) & 0x01u; + decompressed[0] = (packed & 0x001Fu) << 3u; + decompressed[1] = (packed & 0x03E0u) >> 2u; + decompressed[2] = (packed & 0x7C00u) >> 7u; + decompressed[3] = (packed & 0x8000u) >> 8u; } else { 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..567fc7424 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -531,6 +531,102 @@ void register_ps2_memory_tests() t.Equals(sw, 0x00008001u, "zero-extend w"); }); + tc.Run("VIF UNPACK V4-5 expands packed channels", [](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(0x6Fu, 1u, 0u)); // UNPACK V4-5, NUM=1, ADDR=0 + appendU32(packet, 0x0000FC41u); // X=1, Y=2, Z=31, W=1 + + mem.processVIF1Data(packet.data(), static_cast(packet.size())); + + const uint8_t *vu = mem.getVU1Data(); + uint32_t x = 0, y = 0, z = 0, w = 0; + std::memcpy(&x, vu + 0u, 4u); + std::memcpy(&y, vu + 4u, 4u); + std::memcpy(&z, vu + 8u, 4u); + std::memcpy(&w, vu + 12u, 4u); + + t.Equals(x, 8u, "V4-5 should expand X from five bits"); + t.Equals(y, 16u, "V4-5 should expand Y from five bits"); + t.Equals(z, 248u, "V4-5 should expand Z from five bits"); + t.Equals(w, 128u, "V4-5 should expand W from one bit"); + }); + + 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;