Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 41 additions & 5 deletions ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>(srcVec - data) +
3u * static_cast<size_t>(bitsPerComponent / 8);
const size_t fourthComponentBytes =
static_cast<size_t>(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))
{
Expand Down
96 changes: 96 additions & 0 deletions ps2xTest/src/ps2_memory_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint8_t> 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<uint32_t>(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<uint8_t> packet;
appendU32(packet, makeVifCmd(0x66u, 2u, 0u));
packet.insert(packet.end(), {0x01u, 0x02u, 0xFEu, 0x7Fu});

mem.processVIF1Data(packet.data(), static_cast<uint32_t>(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<uint8_t> 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<uint32_t>(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;
Expand Down