mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
Merge branch 'main' of https://github.com/ran-j/PS2Recomp into feature/remove-runtime-guest-threads
This commit is contained in:
@@ -70,7 +70,8 @@ namespace ps2x::iop::detail
|
||||
.responseCounterOffset = 4u,
|
||||
.zeroReceiveBuffer = true,
|
||||
.signalNowaitCompletion = true,
|
||||
.suppressedCompletionCallbacks = {0x001FFD70u},
|
||||
.completeQueuedPlayStreams = true,
|
||||
.suppressedCompletionCallbacks = {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -107,6 +107,7 @@ namespace ps2x::iop::detail
|
||||
uint32_t responseCounterOffset = 0u;
|
||||
bool zeroReceiveBuffer = true;
|
||||
bool signalNowaitCompletion = false;
|
||||
bool completeQueuedPlayStreams = false;
|
||||
std::vector<uint32_t> suppressedCompletionCallbacks;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,11 +7,20 @@
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint16_t kPlayStreamCommand = 1u;
|
||||
constexpr uint32_t kResponseRecordStride = 0x20u;
|
||||
constexpr uint32_t kPackedStreamOffset = 4u;
|
||||
constexpr uint32_t kStreamSlotMask = 0x3Fu;
|
||||
constexpr uint32_t kStreamSlotCount = 48u;
|
||||
constexpr uint32_t kCommandStreamSlotShift = 8u;
|
||||
constexpr uint32_t kResponseStreamSlotShift = 4u;
|
||||
|
||||
class SoundUpdateStubService final : public IopService
|
||||
{
|
||||
public:
|
||||
@@ -34,6 +43,7 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_updateCounter = 0u;
|
||||
m_completedStreamCount = 0u;
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
@@ -62,13 +72,23 @@ namespace ps2x::iop::detail
|
||||
(void)m_host.zeroGuest(request.receive.address, request.receive.size);
|
||||
}
|
||||
|
||||
std::vector<uint32_t> activeStreamSlots;
|
||||
if (m_bindings.completeQueuedPlayStreams && request.receive.address != 0u)
|
||||
{
|
||||
// PlayStream leaves the EE slot in state 2. One active record moves it
|
||||
// to state 1; the following empty update lets SOUND_CopyIOPBuffer clear it.
|
||||
activeStreamSlots = findQueuedPlayStreams(request);
|
||||
trimToReceiveCapacity(activeStreamSlots, request.receive.size);
|
||||
}
|
||||
|
||||
uint32_t counter = 0u;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
counter = ++m_updateCounter;
|
||||
m_completedStreamCount += activeStreamSlots.size();
|
||||
}
|
||||
|
||||
constexpr uint32_t activeStreams = 0u;
|
||||
const uint32_t activeStreams = static_cast<uint32_t>(activeStreamSlots.size());
|
||||
if (request.receive.address != 0u &&
|
||||
request.receive.size >= m_bindings.activeStreamCountOffset + sizeof(activeStreams))
|
||||
{
|
||||
@@ -76,10 +96,20 @@ namespace ps2x::iop::detail
|
||||
(void)m_host.writeGuest(address, &activeStreams, sizeof(activeStreams));
|
||||
}
|
||||
|
||||
if (request.receive.address != 0u &&
|
||||
request.receive.size >= m_bindings.responseCounterOffset + sizeof(counter))
|
||||
for (size_t index = 0u; index < activeStreamSlots.size(); ++index)
|
||||
{
|
||||
const uint32_t address = request.receive.address + m_bindings.responseCounterOffset;
|
||||
const uint32_t packedStream = activeStreamSlots[index] << kResponseStreamSlotShift;
|
||||
const uint32_t offset = m_bindings.activeStreamCountOffset + static_cast<uint32_t>(index) * kResponseRecordStride + kPackedStreamOffset;
|
||||
const uint32_t address = request.receive.address + offset;
|
||||
(void)m_host.writeGuest(address, &packedStream, sizeof(packedStream));
|
||||
}
|
||||
|
||||
const uint32_t counterOffset = m_bindings.responseCounterOffset +
|
||||
activeStreams * kResponseRecordStride;
|
||||
if (request.receive.address != 0u &&
|
||||
request.receive.size >= counterOffset + sizeof(counter))
|
||||
{
|
||||
const uint32_t address = request.receive.address + counterOffset;
|
||||
(void)m_host.writeGuest(address, &counter, sizeof(counter));
|
||||
}
|
||||
|
||||
@@ -90,14 +120,98 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"update_counter", m_updateCounter, false});
|
||||
metrics.push_back({"completed_streams", m_completedStreamCount, false});
|
||||
}
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::vector<uint32_t> findQueuedPlayStreams(const RpcRequest &request) const
|
||||
{
|
||||
std::vector<uint32_t> slots;
|
||||
if (request.send.address == 0u || request.send.size < sizeof(uint16_t))
|
||||
{
|
||||
return slots;
|
||||
}
|
||||
|
||||
uint16_t commandCount = 0u;
|
||||
if (!m_host.readGuest(request.send.address, &commandCount, sizeof(commandCount)))
|
||||
{
|
||||
return slots;
|
||||
}
|
||||
|
||||
uint32_t offset = sizeof(commandCount);
|
||||
for (uint32_t commandIndex = 0u; commandIndex < commandCount; ++commandIndex)
|
||||
{
|
||||
constexpr uint32_t headerSize = sizeof(uint16_t) * 2u;
|
||||
if (offset > request.send.size || request.send.size - offset < headerSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
std::array<uint16_t, 2> header{};
|
||||
if (!m_host.readGuest(request.send.address + offset,
|
||||
header.data(),
|
||||
sizeof(header)))
|
||||
{
|
||||
break;
|
||||
}
|
||||
offset += headerSize;
|
||||
|
||||
const uint32_t argumentBytes =
|
||||
static_cast<uint32_t>(header[1]) * sizeof(uint16_t);
|
||||
if (argumentBytes > request.send.size - offset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (header[0] == kPlayStreamCommand && header[1] >= 2u)
|
||||
{
|
||||
uint16_t encodedSlot = 0u;
|
||||
if (m_host.readGuest(request.send.address + offset + sizeof(uint16_t),
|
||||
&encodedSlot,
|
||||
sizeof(encodedSlot)))
|
||||
{
|
||||
const uint32_t slot =
|
||||
(encodedSlot >> kCommandStreamSlotShift) & kStreamSlotMask;
|
||||
if (slot < kStreamSlotCount &&
|
||||
std::find(slots.begin(), slots.end(), slot) == slots.end())
|
||||
{
|
||||
slots.push_back(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset += argumentBytes;
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
void trimToReceiveCapacity(std::vector<uint32_t> &slots, uint32_t receiveSize) const
|
||||
{
|
||||
size_t count = 0u;
|
||||
for (; count < slots.size(); ++count)
|
||||
{
|
||||
const uint64_t recordOffset =
|
||||
static_cast<uint64_t>(m_bindings.activeStreamCountOffset) +
|
||||
static_cast<uint64_t>(count) * kResponseRecordStride +
|
||||
kPackedStreamOffset;
|
||||
const uint64_t counterOffset =
|
||||
static_cast<uint64_t>(m_bindings.responseCounterOffset) +
|
||||
static_cast<uint64_t>(count + 1u) * kResponseRecordStride;
|
||||
if (recordOffset + sizeof(uint32_t) > receiveSize ||
|
||||
counterOffset + sizeof(uint32_t) > receiveSize)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
slots.resize(count);
|
||||
}
|
||||
|
||||
IopHost &m_host;
|
||||
SoundUpdateStubBindings m_bindings;
|
||||
std::array<uint32_t, 1> m_sids;
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_updateCounter = 0u;
|
||||
uint64_t m_completedStreamCount = 0u;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef PS2RECOMP_ELF_PARSER_H
|
||||
#define PS2RECOMP_ELF_PARSER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <elfio/elfio.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -288,12 +288,12 @@ namespace ps2recomp
|
||||
MMI2_PMULTW = 0x0C,
|
||||
MMI2_PDIVW = 0x0D,
|
||||
MMI2_PCPYLD = 0x0E,
|
||||
MMI2_PMADDH = 0x10,
|
||||
MMI2_PHMADH = 0x11,
|
||||
MMI2_PAND = 0x12,
|
||||
MMI2_PXOR = 0x13,
|
||||
MMI2_PMADDH = 0x14,
|
||||
MMI2_PHMADH = 0x15,
|
||||
MMI2_PMSUBH = 0x18,
|
||||
MMI2_PHMSBH = 0x19,
|
||||
MMI2_PMSUBH = 0x14,
|
||||
MMI2_PHMSBH = 0x15,
|
||||
MMI2_PEXEH = 0x1A,
|
||||
MMI2_PREVH = 0x1B,
|
||||
MMI2_PMULTH = 0x1C,
|
||||
@@ -670,37 +670,21 @@ namespace ps2recomp
|
||||
// VU0_VLDQ = 0x1F // VU0 Load/Store Quad with Decrement
|
||||
// };
|
||||
|
||||
// VU0 Control Register Numbers (used with CFC2/CTC2)
|
||||
// VU0 COP2 control register numbers used by CFC2/CTC2.
|
||||
// Registers 0..15 address VI0..VI15 directly.
|
||||
enum VU0ControlRegisters
|
||||
{
|
||||
VU0_CR_STATUS = 0, // Status/Control register
|
||||
VU0_CR_MAC = 1, // MAC flags register
|
||||
VU0_CR_CLIP = 5, // Clipping flags register
|
||||
VU0_CR_R = 3, // R register (Random number)
|
||||
VU0_CR_I = 4, // I register (Immediate)
|
||||
|
||||
// Add missing registers
|
||||
VU0_CR_VPU_STAT = 2, // VPU-STAT register
|
||||
VU0_CR_TPC = 6, // T (program counter) register
|
||||
VU0_CR_CMSAR0 = 7, // Call/return address 0
|
||||
VU0_CR_FBRST = 8, // VIF/VU reset register
|
||||
VU0_CR_VPU_STAT2 = 9, // VPU-STAT register 2
|
||||
VU0_CR_TPC2 = 10, // T (program counter) register 2
|
||||
VU0_CR_CMSAR1 = 11, // Call/return address 1
|
||||
VU0_CR_FBRST2 = 12, // VIF/VU reset register 2
|
||||
VU0_CR_VPU_STAT3 = 13, // VPU-STAT register 3
|
||||
VU0_CR_CMSAR2 = 14, // Call/return address 2
|
||||
VU0_CR_FBRST3 = 15, // VIF/VU reset register 3
|
||||
VU0_CR_VPU_STAT4 = 16, // VPU-STAT register 4
|
||||
VU0_CR_CMSAR3 = 17, // Call/return address 3
|
||||
VU0_CR_FBRST4 = 18, // VIF/VU reset register 4
|
||||
VU0_CR_ACC = 20, // Accumulator register
|
||||
VU0_CR_INFO = 21, // Information register
|
||||
VU0_CR_CLIP2 = 22, // Clipping flags register 2
|
||||
VU0_CR_P = 26, // P register
|
||||
VU0_CR_XITOP = 27, // XITOP register
|
||||
VU0_CR_ITOP = 28, // ITOP register
|
||||
VU0_CR_TOP = 29 // TOP register
|
||||
VU0_CR_STATUS = 16,
|
||||
VU0_CR_MAC = 17,
|
||||
VU0_CR_CLIP = 18,
|
||||
VU0_CR_R = 20,
|
||||
VU0_CR_I = 21,
|
||||
VU0_CR_Q = 22,
|
||||
VU0_CR_TPC = 26,
|
||||
VU0_CR_CMSAR0 = 27,
|
||||
VU0_CR_FBRST = 28,
|
||||
VU0_CR_VPU_STAT = 29,
|
||||
VU0_CR_CMSAR1 = 31
|
||||
};
|
||||
enum VU0OPSFunctions
|
||||
{
|
||||
|
||||
@@ -34,8 +34,10 @@ namespace ps2recomp
|
||||
bool recompile();
|
||||
void generateOutput();
|
||||
void printReport() const;
|
||||
const RecompilerReporter::Counters &reportCounters() const { return m_reporter.counters(); }
|
||||
|
||||
static StubTarget resolveStubTarget(const std::string& name);
|
||||
static bool IsCorrectnessCriticalFunctionName(const std::string &name);
|
||||
static size_t DiscoverAdditionalEntryPoints(
|
||||
std::vector<Function> &functions,
|
||||
std::unordered_map<uint32_t, std::vector<Instruction>> &decodedFunctions,
|
||||
@@ -65,6 +67,7 @@ namespace ps2recomp
|
||||
std::unordered_set<std::string> m_stubFunctions;
|
||||
std::unordered_set<uint32_t> m_stubFunctionStarts;
|
||||
std::unordered_map<uint32_t, std::string> m_stubHandlerBindingsByStart;
|
||||
std::unordered_set<uint32_t> m_correctnessCriticalFunctionStarts;
|
||||
std::map<uint32_t, std::string> m_generatedStubs;
|
||||
std::unordered_map<uint32_t, std::string> m_functionRenames;
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> m_resumeEntryTargetsByOwner;
|
||||
@@ -74,6 +77,9 @@ namespace ps2recomp
|
||||
void discoverAdditionalEntryPoints();
|
||||
bool shouldSkipFunction(const Function &function) const;
|
||||
bool isStubFunction(const Function &function) const;
|
||||
bool isCorrectnessCriticalFunction(const Function &function) const;
|
||||
bool hasResolvedStubHandler(const Function &function) const;
|
||||
void collectCorrectnessCriticalFunctionStarts();
|
||||
bool generateFunctionHeader();
|
||||
bool generateStubHeader();
|
||||
bool writeToFile(const std::string &path, const std::string &content);
|
||||
|
||||
@@ -46,6 +46,8 @@ namespace ps2recomp
|
||||
size_t unhandledInstructions = 0;
|
||||
size_t indirectFallbackPromotions = 0;
|
||||
size_t indirectFallbackEntries = 0;
|
||||
size_t correctnessCriticalGuestFallbacks = 0;
|
||||
size_t correctnessCriticalFailures = 0;
|
||||
};
|
||||
|
||||
void progress(const std::string &message);
|
||||
@@ -63,6 +65,8 @@ namespace ps2recomp
|
||||
void recordDecodeFailure();
|
||||
void recordAdditionalEntryPoints(size_t count);
|
||||
void recordGeneratedFunctions(size_t count);
|
||||
void recordCorrectnessCriticalGuestFallback();
|
||||
void recordCorrectnessCriticalFailure();
|
||||
void recordIndirectFallbackPromotion(const std::string &functionName,
|
||||
const std::vector<uint32_t> &jumpAddresses,
|
||||
size_t promotedEntryCount);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <filesystem>
|
||||
#include <cctype>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
@@ -750,6 +751,7 @@ namespace ps2recomp
|
||||
m_stubFunctions.clear();
|
||||
m_stubFunctionStarts.clear();
|
||||
m_stubHandlerBindingsByStart.clear();
|
||||
m_correctnessCriticalFunctionStarts.clear();
|
||||
|
||||
for (const auto &name : m_config.skipFunctions)
|
||||
{
|
||||
@@ -809,6 +811,7 @@ namespace ps2recomp
|
||||
m_symbols = m_elfParser->extractSymbols();
|
||||
m_sections = m_elfParser->getSections();
|
||||
m_relocations = m_elfParser->getRelocations();
|
||||
collectCorrectnessCriticalFunctionStarts();
|
||||
|
||||
if (m_functions.empty())
|
||||
{
|
||||
@@ -937,24 +940,81 @@ namespace ps2recomp
|
||||
|
||||
size_t processedCount = 0;
|
||||
size_t failedCount = 0;
|
||||
size_t correctnessCriticalFailureCount = 0;
|
||||
|
||||
for (uint32_t initializerStart : m_correctnessCriticalFunctionStarts)
|
||||
{
|
||||
const auto functionIt = std::find_if(
|
||||
m_functions.begin(), m_functions.end(),
|
||||
[initializerStart](const Function &function)
|
||||
{ return function.start == initializerStart; });
|
||||
if (functionIt == m_functions.end())
|
||||
{
|
||||
const auto bindingIt = m_stubHandlerBindingsByStart.find(initializerStart);
|
||||
if (bindingIt != m_stubHandlerBindingsByStart.end() &&
|
||||
resolveStubTarget(bindingIt->second) != StubTarget::Unknown)
|
||||
{
|
||||
Function manualInitializer{};
|
||||
manualInitializer.name = "manual_initializer_" + bindingIt->second;
|
||||
manualInitializer.start = initializerStart;
|
||||
manualInitializer.end = initializerStart + 4u;
|
||||
m_functions.push_back(std::move(manualInitializer));
|
||||
m_reporter.info(
|
||||
"correctness-critical",
|
||||
"Synthesized initializer entry for resolved handler '" +
|
||||
bindingIt->second + "'");
|
||||
continue;
|
||||
}
|
||||
|
||||
++correctnessCriticalFailureCount;
|
||||
m_reporter.recordCorrectnessCriticalFailure();
|
||||
m_reporter.errorAt(
|
||||
"correctness-critical",
|
||||
".ctors/.init_array",
|
||||
initializerStart,
|
||||
"Initializer table target has no discovered guest function or manual handler");
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &function : m_functions)
|
||||
{
|
||||
m_reporter.recordFunctionProcessed();
|
||||
const bool correctnessCritical = isCorrectnessCriticalFunction(function);
|
||||
|
||||
if (isStubFunction(function))
|
||||
{
|
||||
function.isStub = true;
|
||||
function.isSkipped = false;
|
||||
m_reporter.recordFunctionStubbed();
|
||||
continue;
|
||||
if (!correctnessCritical || hasResolvedStubHandler(function))
|
||||
{
|
||||
function.isStub = true;
|
||||
function.isSkipped = false;
|
||||
m_reporter.recordFunctionStubbed();
|
||||
continue;
|
||||
}
|
||||
|
||||
m_reporter.recordCorrectnessCriticalGuestFallback();
|
||||
m_reporter.warningAt(
|
||||
"correctness-critical",
|
||||
function.name,
|
||||
function.start,
|
||||
"Unresolved initializer stub ignored; recompiling the original guest function");
|
||||
}
|
||||
|
||||
if (shouldSkipFunction(function))
|
||||
{
|
||||
function.isSkipped = true;
|
||||
function.isStub = false;
|
||||
m_reporter.recordFunctionSkipped();
|
||||
continue;
|
||||
if (!correctnessCritical)
|
||||
{
|
||||
function.isSkipped = true;
|
||||
function.isStub = false;
|
||||
m_reporter.recordFunctionSkipped();
|
||||
continue;
|
||||
}
|
||||
|
||||
m_reporter.recordCorrectnessCriticalGuestFallback();
|
||||
m_reporter.warningAt(
|
||||
"correctness-critical",
|
||||
function.name,
|
||||
function.start,
|
||||
"Initializer skip ignored; recompiling the original guest function");
|
||||
}
|
||||
|
||||
if (!decodeFunction(function))
|
||||
@@ -962,11 +1022,26 @@ namespace ps2recomp
|
||||
++failedCount;
|
||||
m_reporter.recordDecodeFailure();
|
||||
m_reporter.recordFunctionSkipped();
|
||||
m_reporter.warningAt("decode", function.name, function.start, "Skipping function due decode failure");
|
||||
function.isSkipped = true;
|
||||
if (correctnessCritical)
|
||||
{
|
||||
++correctnessCriticalFailureCount;
|
||||
m_reporter.recordCorrectnessCriticalFailure();
|
||||
m_reporter.errorAt(
|
||||
"correctness-critical",
|
||||
function.name,
|
||||
function.start,
|
||||
"Initializer could not be recompiled and has no resolved manual handler");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_reporter.warningAt("decode", function.name, function.start, "Skipping function due decode failure");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
function.isStub = false;
|
||||
function.isSkipped = false;
|
||||
function.isRecompiled = true;
|
||||
m_reporter.recordFunctionRecompiled();
|
||||
#if _DEBUG
|
||||
@@ -992,7 +1067,7 @@ namespace ps2recomp
|
||||
}
|
||||
|
||||
m_reporter.progress("recompilation pass completed");
|
||||
return true;
|
||||
return correctnessCriticalFailureCount == 0u;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
@@ -1959,6 +2034,64 @@ namespace ps2recomp
|
||||
return ps2_runtime_calls::isStubName(function.name);
|
||||
}
|
||||
|
||||
bool PS2Recompiler::IsCorrectnessCriticalFunctionName(const std::string &name)
|
||||
{
|
||||
static constexpr const char *kPrefixes[] = {
|
||||
"__ct__",
|
||||
"__sinit_",
|
||||
"_GLOBAL__sub_I_",
|
||||
"GLOBAL__sub_I_",
|
||||
"__static_initialization_and_destruction_0",
|
||||
"__do_global_ctors",
|
||||
};
|
||||
|
||||
for (const char *prefix : kPrefixes)
|
||||
{
|
||||
if (name.rfind(prefix, 0u) == 0u)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PS2Recompiler::isCorrectnessCriticalFunction(const Function &function) const
|
||||
{
|
||||
return IsCorrectnessCriticalFunctionName(function.name) ||
|
||||
m_correctnessCriticalFunctionStarts.contains(function.start);
|
||||
}
|
||||
|
||||
bool PS2Recompiler::hasResolvedStubHandler(const Function &function) const
|
||||
{
|
||||
std::string handlerName = function.name;
|
||||
const auto bindingIt = m_stubHandlerBindingsByStart.find(function.start);
|
||||
if (bindingIt != m_stubHandlerBindingsByStart.end() && !bindingIt->second.empty())
|
||||
handlerName = bindingIt->second;
|
||||
return resolveStubTarget(handlerName) != StubTarget::Unknown;
|
||||
}
|
||||
|
||||
void PS2Recompiler::collectCorrectnessCriticalFunctionStarts()
|
||||
{
|
||||
m_correctnessCriticalFunctionStarts.clear();
|
||||
for (const Section §ion : m_sections)
|
||||
{
|
||||
if (section.name != ".ctors" &&
|
||||
section.name != ".init_array" &&
|
||||
section.name != ".preinit_array")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (section.data == nullptr || section.size < sizeof(uint32_t))
|
||||
continue;
|
||||
|
||||
for (uint32_t offset = 0; offset + sizeof(uint32_t) <= section.size; offset += sizeof(uint32_t))
|
||||
{
|
||||
uint32_t target = 0u;
|
||||
std::memcpy(&target, section.data + offset, sizeof(target));
|
||||
if (target != 0u && target != 0xFFFFFFFFu)
|
||||
m_correctnessCriticalFunctionStarts.insert(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PS2Recompiler::writeToFile(const std::string &path, const std::string &content)
|
||||
{
|
||||
std::ofstream file(path);
|
||||
|
||||
@@ -113,6 +113,18 @@ namespace ps2recomp
|
||||
m_counters.generatedFunctions += count;
|
||||
}
|
||||
|
||||
void RecompilerReporter::recordCorrectnessCriticalGuestFallback()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
++m_counters.correctnessCriticalGuestFallbacks;
|
||||
}
|
||||
|
||||
void RecompilerReporter::recordCorrectnessCriticalFailure()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
++m_counters.correctnessCriticalFailures;
|
||||
}
|
||||
|
||||
void RecompilerReporter::recordIndirectFallbackPromotion(const std::string &functionName,
|
||||
const std::vector<uint32_t> &jumpAddresses,
|
||||
size_t promotedEntryCount)
|
||||
@@ -183,6 +195,8 @@ namespace ps2recomp
|
||||
os << "Indirect fallback promotions: " << m_counters.indirectFallbackPromotions
|
||||
<< " (" << m_counters.indirectFallbackEntries << " fallback entries)" << std::endl;
|
||||
os << "Unhandled instructions: " << m_counters.unhandledInstructions << std::endl;
|
||||
os << "Correctness-critical guest fallbacks: " << m_counters.correctnessCriticalGuestFallbacks
|
||||
<< ", failures: " << m_counters.correctnessCriticalFailures << std::endl;
|
||||
|
||||
size_t warnings = 0;
|
||||
size_t errors = 0;
|
||||
|
||||
@@ -209,8 +209,7 @@ namespace ps2recomp
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); "
|
||||
"__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs, vft, vft, shuffle_pattern,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
@@ -230,8 +229,7 @@ namespace ps2recomp
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); "
|
||||
"__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs, vft, vft, shuffle_pattern,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
@@ -285,8 +283,7 @@ namespace ps2recomp
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); "
|
||||
"__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs, vft,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
@@ -301,8 +298,7 @@ namespace ps2recomp
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); "
|
||||
"__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
@@ -317,8 +313,7 @@ namespace ps2recomp
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); "
|
||||
"__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
@@ -402,12 +397,13 @@ namespace ps2recomp
|
||||
uint8_t vfs = inst.rd;
|
||||
uint8_t vft = inst.rt;
|
||||
uint8_t dest_mask = inst.vectorInfo.vectorField;
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); "
|
||||
return fmt::format("{{ __m128 fs_yzx = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(3,0,2,1)); "
|
||||
"__m128 ft_zxy = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(3,1,0,2)); "
|
||||
"__m128 mul_res = PS2_VMUL(fs_yzx, ft_zxy); "
|
||||
"__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
vfs, vft,
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs, vfs, vft, vft,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
vfd, vfd);
|
||||
@@ -450,8 +446,7 @@ namespace ps2recomp
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); "
|
||||
"__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs, vft,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
@@ -509,8 +504,7 @@ namespace ps2recomp
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); "
|
||||
"__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
@@ -525,8 +519,7 @@ namespace ps2recomp
|
||||
return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); "
|
||||
"__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); "
|
||||
"__m128i mask = _mm_set_epi32({}, {}, {}, {}); "
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); "
|
||||
"ctx->vu0_acc = res; }}",
|
||||
"ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}",
|
||||
vfs,
|
||||
(dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0,
|
||||
@@ -730,8 +723,11 @@ namespace ps2recomp
|
||||
uint8_t vfs = inst.rd;
|
||||
uint8_t vft = inst.rt;
|
||||
uint8_t dest_mask = inst.vectorInfo.vectorField;
|
||||
return fmt::format("{{ __m128 res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}",
|
||||
vfs, vft, codegen::vuMaskExpr(dest_mask));
|
||||
return fmt::format("{{ __m128 fs_yzx = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(3,0,2,1)); "
|
||||
"__m128 ft_zxy = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(3,1,0,2)); "
|
||||
"__m128 res = PS2_VMUL(fs_yzx, ft_zxy); "
|
||||
"ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}",
|
||||
vfs, vfs, vft, vft, codegen::vuMaskExpr(dest_mask));
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translateVU_VITOF(const Instruction &inst, int shift)
|
||||
|
||||
@@ -29,124 +29,77 @@ namespace ps2recomp
|
||||
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_castps_si128(ctx->vu0_vf[{}]));", rt, rd);
|
||||
case COP2_CFC2:
|
||||
{
|
||||
switch (rd) // Control register number is in rd
|
||||
// CFC2/CTC2 use the same 5-bit register field for VI0..VI15 and the VU special control registers.
|
||||
if (rd < 16)
|
||||
{
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, static_cast<uint32_t>(ctx->vi[{}]));", rt, rd);
|
||||
}
|
||||
|
||||
switch (rd)
|
||||
{
|
||||
case VU0_CR_STATUS:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_status);", rt);
|
||||
case VU0_CR_MAC:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_mac_flags);", rt);
|
||||
case VU0_CR_VPU_STAT:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat);", rt);
|
||||
case VU0_CR_CLIP:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_clip_flags & 0x00FFFFFFu);", rt);
|
||||
case VU0_CR_R:
|
||||
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_castps_si128(ctx->vu0_r));", rt);
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, static_cast<uint32_t>(_mm_cvtsi128_si32(_mm_castps_si128(ctx->vu0_r))));", rt);
|
||||
case VU0_CR_I:
|
||||
return fmt::format("{{ uint32_t bits; std::memcpy(&bits, &ctx->vu0_i, sizeof(bits)); SET_GPR_U32(ctx, {}, bits); }}", rt);
|
||||
case VU0_CR_CLIP:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_clip_flags);", rt);
|
||||
case VU0_CR_Q:
|
||||
return fmt::format("{{ uint32_t bits; std::memcpy(&bits, &ctx->vu0_q, sizeof(bits)); SET_GPR_U32(ctx, {}, bits); }}", rt);
|
||||
case VU0_CR_TPC:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_tpc);", rt);
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_tpc >> 3);", rt);
|
||||
case VU0_CR_CMSAR0:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_cmsar0);", rt);
|
||||
case VU0_CR_FBRST:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_fbrst);", rt);
|
||||
case VU0_CR_VPU_STAT2:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat2);", rt);
|
||||
case VU0_CR_TPC2:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_tpc2);", rt);
|
||||
case VU0_CR_VPU_STAT:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat);", rt);
|
||||
case VU0_CR_CMSAR1:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_cmsar1);", rt);
|
||||
case VU0_CR_FBRST2:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_fbrst2);", rt);
|
||||
case VU0_CR_VPU_STAT3:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat3);", rt);
|
||||
case VU0_CR_CMSAR2:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_cmsar2);", rt);
|
||||
case VU0_CR_FBRST3:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_fbrst3);", rt);
|
||||
case VU0_CR_VPU_STAT4:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat4);", rt);
|
||||
case VU0_CR_CMSAR3:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_cmsar3);", rt);
|
||||
case VU0_CR_FBRST4:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_fbrst4);", rt);
|
||||
case VU0_CR_ACC:
|
||||
return fmt::format("SET_GPR_VEC(ctx, {}, _mm_castps_si128(ctx->vu0_acc));", rt);
|
||||
case VU0_CR_INFO: // I dd found on offical docs but ok
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_info);", rt);
|
||||
case VU0_CR_CLIP2:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_clip_flags2);", rt);
|
||||
case VU0_CR_P:
|
||||
return fmt::format("{{ uint32_t bits; std::memcpy(&bits, &ctx->vu0_p, sizeof(bits)); SET_GPR_U32(ctx, {}, bits); }}", rt);
|
||||
case VU0_CR_XITOP: // Maybe this does not exist, maybe we handle to vu0_itop
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_xitop);", rt);
|
||||
case VU0_CR_ITOP:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_itop);", rt);
|
||||
case VU0_CR_TOP:
|
||||
return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_top);", rt);
|
||||
default:
|
||||
return fmt::format("// Unimplemented CFC2 VU CReg: {}", rt);
|
||||
return fmt::format("// Unimplemented CFC2 VU control register: {}", rd);
|
||||
}
|
||||
}
|
||||
case COP2_QMTC2:
|
||||
return fmt::format("ctx->vu0_vf[{}] = _mm_castsi128_ps(GPR_VEC(ctx, {}));", rd, rt);
|
||||
case COP2_CTC2:
|
||||
{
|
||||
switch (rd) // Control register number is in rd
|
||||
if (rd < 16)
|
||||
{
|
||||
if (rd == 0)
|
||||
{
|
||||
return "// CTC2 write to VI0 ignored";
|
||||
}
|
||||
return fmt::format("ctx->vi[{}] = static_cast<uint16_t>(GPR_U32(ctx, {}));", rd, rt);
|
||||
}
|
||||
|
||||
switch (rd)
|
||||
{
|
||||
case VU0_CR_STATUS:
|
||||
return fmt::format("ctx->vu0_status = GPR_U32(ctx, {}) & 0xFFFF;", rt);
|
||||
return fmt::format("ctx->vu0_status = static_cast<uint16_t>(GPR_U32(ctx, {}) & 0xFFFFu);", rt);
|
||||
case VU0_CR_MAC:
|
||||
return fmt::format("ctx->vu0_mac_flags = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_TPC:
|
||||
case VU0_CR_VPU_STAT:
|
||||
return fmt::format("ctx->vu0_vpu_stat = GPR_U32(ctx, {});", rt);
|
||||
return fmt::format("// CTC2 write to read-only VU control register {} ignored", rd);
|
||||
case VU0_CR_CLIP:
|
||||
return fmt::format("ctx->vu0_clip_flags = GPR_U32(ctx, {});", rt);
|
||||
return fmt::format("ctx->vu0_clip_flags = GPR_U32(ctx, {}) & 0x00FFFFFFu;", rt);
|
||||
case VU0_CR_R:
|
||||
return fmt::format("ctx->vu0_r = _mm_castsi128_ps(GPR_VEC(ctx, {}));", rt);
|
||||
return fmt::format("ctx->vu0_r = _mm_castsi128_ps(_mm_set1_epi32(static_cast<int32_t>(GPR_U32(ctx, {}))));", rt);
|
||||
case VU0_CR_I:
|
||||
return fmt::format("{{ uint32_t tmp = GPR_U32(ctx, {}); std::memcpy(&ctx->vu0_i, &tmp, sizeof(tmp)); }}", rt);
|
||||
case VU0_CR_TPC:
|
||||
return fmt::format("ctx->vu0_tpc = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_Q:
|
||||
return fmt::format("{{ uint32_t tmp = GPR_U32(ctx, {}); std::memcpy(&ctx->vu0_q, &tmp, sizeof(tmp)); }}", rt);
|
||||
case VU0_CR_CMSAR0:
|
||||
return fmt::format("ctx->vu0_cmsar0 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_FBRST:
|
||||
return fmt::format("ctx->vu0_fbrst = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_VPU_STAT2:
|
||||
return fmt::format("ctx->vu0_vpu_stat2 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_TPC2:
|
||||
return fmt::format("ctx->vu0_tpc2 = GPR_U32(ctx, {});", rt);
|
||||
return fmt::format("ctx->vu0_fbrst = GPR_U32(ctx, {}) & 0x00000C0Cu;", rt);
|
||||
case VU0_CR_CMSAR1:
|
||||
return fmt::format("ctx->vu0_cmsar1 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_FBRST2:
|
||||
return fmt::format("ctx->vu0_fbrst2 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_VPU_STAT3:
|
||||
return fmt::format("ctx->vu0_vpu_stat3 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_CMSAR2:
|
||||
return fmt::format("ctx->vu0_cmsar2 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_FBRST3:
|
||||
return fmt::format("ctx->vu0_fbrst3 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_VPU_STAT4:
|
||||
return fmt::format("ctx->vu0_vpu_stat4 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_CMSAR3:
|
||||
return fmt::format("ctx->vu0_cmsar3 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_FBRST4:
|
||||
return fmt::format("ctx->vu0_fbrst4 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_ACC:
|
||||
return fmt::format("ctx->vu0_acc = _mm_castsi128_ps(GPR_VEC(ctx, {}));", rt);
|
||||
case VU0_CR_INFO:
|
||||
return fmt::format("ctx->vu0_info = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_CLIP2:
|
||||
return fmt::format("ctx->vu0_clip_flags2 = GPR_U32(ctx, {});", rt);
|
||||
case VU0_CR_P:
|
||||
return fmt::format("{{ uint32_t tmp = GPR_U32(ctx, {}); std::memcpy(&ctx->vu0_p, &tmp, sizeof(tmp)); }}", rt);
|
||||
case VU0_CR_XITOP:
|
||||
return fmt::format("ctx->vu0_xitop = GPR_U32(ctx, {}) & 0x3FF;", rt);
|
||||
case VU0_CR_ITOP:
|
||||
return fmt::format("ctx->vu0_itop = GPR_U32(ctx, {}) & 0x3FF;", rt);
|
||||
case VU0_CR_TOP:
|
||||
return fmt::format("ctx->vu0_top = GPR_U32(ctx, {}) & 0x3FF;", rt);
|
||||
default:
|
||||
return fmt::format("// Unimplemented CTC2 VU CReg: {}", rd);
|
||||
return fmt::format("// Unimplemented CTC2 VU control register: {}", rd);
|
||||
}
|
||||
}
|
||||
case COP2_BC:
|
||||
|
||||
+44
-43
@@ -15,6 +15,7 @@ option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime log
|
||||
option(PS2X_ENABLE_IOP_RPC_TRACE "Log unhandled IOP/SIF RPC trace suggestions" ON)
|
||||
option(PS2X_STRICT_RETURN_DIAGNOSTICS "Route generated JR $ra returns through runtime branch diagnostics" OFF)
|
||||
option(PS2X_SHOW_WINDOWS_CONSOLE "Show a console window for ps2EntryRunner on Windows release builds" ON)
|
||||
option(PS2X_ENABLE_DEBUG_UI "Build the desktop runtime debug UI" ON)
|
||||
|
||||
if(PS2X_ENABLE_SCCACHE)
|
||||
find_program(PS2X_SCCACHE_PROGRAM sccache)
|
||||
@@ -82,50 +83,50 @@ else()
|
||||
)
|
||||
FetchContent_MakeAvailable(raylib)
|
||||
|
||||
if(NOT PS2X_IS_ANDROID)
|
||||
FetchContent_Declare(
|
||||
imgui
|
||||
GIT_REPOSITORY https://github.com/ocornut/imgui.git
|
||||
GIT_TAG "docking"
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_GetProperties(imgui)
|
||||
if(PS2X_ENABLE_DEBUG_UI AND NOT PS2X_IS_ANDROID)
|
||||
FetchContent_Declare(
|
||||
imgui
|
||||
GIT_REPOSITORY https://github.com/ocornut/imgui.git
|
||||
GIT_TAG "v1.92.7-docking"
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_GetProperties(imgui)
|
||||
|
||||
if(NOT imgui_POPULATED)
|
||||
FetchContent_Populate(imgui)
|
||||
if(NOT imgui_POPULATED)
|
||||
FetchContent_Populate(imgui)
|
||||
endif()
|
||||
|
||||
set(PS2X_IMGUI_SOURCE_DIR "${imgui_SOURCE_DIR}")
|
||||
|
||||
add_library(imgui STATIC
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui.cpp"
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui_draw.cpp"
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui_tables.cpp"
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui_widgets.cpp"
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui_demo.cpp"
|
||||
)
|
||||
target_include_directories(imgui PUBLIC "${PS2X_IMGUI_SOURCE_DIR}")
|
||||
|
||||
FetchContent_Declare(
|
||||
rlImGui
|
||||
GIT_REPOSITORY https://github.com/raylib-extras/rlImGui.git
|
||||
GIT_TAG "Raylib_5_5"
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_GetProperties(rlImGui)
|
||||
|
||||
if(NOT rlimgui_POPULATED)
|
||||
FetchContent_Populate(rlImGui)
|
||||
endif()
|
||||
|
||||
set(PS2X_RLIMGUI_SOURCE_DIR "${rlimgui_SOURCE_DIR}")
|
||||
|
||||
add_library(rlImGui STATIC
|
||||
"${PS2X_RLIMGUI_SOURCE_DIR}/rlImGui.cpp"
|
||||
)
|
||||
target_include_directories(rlImGui PUBLIC "${PS2X_RLIMGUI_SOURCE_DIR}")
|
||||
target_link_libraries(rlImGui PUBLIC raylib imgui)
|
||||
endif()
|
||||
|
||||
set(PS2X_IMGUI_SOURCE_DIR "${imgui_SOURCE_DIR}")
|
||||
|
||||
add_library(imgui STATIC
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui.cpp"
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui_draw.cpp"
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui_tables.cpp"
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui_widgets.cpp"
|
||||
"${PS2X_IMGUI_SOURCE_DIR}/imgui_demo.cpp"
|
||||
)
|
||||
target_include_directories(imgui PUBLIC "${PS2X_IMGUI_SOURCE_DIR}")
|
||||
|
||||
FetchContent_Declare(
|
||||
rlImGui
|
||||
GIT_REPOSITORY https://github.com/raylib-extras/rlImGui.git
|
||||
GIT_TAG "Raylib_5_5"
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_GetProperties(rlImGui)
|
||||
|
||||
if(NOT rlimgui_POPULATED)
|
||||
FetchContent_Populate(rlImGui)
|
||||
endif()
|
||||
|
||||
set(PS2X_RLIMGUI_SOURCE_DIR "${rlimgui_SOURCE_DIR}")
|
||||
|
||||
add_library(rlImGui STATIC
|
||||
"${PS2X_RLIMGUI_SOURCE_DIR}/rlImGui.cpp"
|
||||
)
|
||||
target_include_directories(rlImGui PUBLIC "${PS2X_RLIMGUI_SOURCE_DIR}")
|
||||
target_link_libraries(rlImGui PUBLIC raylib imgui)
|
||||
endif() # NOT PS2X_IS_ANDROID
|
||||
endif()
|
||||
|
||||
add_library(ps2_host_backend INTERFACE)
|
||||
@@ -509,7 +510,7 @@ target_link_libraries(ps2EntryRunner
|
||||
ps2_runtime
|
||||
)
|
||||
|
||||
if(NOT PS2X_IS_VITA AND NOT PS2X_IS_ANDROID)
|
||||
if(PS2X_ENABLE_DEBUG_UI AND NOT PS2X_IS_VITA AND NOT PS2X_IS_ANDROID)
|
||||
target_sources(ps2EntryRunner PRIVATE
|
||||
src/lib/ps2_debug_panel.cpp
|
||||
)
|
||||
|
||||
@@ -19,6 +19,18 @@
|
||||
#define AGRESSIVE_LOGS 0
|
||||
#endif
|
||||
|
||||
#define RUNTIME_ERROR(x) \
|
||||
do \
|
||||
{ \
|
||||
std::ostringstream _ps2_runtime_error_stream; \
|
||||
_ps2_runtime_error_stream << x; \
|
||||
const std::string _ps2_runtime_error_text = \
|
||||
_ps2_runtime_error_stream.str(); \
|
||||
\
|
||||
std::cerr << _ps2_runtime_error_text; \
|
||||
ps2_log::append_runtime_log_text(_ps2_runtime_error_text); \
|
||||
} while (0)
|
||||
|
||||
namespace ps2_log
|
||||
{
|
||||
struct RuntimeLogEntry
|
||||
|
||||
@@ -477,8 +477,8 @@ private:
|
||||
std::unique_ptr<ps2x::iop::IopSubsystem> m_iopSubsystem;
|
||||
PS2AudioBackend m_audioBackend;
|
||||
PSPadBackend m_padBackend;
|
||||
VU1Interpreter m_vu0;
|
||||
VU1Interpreter m_vu1;
|
||||
VU1Interpreter m_vu0{VU1Interpreter::Unit::VU0};
|
||||
VU1Interpreter m_vu1{VU1Interpreter::Unit::VU1};
|
||||
R5900Context m_cpuContext;
|
||||
std::unique_ptr<EeScheduler> m_eeScheduler;
|
||||
mutable std::mutex m_eeKernelStateMutex;
|
||||
|
||||
@@ -399,12 +399,15 @@ private:
|
||||
|
||||
GSContext m_ctx[2];
|
||||
GSPrimReg m_prim{};
|
||||
GSPrimReg m_primRegister{};
|
||||
GSPrimReg m_prmodeRegister{};
|
||||
|
||||
uint8_t m_curR = 0x80, m_curG = 0x80, m_curB = 0x80, m_curA = 0x80;
|
||||
float m_curQ = 1.0f;
|
||||
float m_curS = 0.0f, m_curT = 0.0f;
|
||||
uint16_t m_curU = 0, m_curV = 0;
|
||||
uint8_t m_curFog = 0;
|
||||
uint8_t m_fogR = 0, m_fogG = 0, m_fogB = 0;
|
||||
|
||||
bool m_prmodecont = true;
|
||||
bool m_pabe = false;
|
||||
@@ -462,8 +465,9 @@ private:
|
||||
using WriteVramFunc = std::function<void(u8*, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t)>;
|
||||
using ReadVramFunc = std::function<u32(u8*, u32, u32, u32, u32)>;
|
||||
|
||||
std::array<ReadVramFunc, 0x3F> m_read_vram_funcs{ };
|
||||
std::array<WriteVramFunc, 0x3F> m_write_vram_funcs{ };
|
||||
static constexpr size_t kPsmHandlerCount = 1u << 6u;
|
||||
std::array<ReadVramFunc, kPsmHandlerCount> m_read_vram_funcs{ };
|
||||
std::array<WriteVramFunc, kPsmHandlerCount> m_write_vram_funcs{ };
|
||||
};
|
||||
|
||||
inline u32 GS::ReadVram(u32 psm, u32 base, u32 bw, u32 x, u32 y) const
|
||||
|
||||
@@ -9,7 +9,7 @@ class GSRasterizer
|
||||
{
|
||||
public:
|
||||
void drawPrimitive(GS *gs);
|
||||
void writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a);
|
||||
void writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t fog);
|
||||
uint32_t sampleTexture(GS *gs, float s, float t, float q, uint16_t u, uint16_t v);
|
||||
uint32_t lookupCLUT(GS *gs, uint8_t index, uint32_t cbp, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm);
|
||||
|
||||
|
||||
@@ -279,6 +279,7 @@ public:
|
||||
uint64_t gifCopyCount() const { return m_gifCopyCount.load(std::memory_order_relaxed); }
|
||||
uint64_t gsWriteCount() const { return m_gsWriteCount.load(std::memory_order_relaxed); }
|
||||
uint64_t vifWriteCount() const { return m_vifWriteCount.load(std::memory_order_relaxed); }
|
||||
uint64_t getVU0CodeGeneration() const { return m_vu0CodeGeneration.load(std::memory_order_relaxed); }
|
||||
uint64_t getVU1CodeGeneration() const { return m_vu1CodeGeneration.load(std::memory_order_relaxed); }
|
||||
|
||||
// Read/write memory
|
||||
@@ -366,6 +367,7 @@ public:
|
||||
std::atomic<uint64_t> m_gifCopyCount{0};
|
||||
std::atomic<uint64_t> m_gsWriteCount{0};
|
||||
std::atomic<uint64_t> m_vifWriteCount{0};
|
||||
std::atomic<uint64_t> m_vu0CodeGeneration{0};
|
||||
std::atomic<uint64_t> m_vu1CodeGeneration{0};
|
||||
// I/O registers
|
||||
std::unordered_map<uint32_t, uint32_t> m_ioRegisters;
|
||||
@@ -425,6 +427,7 @@ public:
|
||||
|
||||
bool isAddressInRegion(uint32_t address, const CodeRegion ®ion);
|
||||
void markModified(uint32_t address, uint32_t size);
|
||||
void markVU0CodeModified() { m_vu0CodeGeneration.fetch_add(1, std::memory_order_relaxed); }
|
||||
void markVU1CodeModified() { m_vu1CodeGeneration.fetch_add(1, std::memory_order_relaxed); }
|
||||
bool isScratchpad(uint32_t address) const;
|
||||
uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#ifndef PS2_VU1_H
|
||||
#define PS2_VU1_H
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class GS;
|
||||
class PS2Memory;
|
||||
@@ -15,13 +15,20 @@ struct VU1State
|
||||
float q;
|
||||
float p;
|
||||
float i;
|
||||
uint32_t r;
|
||||
uint32_t pc;
|
||||
uint32_t mac;
|
||||
uint32_t clip;
|
||||
uint32_t status;
|
||||
uint64_t cycles;
|
||||
bool ebit;
|
||||
uint32_t top; // VIF1 TOP visible to VU1 XTOP
|
||||
uint32_t itop; // VIF1 ITOP visible to VU1 XITOP
|
||||
bool haltAfterDelaySlot;
|
||||
bool dBitEnabled;
|
||||
bool tBitEnabled;
|
||||
bool stoppedByD;
|
||||
bool stoppedByT;
|
||||
uint32_t top; // VIF TOP visible to XTOP
|
||||
uint32_t itop; // VIF ITOP visible to XITOP
|
||||
|
||||
bool branchPending;
|
||||
uint32_t branchTarget;
|
||||
@@ -31,7 +38,13 @@ struct VU1State
|
||||
class VU1Interpreter
|
||||
{
|
||||
public:
|
||||
VU1Interpreter();
|
||||
enum class Unit : uint8_t
|
||||
{
|
||||
VU0,
|
||||
VU1
|
||||
};
|
||||
|
||||
explicit VU1Interpreter(Unit unit = Unit::VU1);
|
||||
|
||||
void reset();
|
||||
|
||||
@@ -50,38 +63,236 @@ public:
|
||||
const VU1State &state() const { return m_state; }
|
||||
|
||||
private:
|
||||
enum Pipeline : uint8_t
|
||||
{
|
||||
PipelineNone = 0,
|
||||
PipelineFmac,
|
||||
PipelineLsu,
|
||||
PipelineFdiv,
|
||||
PipelineEfu,
|
||||
PipelineIalu,
|
||||
PipelineBranch,
|
||||
PipelineXgkick
|
||||
};
|
||||
|
||||
struct VfAccess
|
||||
{
|
||||
uint8_t reg = 0;
|
||||
uint8_t lanes = 0;
|
||||
};
|
||||
|
||||
struct InstructionUsage
|
||||
{
|
||||
std::array<VfAccess, 2> vfRead{};
|
||||
VfAccess vfWrite{};
|
||||
uint8_t vfReadCount = 0;
|
||||
uint16_t viRead = 0;
|
||||
uint16_t viWrite = 0;
|
||||
uint8_t accRead = 0;
|
||||
uint8_t accWrite = 0;
|
||||
uint8_t latency = 0;
|
||||
uint8_t vfLatency = 0;
|
||||
uint8_t viLatency = 0;
|
||||
Pipeline pipeline = PipelineNone;
|
||||
bool waitQ = false;
|
||||
bool waitP = false;
|
||||
bool readsClip = false;
|
||||
bool writesClip = false;
|
||||
bool delaysNextBranchRead = false;
|
||||
bool reserved = false;
|
||||
};
|
||||
|
||||
struct DecodedInstructionPair
|
||||
{
|
||||
uint32_t lower = 0;
|
||||
uint32_t upper = 0;
|
||||
InstructionUsage lowerUsage{};
|
||||
InstructionUsage upperUsage{};
|
||||
bool iBit = false;
|
||||
bool eBit = false;
|
||||
bool lowerBeforeUpper = false;
|
||||
bool mBit = false;
|
||||
bool dBit = false;
|
||||
bool tBit = false;
|
||||
uint8_t upperVfShadowReg = 0;
|
||||
uint8_t suppressedLowerVf = 0;
|
||||
};
|
||||
|
||||
struct FlagPipelineEntry
|
||||
{
|
||||
uint64_t readyCycle = 0;
|
||||
uint64_t issueCycle = 0;
|
||||
uint32_t mac = 0;
|
||||
uint32_t status = 0;
|
||||
uint32_t extraSticky = 0;
|
||||
uint32_t clip = 0;
|
||||
bool valid = false;
|
||||
bool writesMac = false;
|
||||
bool writesStatus = false;
|
||||
bool writesSticky = false;
|
||||
bool writesClip = false;
|
||||
};
|
||||
|
||||
struct ScalarPipelineEntry
|
||||
{
|
||||
uint64_t readyCycle = 0;
|
||||
float value = 0.0f;
|
||||
uint32_t statusDi = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct PendingStore
|
||||
{
|
||||
uint64_t readyCycle = 0;
|
||||
uint32_t address = 0;
|
||||
std::array<uint32_t, 4> words{};
|
||||
uint8_t laneMask = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct PendingVfWrite
|
||||
{
|
||||
uint64_t readyCycle = 0;
|
||||
uint64_t sequence = 0;
|
||||
std::array<float, 4> value{};
|
||||
uint8_t reg = 0;
|
||||
uint8_t laneMask = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct PendingViWrite
|
||||
{
|
||||
uint64_t readyCycle = 0;
|
||||
uint64_t sequence = 0;
|
||||
int32_t value = 0;
|
||||
uint8_t reg = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct PendingAccWrite
|
||||
{
|
||||
uint64_t readyCycle = 0;
|
||||
uint64_t sequence = 0;
|
||||
std::array<float, 4> value{};
|
||||
uint8_t laneMask = 0;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct XgkickPipeline
|
||||
{
|
||||
static constexpr uint32_t kBufferSize = 0x10000u;
|
||||
std::array<uint8_t, kBufferSize> packet{};
|
||||
uint32_t sourceAddress = 0;
|
||||
uint32_t totalBytes = 0;
|
||||
uint32_t copiedBytes = 0;
|
||||
uint32_t currentTagEnd = 0;
|
||||
uint32_t cycleCredit = 0;
|
||||
uint64_t issueCycle = 0;
|
||||
bool active = false;
|
||||
bool currentTagEop = false;
|
||||
};
|
||||
|
||||
static constexpr uint32_t kFmacLatency = 4u;
|
||||
static constexpr uint32_t kAccForwardLatency = 1u;
|
||||
static constexpr uint32_t kMaxFlagEntries = 8u;
|
||||
static constexpr uint32_t kMaxPendingStores = 8u;
|
||||
static constexpr uint32_t kMaxPendingVfWrites = 16u;
|
||||
static constexpr uint32_t kMaxPendingViWrites = 8u;
|
||||
static constexpr uint32_t kMaxPendingAccWrites = 8u;
|
||||
static constexpr uint32_t kMaxDecodedPairs = 0x4000u / 8u;
|
||||
|
||||
Unit m_unit;
|
||||
VU1State m_state;
|
||||
std::vector<DecodedInstructionPair> m_decodedCodeCache;
|
||||
std::array<DecodedInstructionPair, kMaxDecodedPairs> m_decodedCodeCache{};
|
||||
const uint8_t *m_cachedVuCode = nullptr;
|
||||
const PS2Memory *m_cachedMemory = nullptr;
|
||||
uint32_t m_cachedCodeSize = 0;
|
||||
uint64_t m_cachedCodeGeneration = 0;
|
||||
bool m_decodedCodeCacheValid = false;
|
||||
|
||||
std::array<FlagPipelineEntry, kMaxFlagEntries> m_flagPipeline{};
|
||||
ScalarPipelineEntry m_fdiv{};
|
||||
std::array<ScalarPipelineEntry, 2> m_efu{};
|
||||
std::array<PendingStore, kMaxPendingStores> m_storePipeline{};
|
||||
std::array<PendingVfWrite, kMaxPendingVfWrites> m_vfWritePipeline{};
|
||||
std::array<PendingViWrite, kMaxPendingViWrites> m_viWritePipeline{};
|
||||
std::array<PendingAccWrite, kMaxPendingAccWrites> m_accWritePipeline{};
|
||||
XgkickPipeline m_xgkick{};
|
||||
std::array<std::array<uint64_t, 4>, 32> m_vfReady{};
|
||||
std::array<uint64_t, 16> m_viReady{};
|
||||
std::array<uint64_t, 4> m_accReady{};
|
||||
std::array<std::array<uint64_t, 4>, 32> m_vfLatestWrite{};
|
||||
std::array<uint64_t, 16> m_viLatestWrite{};
|
||||
std::array<uint64_t, 4> m_accLatestWrite{};
|
||||
|
||||
uint64_t m_cycle = 0;
|
||||
uint64_t m_nextWriteSequence = 0;
|
||||
uint64_t m_efuResourceReady = 0;
|
||||
uint32_t m_workingClip = 0;
|
||||
uint32_t m_currentUpperInstruction = 0;
|
||||
int32_t m_viBranchBackupValue = 0;
|
||||
uint8_t m_viBranchBackupReg = 0;
|
||||
bool m_viBranchBackupValid = false;
|
||||
uint8_t *m_activeVuData = nullptr;
|
||||
uint32_t m_activeVuDataSize = 0;
|
||||
GS *m_activeGs = nullptr;
|
||||
PS2Memory *m_activeMemory = nullptr;
|
||||
bool m_stopRequested = false;
|
||||
bool m_pendingHaltD = false;
|
||||
bool m_pendingHaltT = false;
|
||||
|
||||
void run(uint8_t *vuCode, uint32_t codeSize,
|
||||
uint8_t *vuData, uint32_t dataSize,
|
||||
GS &gs, PS2Memory *memory, uint32_t maxCycles);
|
||||
|
||||
InstructionUsage decodeUpperUsage(uint32_t upper) const;
|
||||
InstructionUsage decodeLowerUsage(uint32_t lower) const;
|
||||
static void addVfRead(InstructionUsage &usage, uint8_t reg, uint8_t lanes);
|
||||
static void addVfWrite(InstructionUsage &usage, uint8_t reg, uint8_t lanes);
|
||||
static uint8_t vfReadLanes(const InstructionUsage &usage, uint8_t reg);
|
||||
DecodedInstructionPair decodeInstructionPair(const uint8_t *vuCode, uint32_t pc) const;
|
||||
DecodedInstructionPair getDecodedInstructionPairForPc(const uint8_t *vuCode, uint32_t codeSize,
|
||||
PS2Memory *memory, uint32_t pc);
|
||||
void rebuildDecodedCodeCache(const uint8_t *vuCode, uint32_t codeSize,
|
||||
const PS2Memory *memory, uint64_t generation);
|
||||
DecodedInstructionPair getDecodedInstructionPairForPc(const uint8_t *vuCode, uint32_t codeSize, PS2Memory *memory, uint32_t pc);
|
||||
void rebuildDecodedCodeCache(const uint8_t *vuCode, uint32_t codeSize, const PS2Memory *memory, uint64_t generation);
|
||||
|
||||
void execUpper(uint32_t instr);
|
||||
void execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr);
|
||||
|
||||
void applyDest(float *dst, const float *result, uint8_t dest);
|
||||
void applyDestAcc(const float *result, uint8_t dest);
|
||||
void applyFmacDest(float *dst, float *result, uint8_t dest);
|
||||
void applyFmacDestAcc(float *result, uint8_t dest);
|
||||
void normalizeFmacResult(float *result, uint8_t dest, uint8_t laneFlags[4]);
|
||||
bool calculateFmacExactResult(uint32_t component, long double &result) const;
|
||||
uint8_t normalizeFmacExactResult(float &value, long double exactResult) const;
|
||||
uint32_t calculateFmacProductSticky(uint8_t dest) const;
|
||||
void updateFmacFlags(const uint8_t laneFlags[4], uint8_t dest, uint32_t extraSticky);
|
||||
void queueFsset(uint16_t immediate);
|
||||
void queueClip(uint32_t clip);
|
||||
void queueFcset(uint32_t clip);
|
||||
void queueQ(float value, uint32_t latency, uint32_t statusDi);
|
||||
void queueP(float value, uint32_t latency);
|
||||
void queueStore(uint32_t address, const uint32_t words[4], uint8_t laneMask);
|
||||
void queueVfWrite(uint8_t reg, uint8_t laneMask, const float value[4], uint32_t latency);
|
||||
void queueViWrite(uint8_t reg, int32_t value, uint32_t latency);
|
||||
void queueAccWrite(uint8_t laneMask, const float value[4], uint32_t latency);
|
||||
void startXgkick(uint32_t qwordAddress);
|
||||
|
||||
void resetScheduler();
|
||||
void commitReadyPipelines();
|
||||
void advanceOneCycle();
|
||||
void advanceTo(uint64_t targetCycle);
|
||||
void flushPipelines();
|
||||
void progressXgkick();
|
||||
void finishXgkick();
|
||||
uint64_t calculatePairReadyCycle(const DecodedInstructionPair &decoded) const;
|
||||
void markPairWrites(const DecodedInstructionPair &decoded);
|
||||
bool pipelinesPending() const;
|
||||
|
||||
float normalizeOperand(float value) const;
|
||||
float normalizeResult(float value, uint32_t &laneFlags) const;
|
||||
uint32_t microAddressMask() const;
|
||||
int32_t readBranchVi(uint8_t reg) const;
|
||||
void recordViWriteForBranch(uint8_t reg, int32_t oldValue);
|
||||
void reportReservedInstruction(bool upper, uint32_t instruction);
|
||||
float broadcast(const float *vf, uint8_t bc);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
#include "Common.h"
|
||||
#include "VU.h"
|
||||
//TODO use glm
|
||||
|
||||
namespace ps2_stubs
|
||||
{
|
||||
void sceVu0ecossin(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0ecossin", rdram, ctx, runtime);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -77,6 +72,7 @@ namespace ps2_stubs
|
||||
return true;
|
||||
}
|
||||
|
||||
// Row-major matrix product: out = lhs * rhs (lhs is the left factor).
|
||||
void mulVuMatrix(const float (&lhs)[16], const float (&rhs)[16], float (&out)[16])
|
||||
{
|
||||
std::fill(std::begin(out), std::end(out), 0.0f);
|
||||
@@ -100,6 +96,94 @@ namespace ps2_stubs
|
||||
out[10] = 1.0f;
|
||||
out[15] = 1.0f;
|
||||
}
|
||||
|
||||
// Rigid-transform inverse under the file's row-vector convention
|
||||
// (translation in row 3, ApplyMatrix computes v*M): transpose the 3x3
|
||||
// rotation block, zero its 4th column, and set the translation row to
|
||||
// -t*R^T -- each lane dots t with a ROW of R: out[12+col] =
|
||||
// -(t . R[col]). This makes M * rigidInverse(M) == I. Copy [15].
|
||||
void rigidInverse(const float (&in)[16], float (&out)[16])
|
||||
{
|
||||
for (int row = 0; row < 3; ++row)
|
||||
{
|
||||
for (int col = 0; col < 3; ++col)
|
||||
out[4 * row + col] = in[4 * col + row];
|
||||
out[4 * row + 3] = 0.0f;
|
||||
}
|
||||
const float tx = in[12], ty = in[13], tz = in[14];
|
||||
for (int col = 0; col < 3; ++col)
|
||||
out[12 + col] = -((tx * in[4 * col]) + (ty * in[4 * col + 1]) + (tz * in[4 * col + 2]));
|
||||
out[15] = in[15];
|
||||
}
|
||||
|
||||
void axisRotateMatrix(const float (&in)[16], float angle, int axis, float (&out)[16])
|
||||
{
|
||||
float rot[16]{};
|
||||
makeIdentityMatrix(rot);
|
||||
const float cs = std::cos(angle);
|
||||
const float sn = std::sin(angle);
|
||||
if (axis == 2)
|
||||
{
|
||||
rot[0] = cs;
|
||||
rot[1] = sn;
|
||||
rot[4] = -sn;
|
||||
rot[5] = cs;
|
||||
}
|
||||
else if (axis == 1)
|
||||
{
|
||||
rot[0] = cs;
|
||||
rot[2] = -sn;
|
||||
rot[8] = sn;
|
||||
rot[10] = cs;
|
||||
}
|
||||
else
|
||||
{
|
||||
rot[5] = cs;
|
||||
rot[6] = sn;
|
||||
rot[9] = -sn;
|
||||
rot[10] = cs;
|
||||
}
|
||||
mulVuMatrix(in, rot, out);
|
||||
}
|
||||
|
||||
// Applies the matrix to the vertex, perspective-divides xyz by w
|
||||
// (w==0 maps to a zero divide rather than Inf/NaN), then converts
|
||||
// x/y to 12.4 fixed point (x16) unconditionally. z/w take the same
|
||||
// x16 conversion when fullFtoi4 is set; otherwise they are plain
|
||||
// integer-truncated (FTOI0) after the divide instead.
|
||||
void rotTransPersOne(const float (&m)[16], const float (&v)[4], bool fullFtoi4, int32_t (&out)[4])
|
||||
{
|
||||
float t[4];
|
||||
t[0] = (m[0] * v[0]) + (m[4] * v[1]) + (m[8] * v[2]) + (m[12] * v[3]);
|
||||
t[1] = (m[1] * v[0]) + (m[5] * v[1]) + (m[9] * v[2]) + (m[13] * v[3]);
|
||||
t[2] = (m[2] * v[0]) + (m[6] * v[1]) + (m[10] * v[2]) + (m[14] * v[3]);
|
||||
t[3] = (m[3] * v[0]) + (m[7] * v[1]) + (m[11] * v[2]) + (m[15] * v[3]);
|
||||
const float q = (t[3] != 0.0f) ? (1.0f / t[3]) : 0.0f;
|
||||
t[0] *= q;
|
||||
t[1] *= q;
|
||||
t[2] *= q;
|
||||
out[0] = static_cast<int32_t>(t[0] * 16.0f);
|
||||
out[1] = static_cast<int32_t>(t[1] * 16.0f);
|
||||
out[2] = fullFtoi4 ? static_cast<int32_t>(t[2] * 16.0f) : static_cast<int32_t>(t[2]);
|
||||
out[3] = fullFtoi4 ? static_cast<int32_t>(t[3] * 16.0f) : static_cast<int32_t>(t[3]);
|
||||
}
|
||||
|
||||
// Guard-band proxy for the COP2 sticky clip flags: nonzero => the
|
||||
// vertex is offscreen. Not the hardware per-plane flag layout.
|
||||
constexpr float kScreenClipGuard = 4096.0f;
|
||||
int32_t screenClipCode(const float (&v)[4])
|
||||
{
|
||||
int32_t code = 0;
|
||||
if (v[0] > kScreenClipGuard)
|
||||
code |= 0x1;
|
||||
if (v[0] < -kScreenClipGuard)
|
||||
code |= 0x2;
|
||||
if (v[1] > kScreenClipGuard)
|
||||
code |= 0x4;
|
||||
if (v[1] < -kScreenClipGuard)
|
||||
code |= 0x8;
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
void sceVpu0Reset(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -147,27 +231,139 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0CameraMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0CameraMatrix", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t eyeAddr = getRegU32(ctx, 5);
|
||||
const uint32_t fwdAddr = getRegU32(ctx, 6);
|
||||
const uint32_t upAddr = getRegU32(ctx, 7);
|
||||
float eye[4]{}, fwd[4]{}, up[4]{};
|
||||
if (readVuVec4f(rdram, eyeAddr, eye) && readVuVec4f(rdram, fwdAddr, fwd) && readVuVec4f(rdram, upAddr, up))
|
||||
{
|
||||
auto cross = [](const float (&l)[4], const float (&r)[4], float (&o)[4])
|
||||
{
|
||||
o[0] = (l[1] * r[2]) - (l[2] * r[1]);
|
||||
o[1] = (l[2] * r[0]) - (l[0] * r[2]);
|
||||
o[2] = (l[0] * r[1]) - (l[1] * r[0]);
|
||||
o[3] = 0.0f;
|
||||
};
|
||||
auto normalize = [](const float (&s)[4], float (&o)[4])
|
||||
{
|
||||
const float len = std::sqrt((s[0] * s[0]) + (s[1] * s[1]) + (s[2] * s[2]) + (s[3] * s[3]));
|
||||
const float inv = (len > 1.0e-6f) ? (1.0f / len) : 0.0f;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
o[i] = s[i] * inv;
|
||||
};
|
||||
float rawCross[4]{}, row0[4]{}, row1[4]{}, row2[4]{};
|
||||
cross(up, fwd, rawCross);
|
||||
normalize(rawCross, row0);
|
||||
normalize(fwd, row2);
|
||||
cross(row2, row0, row1);
|
||||
|
||||
float m[16]{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
m[i] = row0[i];
|
||||
m[4 + i] = row1[i];
|
||||
m[8 + i] = row2[i];
|
||||
}
|
||||
m[12] = eye[0];
|
||||
m[13] = eye[1];
|
||||
m[14] = eye[2];
|
||||
m[15] = 1.0f;
|
||||
|
||||
float out[16]{};
|
||||
rigidInverse(m, out);
|
||||
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0ClampVector(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0ClampVector", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t srcAddr = getRegU32(ctx, 5);
|
||||
const float lo = ctx ? ctx->f[12] : 0.0f;
|
||||
const float hi = ctx ? ctx->f[13] : 0.0f;
|
||||
float src[4]{}, out[4]{};
|
||||
if (readVuVec4f(rdram, srcAddr, src))
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
float v = src[i];
|
||||
v = (v < lo) ? lo : v;
|
||||
v = (v > hi) ? hi : v;
|
||||
out[i] = v;
|
||||
}
|
||||
(void)writeVuVec4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0ClipAll(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0ClipAll", rdram, ctx, runtime);
|
||||
const uint32_t loAddr = getRegU32(ctx, 4);
|
||||
const uint32_t hiAddr = getRegU32(ctx, 5);
|
||||
const uint32_t matAddr = getRegU32(ctx, 6);
|
||||
uint32_t vAddr = getRegU32(ctx, 7);
|
||||
const int32_t count = static_cast<int32_t>(getRegU32(ctx, 8));
|
||||
float lo[4]{}, hi[4]{}, m[16]{};
|
||||
int32_t result = 0;
|
||||
if (readVuVec4f(rdram, loAddr, lo) && readVuVec4f(rdram, hiAddr, hi) && readVuMatrix4f(rdram, matAddr, m))
|
||||
{
|
||||
result = 1;
|
||||
for (int32_t i = 0; i < count; ++i)
|
||||
{
|
||||
float v[4]{};
|
||||
if (!readVuVec4f(rdram, vAddr, v))
|
||||
break;
|
||||
const float tx = (m[0] * v[0]) + (m[4] * v[1]) + (m[8] * v[2]) + (m[12] * v[3]);
|
||||
const float ty = (m[1] * v[0]) + (m[5] * v[1]) + (m[9] * v[2]) + (m[13] * v[3]);
|
||||
const float tw = (m[3] * v[0]) + (m[7] * v[1]) + (m[11] * v[2]) + (m[15] * v[3]);
|
||||
const bool outsideX = (tx < lo[0] * tw) || (tx > hi[0] * tw);
|
||||
const bool outsideY = (ty < lo[1] * tw) || (ty > hi[1] * tw);
|
||||
if (!outsideX && !outsideY)
|
||||
{
|
||||
result = 0;
|
||||
break;
|
||||
}
|
||||
vAddr += 16u;
|
||||
}
|
||||
}
|
||||
setReturnS32(ctx, result);
|
||||
}
|
||||
|
||||
void sceVu0ClipScreen(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0ClipScreen", rdram, ctx, runtime);
|
||||
const uint32_t vAddr = getRegU32(ctx, 4);
|
||||
float v[4]{};
|
||||
int32_t code = 0;
|
||||
if (readVuVec4f(rdram, vAddr, v))
|
||||
{
|
||||
code = screenClipCode(v);
|
||||
}
|
||||
setReturnS32(ctx, code);
|
||||
}
|
||||
|
||||
void sceVu0ClipScreen3(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0ClipScreen3", rdram, ctx, runtime);
|
||||
const uint32_t v0Addr = getRegU32(ctx, 4);
|
||||
const uint32_t v1Addr = getRegU32(ctx, 5);
|
||||
const uint32_t v2Addr = getRegU32(ctx, 6);
|
||||
float v0[4]{}, v1[4]{}, v2[4]{};
|
||||
int32_t code = 0;
|
||||
if (readVuVec4f(rdram, v0Addr, v0))
|
||||
{
|
||||
code |= screenClipCode(v0);
|
||||
}
|
||||
if (readVuVec4f(rdram, v1Addr, v1))
|
||||
{
|
||||
code |= screenClipCode(v1);
|
||||
}
|
||||
if (readVuVec4f(rdram, v2Addr, v2))
|
||||
{
|
||||
code |= screenClipCode(v2);
|
||||
}
|
||||
setReturnS32(ctx, code);
|
||||
}
|
||||
|
||||
void sceVu0CopyMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -211,17 +407,108 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0DivVector(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0DivVector", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t srcAddr = getRegU32(ctx, 5);
|
||||
const float divisor = ctx ? ctx->f[12] : 1.0f;
|
||||
float src[4]{}, out[4]{};
|
||||
if (readVuVec4f(rdram, srcAddr, src))
|
||||
{
|
||||
const float q = (divisor != 0.0f) ? (1.0f / divisor) : 0.0f;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
out[i] = src[i] * q;
|
||||
}
|
||||
(void)writeVuVec4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0DivVectorXYZ(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0DivVectorXYZ", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t srcAddr = getRegU32(ctx, 5);
|
||||
const float divisor = ctx ? ctx->f[12] : 1.0f;
|
||||
float src[4]{}, out[4]{};
|
||||
if (readVuVec4f(rdram, srcAddr, src))
|
||||
{
|
||||
const float q = (divisor != 0.0f) ? (1.0f / divisor) : 0.0f;
|
||||
out[0] = src[0] * q;
|
||||
out[1] = src[1] * q;
|
||||
out[2] = src[2] * q;
|
||||
out[3] = src[3];
|
||||
(void)writeVuVec4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0DropShadowMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0DropShadowMatrix", rdram, ctx, runtime);
|
||||
// Two documented drop-shadow modes reconstructed from behavior; not
|
||||
// claimed bit-exact to a specific SDK build.
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t nAddr = getRegU32(ctx, 5);
|
||||
const uint32_t mode = getRegU32(ctx, 6);
|
||||
const float lx = ctx ? ctx->f[12] : 0.0f;
|
||||
const float ly = ctx ? ctx->f[13] : 0.0f;
|
||||
const float lz = ctx ? ctx->f[14] : 0.0f;
|
||||
float n[4]{};
|
||||
if (readVuVec4f(rdram, nAddr, n))
|
||||
{
|
||||
const float nx = n[0], ny = n[1], nz = n[2];
|
||||
const float d = (lx * nx) + (ly * ny) + (lz * nz);
|
||||
float out[16]{};
|
||||
if (mode != 0)
|
||||
{
|
||||
const float k = 1.0f - d;
|
||||
out[0] = (lx * nx) + k;
|
||||
out[1] = lx * ny;
|
||||
out[2] = lx * nz;
|
||||
out[3] = lx;
|
||||
out[4] = ly * nx;
|
||||
out[5] = (ly * ny) + k;
|
||||
out[6] = ly * nz;
|
||||
out[7] = ly;
|
||||
out[8] = lz * nx;
|
||||
out[9] = lz * ny;
|
||||
out[10] = (lz * nz) + k;
|
||||
out[11] = lz;
|
||||
out[12] = -nx;
|
||||
out[13] = -ny;
|
||||
out[14] = -nz;
|
||||
out[15] = -d;
|
||||
}
|
||||
else
|
||||
{
|
||||
const float k = (d != 0.0f) ? (-1.0f / d) : 0.0f;
|
||||
out[0] = k * ((lx * nx) - d);
|
||||
out[1] = k * (lx * ny);
|
||||
out[2] = k * (lx * nz);
|
||||
out[3] = 0.0f;
|
||||
out[4] = k * (ly * nx);
|
||||
out[5] = k * ((ly * ny) - d);
|
||||
out[6] = k * (ly * nz);
|
||||
out[7] = 0.0f;
|
||||
out[8] = k * (lz * nx);
|
||||
out[9] = k * (lz * ny);
|
||||
out[10] = k * ((lz * nz) - d);
|
||||
out[11] = 0.0f;
|
||||
out[12] = k * -nx;
|
||||
out[13] = k * -ny;
|
||||
out[14] = k * -nz;
|
||||
out[15] = 1.0f;
|
||||
}
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0ecossin(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const float angle = ctx ? ctx->f[12] : 0.0f;
|
||||
float out[4] = {std::cos(angle), std::sin(angle), 0.0f, 0.0f};
|
||||
(void)writeVuVec4f(rdram, dstAddr, out);
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0FTOI0Vector(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -280,17 +567,53 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0InterVector(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0InterVector", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t aAddr = getRegU32(ctx, 5);
|
||||
const uint32_t bAddr = getRegU32(ctx, 6);
|
||||
const float t = ctx ? ctx->f[12] : 0.0f;
|
||||
float a[4]{}, b[4]{}, out[4]{};
|
||||
if (readVuVec4f(rdram, aAddr, a) && readVuVec4f(rdram, bAddr, b))
|
||||
{
|
||||
const float invT = 1.0f - t;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
out[i] = (a[i] * t) + (b[i] * invT);
|
||||
}
|
||||
(void)writeVuVec4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0InterVectorXYZ(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0InterVectorXYZ", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t aAddr = getRegU32(ctx, 5);
|
||||
const uint32_t bAddr = getRegU32(ctx, 6);
|
||||
const float t = ctx ? ctx->f[12] : 0.0f;
|
||||
float a[4]{}, b[4]{}, out[4]{};
|
||||
if (readVuVec4f(rdram, aAddr, a) && readVuVec4f(rdram, bAddr, b))
|
||||
{
|
||||
const float invT = 1.0f - t;
|
||||
out[0] = (a[0] * t) + (b[0] * invT);
|
||||
out[1] = (a[1] * t) + (b[1] * invT);
|
||||
out[2] = (a[2] * t) + (b[2] * invT);
|
||||
out[3] = a[3];
|
||||
(void)writeVuVec4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0InversMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0InversMatrix", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t srcAddr = getRegU32(ctx, 5);
|
||||
float in[16]{}, out[16]{};
|
||||
if (readVuMatrix4f(rdram, srcAddr, in))
|
||||
{
|
||||
rigidInverse(in, out);
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0ITOF0Vector(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -346,17 +669,60 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0LightColorMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0LightColorMatrix", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t c0Addr = getRegU32(ctx, 5);
|
||||
const uint32_t c1Addr = getRegU32(ctx, 6);
|
||||
const uint32_t c2Addr = getRegU32(ctx, 7);
|
||||
const uint32_t c3Addr = getRegU32(ctx, 8);
|
||||
float c0[4]{}, c1[4]{}, c2[4]{}, c3[4]{};
|
||||
if (readVuVec4f(rdram, c0Addr, c0) && readVuVec4f(rdram, c1Addr, c1) &&
|
||||
readVuVec4f(rdram, c2Addr, c2) && readVuVec4f(rdram, c3Addr, c3))
|
||||
{
|
||||
float out[16]{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
out[i] = c0[i];
|
||||
out[4 + i] = c1[i];
|
||||
out[8 + i] = c2[i];
|
||||
out[12 + i] = c3[i];
|
||||
}
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0MulMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0MulMatrix", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t m0Addr = getRegU32(ctx, 5);
|
||||
const uint32_t m1Addr = getRegU32(ctx, 6);
|
||||
float m0[16]{}, m1[16]{}, out[16]{};
|
||||
if (readVuMatrix4f(rdram, m0Addr, m0) && readVuMatrix4f(rdram, m1Addr, m1))
|
||||
{
|
||||
// out = m0 * m1 (first source . second source), matching the
|
||||
// file's mulVuMatrix(lhs,rhs)=lhs.rhs convention and the RotMatrix
|
||||
// / ViewScreenMatrix siblings (first operand on the left).
|
||||
mulVuMatrix(m0, m1, out);
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0MulVector(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0MulVector", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t lhsAddr = getRegU32(ctx, 5);
|
||||
const uint32_t rhsAddr = getRegU32(ctx, 6);
|
||||
float lhs[4]{}, rhs[4]{}, out[4]{};
|
||||
if (readVuVec4f(rdram, lhsAddr, lhs) && readVuVec4f(rdram, rhsAddr, rhs))
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
out[i] = lhs[i] * rhs[i];
|
||||
}
|
||||
(void)writeVuVec4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0Normalize(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -382,7 +748,42 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0NormalLightMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0NormalLightMatrix", rdram, ctx, runtime);
|
||||
// Rows = normalize(-light); the 4x4 is transposed so directions occupy
|
||||
// columns (one ApplyMatrix then yields per-light N.L).
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t l0Addr = getRegU32(ctx, 5);
|
||||
const uint32_t l1Addr = getRegU32(ctx, 6);
|
||||
const uint32_t l2Addr = getRegU32(ctx, 7);
|
||||
float l0[4]{}, l1[4]{}, l2[4]{};
|
||||
if (readVuVec4f(rdram, l0Addr, l0) && readVuVec4f(rdram, l1Addr, l1) && readVuVec4f(rdram, l2Addr, l2))
|
||||
{
|
||||
auto negNormalize = [](const float (&s)[4], float (&o)[4])
|
||||
{
|
||||
const float len = std::sqrt((s[0] * s[0]) + (s[1] * s[1]) + (s[2] * s[2]) + (s[3] * s[3]));
|
||||
const float inv = (len > 1.0e-6f) ? (1.0f / len) : 0.0f;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
o[i] = -s[i] * inv;
|
||||
};
|
||||
float r0[4]{}, r1[4]{}, r2[4]{};
|
||||
negNormalize(l0, r0);
|
||||
negNormalize(l1, r1);
|
||||
negNormalize(l2, r2);
|
||||
float m[16]{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
m[i] = r0[i];
|
||||
m[4 + i] = r1[i];
|
||||
m[8 + i] = r2[i];
|
||||
m[12 + i] = 0.0f;
|
||||
}
|
||||
m[15] = 1.0f;
|
||||
float out[16]{};
|
||||
for (int row = 0; row < 4; ++row)
|
||||
for (int col = 0; col < 4; ++col)
|
||||
out[4 * row + col] = m[4 * col + row];
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0OuterProduct(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -404,7 +805,19 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0RotMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0RotMatrix", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t srcAddr = getRegU32(ctx, 5);
|
||||
const uint32_t rotAddr = getRegU32(ctx, 6);
|
||||
float src[16]{}, rotVec[4]{};
|
||||
if (readVuMatrix4f(rdram, srcAddr, src) && readVuVec4f(rdram, rotAddr, rotVec))
|
||||
{
|
||||
float afterZ[16]{}, afterY[16]{}, afterX[16]{};
|
||||
axisRotateMatrix(src, rotVec[2], 2, afterZ);
|
||||
axisRotateMatrix(afterZ, rotVec[1], 1, afterY);
|
||||
axisRotateMatrix(afterY, rotVec[0], 0, afterX);
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, afterX);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0RotMatrixX(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -472,12 +885,44 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0RotTransPers(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0RotTransPers", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t matAddr = getRegU32(ctx, 5);
|
||||
const uint32_t vAddr = getRegU32(ctx, 6);
|
||||
const bool fullFtoi4 = (getRegU32(ctx, 7) != 0);
|
||||
float m[16]{}, v[4]{};
|
||||
if (readVuMatrix4f(rdram, matAddr, m) && readVuVec4f(rdram, vAddr, v))
|
||||
{
|
||||
int32_t out[4]{};
|
||||
rotTransPersOne(m, v, fullFtoi4, out);
|
||||
(void)writeVuVec4i(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0RotTransPersN(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0RotTransPersN", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t matAddr = getRegU32(ctx, 5);
|
||||
uint32_t vAddr = getRegU32(ctx, 6);
|
||||
const int32_t count = static_cast<int32_t>(getRegU32(ctx, 7));
|
||||
const bool fullFtoi4 = (getRegU32(ctx, 8) != 0);
|
||||
float m[16]{};
|
||||
if (readVuMatrix4f(rdram, matAddr, m))
|
||||
{
|
||||
uint32_t outAddr = dstAddr;
|
||||
for (int32_t i = 0; i < count; ++i)
|
||||
{
|
||||
float v[4]{};
|
||||
if (!readVuVec4f(rdram, vAddr, v))
|
||||
break;
|
||||
int32_t out[4]{};
|
||||
rotTransPersOne(m, v, fullFtoi4, out);
|
||||
(void)writeVuVec4i(rdram, outAddr, out);
|
||||
vAddr += 16u;
|
||||
outAddr += 16u;
|
||||
}
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0ScaleVector(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -509,7 +954,19 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0ScaleVectorXYZ(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0ScaleVectorXYZ", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t srcAddr = getRegU32(ctx, 5);
|
||||
const float scale = ctx ? ctx->f[12] : 0.0f;
|
||||
float src[4]{}, out[4]{};
|
||||
if (readVuVec4f(rdram, srcAddr, src))
|
||||
{
|
||||
out[0] = src[0] * scale;
|
||||
out[1] = src[1] * scale;
|
||||
out[2] = src[2] * scale;
|
||||
out[3] = src[3];
|
||||
(void)writeVuVec4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0SubVector(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -531,7 +988,20 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0TransMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0TransMatrix", rdram, ctx, runtime);
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const uint32_t srcAddr = getRegU32(ctx, 5);
|
||||
const uint32_t vAddr = getRegU32(ctx, 6);
|
||||
float src[16]{}, v[4]{};
|
||||
if (readVuMatrix4f(rdram, srcAddr, src) && readVuVec4f(rdram, vAddr, v))
|
||||
{
|
||||
float out[16]{};
|
||||
std::memcpy(out, src, sizeof(out));
|
||||
out[12] = src[12] + v[0];
|
||||
out[13] = src[13] + v[1];
|
||||
out[14] = src[14] + v[2];
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, out);
|
||||
}
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
|
||||
void sceVu0TransposeMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
@@ -579,6 +1049,55 @@ namespace ps2_stubs
|
||||
|
||||
void sceVu0ViewScreenMatrix(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
TODO_NAMED("sceVu0ViewScreenMatrix", rdram, ctx, runtime);
|
||||
// Near/far params handled by formula shape; SDK parameter names
|
||||
// not pinned. Args follow the out-of-line libvu0 register convention
|
||||
// used throughout this file: eight scalar floats in f12..f19 (p0..p7)
|
||||
// and the ninth (p8) as the first stack-passed argument. That
|
||||
// eight-FP-arg-register layout is the n32/EABI convention the EE
|
||||
// toolchain emits (an o32 layout would carry only two FP args in
|
||||
// f12/f14 and spill p2..p7 too), and under it no GPR home/save area is
|
||||
// reserved, so the ninth float is at 0(sp) -- read below. A target ABI
|
||||
// that reserves a home area would shift only that read by its size.
|
||||
const uint32_t dstAddr = getRegU32(ctx, 4);
|
||||
const float p0 = ctx ? ctx->f[12] : 0.0f;
|
||||
const float p1 = ctx ? ctx->f[13] : 0.0f;
|
||||
const float p2 = ctx ? ctx->f[14] : 0.0f;
|
||||
const float p3 = ctx ? ctx->f[15] : 0.0f;
|
||||
const float p4 = ctx ? ctx->f[16] : 0.0f;
|
||||
const float p5 = ctx ? ctx->f[17] : 0.0f;
|
||||
const float p6 = ctx ? ctx->f[18] : 0.0f;
|
||||
const float p7 = ctx ? ctx->f[19] : 0.0f;
|
||||
float p8 = 0.0f;
|
||||
if (const uint8_t *sp = getConstMemPtr(rdram, getRegU32(ctx, 29)))
|
||||
{
|
||||
std::memcpy(&p8, sp, sizeof(p8));
|
||||
}
|
||||
|
||||
const float denom = p8 - p7;
|
||||
const float zScale = (denom != 0.0f) ? ((p8 * p7 * (p6 - p5)) / denom) : 0.0f;
|
||||
const float zOffset = (denom != 0.0f) ? (((p5 * p8) - (p6 * p7)) / denom) : 0.0f;
|
||||
|
||||
float scaleMat[16]{};
|
||||
makeIdentityMatrix(scaleMat);
|
||||
scaleMat[0] = p0;
|
||||
scaleMat[5] = p0;
|
||||
scaleMat[10] = 0.0f;
|
||||
scaleMat[11] = 1.0f;
|
||||
scaleMat[14] = 1.0f;
|
||||
scaleMat[15] = 0.0f;
|
||||
|
||||
float projMat[16]{};
|
||||
makeIdentityMatrix(projMat);
|
||||
projMat[0] = p1;
|
||||
projMat[5] = p2;
|
||||
projMat[10] = zScale;
|
||||
projMat[12] = p3;
|
||||
projMat[13] = p4;
|
||||
projMat[14] = zOffset;
|
||||
|
||||
float out[16]{};
|
||||
mulVuMatrix(scaleMat, projMat, out);
|
||||
(void)writeVuMatrix4f(rdram, dstAddr, out);
|
||||
setReturnS32(ctx, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,21 @@ namespace
|
||||
static constexpr uint32_t kHostFrameWidth = 640u;
|
||||
static constexpr uint32_t kHostFrameHeight = 512u;
|
||||
|
||||
GSPrimReg decodePrimRegister(uint64_t value)
|
||||
{
|
||||
GSPrimReg prim{};
|
||||
prim.type = static_cast<GSPrimType>(value & 0x7u);
|
||||
prim.iip = ((value >> 3) & 1u) != 0u;
|
||||
prim.tme = ((value >> 4) & 1u) != 0u;
|
||||
prim.fge = ((value >> 5) & 1u) != 0u;
|
||||
prim.abe = ((value >> 6) & 1u) != 0u;
|
||||
prim.aa1 = ((value >> 7) & 1u) != 0u;
|
||||
prim.fst = ((value >> 8) & 1u) != 0u;
|
||||
prim.ctxt = ((value >> 9) & 1u) != 0u;
|
||||
prim.fix = ((value >> 10) & 1u) != 0u;
|
||||
return prim;
|
||||
}
|
||||
|
||||
uint16_t encodeFramePixelPSMCT16(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
|
||||
{
|
||||
return static_cast<uint16_t>(((r >> 3) & 0x1Fu) |
|
||||
@@ -109,7 +124,8 @@ namespace
|
||||
|
||||
bool validatePackedGifPacket(const uint8_t *data, uint32_t sizeBytes)
|
||||
{
|
||||
return visitPackedGifPacket(data, sizeBytes, [](const PackedGifPacketTag &) { return true; });
|
||||
return visitPackedGifPacket(data, sizeBytes, [](const PackedGifPacketTag &)
|
||||
{ return true; });
|
||||
}
|
||||
|
||||
void decodeDisplaySize(uint64_t display64, uint32_t &outWidth, uint32_t &outHeight)
|
||||
@@ -265,7 +281,7 @@ namespace
|
||||
return count;
|
||||
}
|
||||
|
||||
bool clearFramebufferRect(GS* gs, const GSContext &ctx, uint32_t rgba)
|
||||
bool clearFramebufferRect(GS *gs, const GSContext &ctx, uint32_t rgba)
|
||||
{
|
||||
if (ctx.frame.fbw == 0u)
|
||||
{
|
||||
@@ -365,7 +381,7 @@ GS::GS()
|
||||
|
||||
InitLookupTables();
|
||||
|
||||
for (usz i = 0; i < 0x3F; ++i)
|
||||
for (usz i = 0; i < m_read_vram_funcs.size(); ++i)
|
||||
{
|
||||
switch (i)
|
||||
{
|
||||
@@ -444,6 +460,8 @@ void GS::reset()
|
||||
std::lock_guard<std::recursive_mutex> lock(m_stateMutex);
|
||||
std::memset(m_ctx, 0, sizeof(m_ctx));
|
||||
m_prim = {};
|
||||
m_primRegister = {};
|
||||
m_prmodeRegister = {};
|
||||
m_curR = 0x80;
|
||||
m_curG = 0x80;
|
||||
m_curB = 0x80;
|
||||
@@ -454,6 +472,9 @@ void GS::reset()
|
||||
m_curU = 0;
|
||||
m_curV = 0;
|
||||
m_curFog = 0;
|
||||
m_fogR = 0;
|
||||
m_fogG = 0;
|
||||
m_fogB = 0;
|
||||
m_prmodecont = true;
|
||||
m_pabe = false;
|
||||
m_texa = {0u, false, 0u};
|
||||
@@ -553,7 +574,6 @@ GSDebugSnapshot GS::getDebugSnapshot() const
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
|
||||
std::vector<GSDebugHistoryEntry> GS::getDebugHistory() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(m_stateMutex);
|
||||
@@ -1315,7 +1335,6 @@ void GS::processGIFPacket(const uint8_t *data, uint32_t sizeBytes)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
uint32_t offset = 0;
|
||||
while (offset + 16 <= sizeBytes)
|
||||
{
|
||||
@@ -1394,7 +1413,7 @@ bool GS::processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes)
|
||||
return false;
|
||||
|
||||
const bool processed = visitPackedGifPacket(data, sizeBytes, [&](const PackedGifPacketTag &tag)
|
||||
{
|
||||
{
|
||||
m_curQ = 1.0f;
|
||||
|
||||
recordGifTagDebugEventUnlocked(sizeBytes, tag.nloop, GIF_FMT_PACKED, tag.nreg);
|
||||
@@ -1415,8 +1434,7 @@ bool GS::processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes)
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return true; });
|
||||
|
||||
if (!processed)
|
||||
return false;
|
||||
@@ -1775,15 +1793,17 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value)
|
||||
{
|
||||
case GS_REG_PRIM:
|
||||
{
|
||||
m_prim.type = static_cast<GSPrimType>(value & 0x7);
|
||||
m_prim.iip = ((value >> 3) & 1) != 0;
|
||||
m_prim.tme = ((value >> 4) & 1) != 0;
|
||||
m_prim.fge = ((value >> 5) & 1) != 0;
|
||||
m_prim.abe = ((value >> 6) & 1) != 0;
|
||||
m_prim.aa1 = ((value >> 7) & 1) != 0;
|
||||
m_prim.fst = ((value >> 8) & 1) != 0;
|
||||
m_prim.ctxt = ((value >> 9) & 1) != 0;
|
||||
m_prim.fix = ((value >> 10) & 1) != 0;
|
||||
m_primRegister = decodePrimRegister(value);
|
||||
if (m_prmodecont)
|
||||
{
|
||||
m_prim = m_primRegister;
|
||||
}
|
||||
else
|
||||
{
|
||||
// PRIM always selects the primitive topology. With AC=0, all
|
||||
// rendering attributes remain sourced from PRMODE.
|
||||
m_prim.type = m_primRegister.type;
|
||||
}
|
||||
m_vtxCount = 0;
|
||||
m_vtxIndex = 0;
|
||||
break;
|
||||
@@ -1912,21 +1932,24 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value)
|
||||
break;
|
||||
}
|
||||
case GS_REG_PRMODECONT:
|
||||
{
|
||||
m_prmodecont = (value & 1) != 0;
|
||||
const GSPrimType type = m_primRegister.type;
|
||||
m_prim = m_prmodecont ? m_primRegister : m_prmodeRegister;
|
||||
m_prim.type = type;
|
||||
break;
|
||||
}
|
||||
case GS_REG_PRMODE:
|
||||
{
|
||||
m_prmodeRegister = decodePrimRegister(value);
|
||||
if (!m_prmodecont)
|
||||
{
|
||||
m_prim.iip = ((value >> 3) & 1) != 0;
|
||||
m_prim.tme = ((value >> 4) & 1) != 0;
|
||||
m_prim.fge = ((value >> 5) & 1) != 0;
|
||||
m_prim.abe = ((value >> 6) & 1) != 0;
|
||||
m_prim.aa1 = ((value >> 7) & 1) != 0;
|
||||
m_prim.fst = ((value >> 8) & 1) != 0;
|
||||
m_prim.ctxt = ((value >> 9) & 1) != 0;
|
||||
m_prim.fix = ((value >> 10) & 1) != 0;
|
||||
const GSPrimType type = m_primRegister.type;
|
||||
m_prim = m_prmodeRegister;
|
||||
m_prim.type = type;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GS_REG_TEXCLUT:
|
||||
m_texclut.cbw = static_cast<uint8_t>(value & 0x3Fu);
|
||||
m_texclut.cou = static_cast<uint8_t>((value >> 6) & 0x3Fu);
|
||||
@@ -2041,9 +2064,13 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value)
|
||||
case GS_REG_PABE:
|
||||
m_pabe = (value & 1u) != 0u;
|
||||
break;
|
||||
case GS_REG_FOGCOL:
|
||||
m_fogR = static_cast<uint8_t>(value & 0xFFu);
|
||||
m_fogG = static_cast<uint8_t>((value >> 8) & 0xFFu);
|
||||
m_fogB = static_cast<uint8_t>((value >> 16) & 0xFFu);
|
||||
break;
|
||||
case GS_REG_TEXFLUSH:
|
||||
case GS_REG_SCANMSK:
|
||||
case GS_REG_FOGCOL:
|
||||
case GS_REG_DIMX:
|
||||
case GS_REG_DTHE:
|
||||
case GS_REG_COLCLAMP:
|
||||
@@ -2180,7 +2207,6 @@ void GS::performLocalToLocalTransfer()
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
// left -> right
|
||||
// bottom -> top (invert y)
|
||||
case 1:
|
||||
@@ -2271,9 +2297,6 @@ void GS::vertexKick(bool drawing)
|
||||
}
|
||||
});
|
||||
|
||||
if (!drawing)
|
||||
return;
|
||||
|
||||
int needed = 0;
|
||||
switch (m_prim.type)
|
||||
{
|
||||
@@ -2305,8 +2328,11 @@ void GS::vertexKick(bool drawing)
|
||||
if (m_vtxCount < needed)
|
||||
return;
|
||||
|
||||
m_rasterizer.drawPrimitive(this);
|
||||
recordDrawDebugEventUnlocked(needed);
|
||||
if (drawing)
|
||||
{
|
||||
m_rasterizer.drawPrimitive(this);
|
||||
recordDrawDebugEventUnlocked(needed);
|
||||
}
|
||||
|
||||
switch (m_prim.type)
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
using namespace GSInternal;
|
||||
|
||||
@@ -27,8 +26,8 @@ namespace
|
||||
|
||||
u16 Rgba8888ToRgba5551(u32 c)
|
||||
{
|
||||
uint32_t r = ((c >> 0) & 0xFF) >> 3;
|
||||
uint32_t g = ((c >> 8) & 0xFF) >> 3;
|
||||
uint32_t r = ((c >> 0) & 0xFF) >> 3;
|
||||
uint32_t g = ((c >> 8) & 0xFF) >> 3;
|
||||
uint32_t b = ((c >> 16) & 0xFF) >> 3;
|
||||
uint32_t a = ((c >> 24) & 0xFF) >> 7;
|
||||
|
||||
@@ -37,8 +36,8 @@ namespace
|
||||
|
||||
u32 Rgba5551ToRgba8888(u16 c)
|
||||
{
|
||||
u32 r = ((c >> 0) & 0x1F) << 3;
|
||||
u32 g = ((c >> 5) & 0x1F) << 3;
|
||||
u32 r = ((c >> 0) & 0x1F) << 3;
|
||||
u32 g = ((c >> 5) & 0x1F) << 3;
|
||||
u32 b = ((c >> 10) & 0x1F) << 3;
|
||||
u32 a = ((c >> 15) & 0x01) << 7;
|
||||
|
||||
@@ -101,6 +100,28 @@ namespace
|
||||
std::atomic<uint32_t> s_debugPixelCount{0};
|
||||
std::atomic<uint32_t> s_debugContext1PrimitiveCount{0};
|
||||
std::atomic<uint32_t> s_debugFbp150PixelCount{0};
|
||||
|
||||
int wrapTextureCoordinate(int coordinate,
|
||||
int textureSize,
|
||||
uint8_t mode,
|
||||
uint16_t regionMin,
|
||||
uint16_t regionMax)
|
||||
{
|
||||
switch (mode & 0x3u)
|
||||
{
|
||||
case 0: // REPEAT
|
||||
return static_cast<int>(static_cast<uint32_t>(coordinate) & static_cast<uint32_t>(textureSize - 1));
|
||||
case 1: // CLAMP
|
||||
return clampInt(coordinate, 0, textureSize - 1);
|
||||
case 2: // REGION_CLAMP
|
||||
return std::min(std::max(coordinate, static_cast<int>(regionMin)), static_cast<int>(regionMax));
|
||||
case 3: // REGION_REPEAT
|
||||
return static_cast<int>((static_cast<uint32_t>(coordinate) & static_cast<uint32_t>(regionMin)) | static_cast<uint32_t>(regionMax));
|
||||
default:
|
||||
return coordinate;
|
||||
}
|
||||
}
|
||||
|
||||
bool passesAlphaTest(uint64_t testReg, uint8_t alpha)
|
||||
{
|
||||
if ((testReg & 0x1u) == 0u)
|
||||
@@ -132,29 +153,67 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
struct AlphaTestResult
|
||||
struct PixelWriteMask
|
||||
{
|
||||
bool writeFramebuffer;
|
||||
bool preserveDestinationAlpha;
|
||||
bool writeRgb = true;
|
||||
bool writeAlpha = true;
|
||||
bool writeDepth = true;
|
||||
|
||||
bool writesFramebuffer() const
|
||||
{
|
||||
return writeRgb || writeAlpha;
|
||||
}
|
||||
|
||||
bool writesAnything() const
|
||||
{
|
||||
return writesFramebuffer() || writeDepth;
|
||||
}
|
||||
};
|
||||
|
||||
AlphaTestResult classifyAlphaTest(uint64_t testReg, uint8_t alpha)
|
||||
PixelWriteMask classifyAlphaTest(uint64_t testReg, uint8_t alpha, uint8_t framePsm)
|
||||
{
|
||||
const bool pass = passesAlphaTest(testReg, alpha);
|
||||
if (pass)
|
||||
return {true, false};
|
||||
return {};
|
||||
|
||||
// TEST.AFAIL controls what happens when the alpha comparison fails.
|
||||
switch (static_cast<uint8_t>((testReg >> 12) & 0x3u))
|
||||
{
|
||||
case 1: // FB_ONLY
|
||||
return {true, false};
|
||||
case 3: // RGB_ONLY
|
||||
return {true, true};
|
||||
case 0: // KEEP
|
||||
return {true, true, false};
|
||||
case 2: // ZB_ONLY
|
||||
return {false, false, true};
|
||||
case 3: // RGB_ONLY
|
||||
// RGB_ONLY is only distinct for RGBA32. The GS treats it as
|
||||
// FB_ONLY for RGB24 and RGBA16 framebuffers.
|
||||
if (framePsm == GS_PSM_CT32)
|
||||
return {true, false, false};
|
||||
return {true, true, false};
|
||||
case 0: // KEEP
|
||||
default:
|
||||
return {false, false};
|
||||
return {false, false, false};
|
||||
}
|
||||
}
|
||||
|
||||
bool passesDestinationAlphaTest(uint64_t testReg, uint8_t framePsm, uint32_t rawFramebufferPixel)
|
||||
{
|
||||
const bool date = ((testReg >> 14) & 0x1u) != 0u;
|
||||
if (!date)
|
||||
return true;
|
||||
|
||||
const bool datm = ((testReg >> 15) & 0x1u) != 0u;
|
||||
switch (framePsm)
|
||||
{
|
||||
case GS_PSM_CT32:
|
||||
return (((rawFramebufferPixel >> 31) & 0x1u) != 0u) == datm;
|
||||
case GS_PSM_CT16:
|
||||
case GS_PSM_CT16S:
|
||||
return (((rawFramebufferPixel >> 15) & 0x1u) != 0u) == datm;
|
||||
case GS_PSM_CT24:
|
||||
// RGB24 has no destination alpha, so DATE always passes.
|
||||
return true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,36 +277,52 @@ namespace
|
||||
|
||||
uint32_t swizzleClutIndexCSM1(uint32_t index)
|
||||
{
|
||||
return (index & 0xE7u) | ((index & 0x08u) << 1u) | ((index & 0x10u) >> 1u);
|
||||
// CSM1 swaps address bits 3 and 4. Preserve the remaining bits:
|
||||
// 16-bit CLUTs expose a ninth address bit through CSA[4].
|
||||
return (index & ~0x18u) | ((index & 0x08u) << 1u) | ((index & 0x10u) >> 1u);
|
||||
}
|
||||
|
||||
// TODO: clut cache
|
||||
uint32_t resolveClutIndex(uint8_t index, uint8_t csm, uint8_t csa, uint8_t sourcePsm)
|
||||
uint32_t resolveClutIndex(uint8_t index, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm)
|
||||
{
|
||||
uint32_t clutIndex = static_cast<uint32_t>(index);
|
||||
|
||||
// CSM2 addresses the source directly through TEXCLUT. CSA is required
|
||||
// to be zero there, so it must not offset the source coordinates.
|
||||
if (csm != 0u)
|
||||
return (sourcePsm == GS_PSM_T4 ||
|
||||
sourcePsm == GS_PSM_T4HH ||
|
||||
sourcePsm == GS_PSM_T4HL)
|
||||
? (clutIndex & 0x0Fu)
|
||||
: clutIndex;
|
||||
|
||||
const bool is16BitClut = cpsm == GS_PSM_CT16 || cpsm == GS_PSM_CT16S;
|
||||
const uint32_t csaMask = is16BitClut ? 0x1Fu : 0x0Fu;
|
||||
const uint32_t clutIndexMask = is16BitClut ? 0x1FFu : 0x0FFu;
|
||||
const uint32_t clutBase = (static_cast<uint32_t>(csa) & csaMask) << 4u;
|
||||
|
||||
switch (sourcePsm)
|
||||
{
|
||||
case GS_PSM_T4:
|
||||
case GS_PSM_T4HH:
|
||||
case GS_PSM_T4HL:
|
||||
{
|
||||
clutIndex = (static_cast<uint32_t>(csa) << 4u) | (clutIndex & 0x0Fu);
|
||||
|
||||
if (csm == 0u)
|
||||
clutIndex = swizzleClutIndexCSM1(clutIndex);
|
||||
}
|
||||
break;
|
||||
clutIndex = clutBase + (clutIndex & 0x0Fu);
|
||||
break;
|
||||
case GS_PSM_T8:
|
||||
case GS_PSM_T8H:
|
||||
if (csm == 0)
|
||||
clutIndex = swizzleClutIndexCSM1(clutIndex);
|
||||
clutIndex = clutBase + clutIndex;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
return clutIndex;
|
||||
}
|
||||
|
||||
return clutIndex;
|
||||
return swizzleClutIndexCSM1(clutIndex & clutIndexMask);
|
||||
}
|
||||
|
||||
int textureDimension(uint8_t exponent)
|
||||
{
|
||||
// TEX0.TW/TH saturate at 1024 pixels on the GS.
|
||||
return 1 << std::min<uint32_t>(exponent, 10u);
|
||||
}
|
||||
|
||||
bool tex1UsesLinearFilter(uint64_t tex1)
|
||||
@@ -399,7 +474,7 @@ void GSRasterizer::drawPrimitive(GS *gs)
|
||||
const auto &ctx = gs->activeContext();
|
||||
int px = static_cast<int>(v.x) - (ctx.xyoffset.ofx >> 4);
|
||||
int py = static_cast<int>(v.y) - (ctx.xyoffset.ofy >> 4);
|
||||
writePixel(gs, px, py, static_cast<u32>(v.z), v.r, v.g, v.b, v.a);
|
||||
writePixel(gs, px, py, static_cast<u32>(v.z), v.r, v.g, v.b, v.a, v.fog);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -407,51 +482,72 @@ void GSRasterizer::drawPrimitive(GS *gs)
|
||||
}
|
||||
}
|
||||
|
||||
void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
|
||||
void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t fog)
|
||||
{
|
||||
const auto &ctx = gs->activeContext();
|
||||
|
||||
if (x < ctx.scissor.x0 || x > ctx.scissor.x1 ||
|
||||
y < ctx.scissor.y0 || y > ctx.scissor.y1)
|
||||
if (x < ctx.scissor.x0 || x > ctx.scissor.x1 || y < ctx.scissor.y0 || y > ctx.scissor.y1)
|
||||
return;
|
||||
|
||||
const AlphaTestResult alphaTest = classifyAlphaTest(ctx.test, a);
|
||||
if (gs->m_prim.fge)
|
||||
{
|
||||
const uint32_t inverseFog = 255u - fog;
|
||||
auto applyFog = [&](uint8_t input, uint8_t fogColor) -> uint8_t
|
||||
{
|
||||
return static_cast<uint8_t>(((static_cast<uint32_t>(fog) * input) >> 8) + ((inverseFog * fogColor) >> 8));
|
||||
};
|
||||
|
||||
if (!alphaTest.writeFramebuffer)
|
||||
return;
|
||||
r = applyFog(r, gs->m_fogR);
|
||||
g = applyFog(g, gs->m_fogG);
|
||||
b = applyFog(b, gs->m_fogB);
|
||||
}
|
||||
|
||||
u8* vram = gs->m_vram;
|
||||
|
||||
const u32 fbp = GSInternal::framePageBaseToBlock(ctx.frame.fbp);
|
||||
const u32 fbw = std::max<u32>(ctx.frame.fbw, 1u);
|
||||
const u32 fbp = GSInternal::framePageBaseToBlock(ctx.frame.fbp);
|
||||
const u32 fbw = std::max<u32>(ctx.frame.fbw, 1u);
|
||||
const u32 fpsm = ctx.frame.psm;
|
||||
const u32 fmsk = ctx.frame.fbmsk;
|
||||
const u32 zbp = GSInternal::framePageBaseToBlock(ctx.zbuf.zbp);
|
||||
const u32 zpsm = ctx.zbuf.psm;
|
||||
|
||||
const PixelWriteMask writeMask = classifyAlphaTest(ctx.test, a, static_cast<uint8_t>(fpsm));
|
||||
if (!writeMask.writesAnything())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t ztestMethod = static_cast<uint32_t>((ctx.test >> 17) & 3u);
|
||||
const bool alphaBlendEnabled = gs->m_prim.abe;
|
||||
const bool destinationAlpha = alphaTest.preserveDestinationAlpha;
|
||||
const bool preserveDestinationAlpha = writeMask.writeRgb && !writeMask.writeAlpha && fpsm == GS_PSM_CT32;
|
||||
const bool destinationAlphaTestNeedsRead = ((ctx.test >> 14) & 0x1u) != 0u && (fpsm == GS_PSM_CT32 || fpsm == GS_PSM_CT16 || fpsm == GS_PSM_CT16S);
|
||||
|
||||
// small optimization, avoid reading the framebuffer for simple draws
|
||||
// TODO: only one address lookup for rmw
|
||||
const bool frmw = (ctx.frame.fbmsk != 0) || alphaBlendEnabled || destinationAlpha;
|
||||
const bool frmw = destinationAlphaTestNeedsRead || (writeMask.writesFramebuffer() && ((ctx.frame.fbmsk != 0) || alphaBlendEnabled || preserveDestinationAlpha));
|
||||
|
||||
u32 rawFramebufferPixel = 0;
|
||||
u32 fbrgba = 0;
|
||||
if (frmw)
|
||||
{
|
||||
fbrgba = gs->ReadVram(fpsm, fbp, fbw, x, y);
|
||||
rawFramebufferPixel = gs->ReadVram(fpsm, fbp, fbw, x, y);
|
||||
fbrgba = rawFramebufferPixel;
|
||||
|
||||
if (bitsPerPixel(fpsm) == 16)
|
||||
{
|
||||
fbrgba = Rgba5551ToRgba8888(fbrgba);
|
||||
}
|
||||
else if (fpsm == GS_PSM_CT24)
|
||||
{
|
||||
// The GS supplies 0x80 as destination alpha for RGB24 blending.
|
||||
fbrgba |= 0x80000000u;
|
||||
}
|
||||
}
|
||||
|
||||
uint ztest_method = (ctx.test >> 17) & 3;
|
||||
|
||||
if (!passesDestinationAlphaTest(ctx.test, static_cast<uint8_t>(fpsm), rawFramebufferPixel))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool zpass = false;
|
||||
switch (ztest_method)
|
||||
uint32_t storedZ = 0u;
|
||||
switch (ztestMethod)
|
||||
{
|
||||
case 0:
|
||||
zpass = false;
|
||||
@@ -460,10 +556,12 @@ void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g,
|
||||
zpass = true;
|
||||
break;
|
||||
case 2:
|
||||
zpass = z >= gs->ReadVram(zpsm, zbp, fbw, x, y);
|
||||
storedZ = gs->ReadVram(zpsm, zbp, fbw, x, y);
|
||||
zpass = static_cast<uint32_t>(z) >= storedZ;
|
||||
break;
|
||||
case 3:
|
||||
zpass = z > gs->ReadVram(zpsm, zbp, fbw, x, y);
|
||||
storedZ = gs->ReadVram(zpsm, zbp, fbw, x, y);
|
||||
zpass = static_cast<uint32_t>(z) > storedZ;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -472,81 +570,79 @@ void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g,
|
||||
return;
|
||||
}
|
||||
|
||||
const u8 srcR = r;
|
||||
const u8 srcG = g;
|
||||
const u8 srcB = b;
|
||||
|
||||
if (gs->m_prim.abe)
|
||||
if (writeMask.writesFramebuffer())
|
||||
{
|
||||
uint8_t dr = fbrgba & 0xFF;
|
||||
uint8_t dg = (fbrgba >> 8) & 0xFF;
|
||||
uint8_t db = (fbrgba >> 16) & 0xFF;
|
||||
uint8_t da = (fbrgba >> 24) & 0xFF;
|
||||
const u8 srcR = r;
|
||||
const u8 srcG = g;
|
||||
const u8 srcB = b;
|
||||
|
||||
// PABE disables alpha blending when the source alpha MSB is clear.
|
||||
if (!(gs->m_pabe && (a & 0x80u) == 0u))
|
||||
if (gs->m_prim.abe)
|
||||
{
|
||||
uint64_t alphaReg = ctx.alpha;
|
||||
uint8_t asel = alphaReg & 3;
|
||||
uint8_t bsel = (alphaReg >> 2) & 3;
|
||||
uint8_t csel = (alphaReg >> 4) & 3;
|
||||
uint8_t dsel = (alphaReg >> 6) & 3;
|
||||
uint8_t fix = static_cast<uint8_t>((alphaReg >> 32) & 0xFF);
|
||||
uint8_t dr = fbrgba & 0xFF;
|
||||
uint8_t dg = (fbrgba >> 8) & 0xFF;
|
||||
uint8_t db = (fbrgba >> 16) & 0xFF;
|
||||
uint8_t da = (fbrgba >> 24) & 0xFF;
|
||||
|
||||
auto pickRGB = [&](uint8_t sel, int cs, int cd) -> int
|
||||
// PABE disables alpha blending when the source alpha MSB is clear.
|
||||
if (!(gs->m_pabe && (a & 0x80u) == 0u))
|
||||
{
|
||||
if (sel == 0)
|
||||
return cs;
|
||||
if (sel == 1)
|
||||
return cd;
|
||||
return 0;
|
||||
};
|
||||
int cAlpha = (csel == 0) ? a : (csel == 1) ? da
|
||||
: fix;
|
||||
uint64_t alphaReg = ctx.alpha;
|
||||
uint8_t asel = alphaReg & 3;
|
||||
uint8_t bsel = (alphaReg >> 2) & 3;
|
||||
uint8_t csel = (alphaReg >> 4) & 3;
|
||||
uint8_t dsel = (alphaReg >> 6) & 3;
|
||||
uint8_t fix = static_cast<uint8_t>((alphaReg >> 32) & 0xFF);
|
||||
|
||||
r = clampU8(((pickRGB(asel, r, dr) - pickRGB(bsel, r, dr)) * cAlpha >> 7) + pickRGB(dsel, r, dr));
|
||||
g = clampU8(((pickRGB(asel, g, dg) - pickRGB(bsel, g, dg)) * cAlpha >> 7) + pickRGB(dsel, g, dg));
|
||||
b = clampU8(((pickRGB(asel, b, db) - pickRGB(bsel, b, db)) * cAlpha >> 7) + pickRGB(dsel, b, db));
|
||||
auto pickRGB = [&](uint8_t sel, int cs, int cd) -> int
|
||||
{
|
||||
if (sel == 0)
|
||||
return cs;
|
||||
if (sel == 1)
|
||||
return cd;
|
||||
return 0;
|
||||
};
|
||||
int cAlpha = (csel == 0) ? a : (csel == 1) ? da
|
||||
: fix;
|
||||
|
||||
r = clampU8(((pickRGB(asel, r, dr) - pickRGB(bsel, r, dr)) * cAlpha >> 7) + pickRGB(dsel, r, dr));
|
||||
g = clampU8(((pickRGB(asel, g, dg) - pickRGB(bsel, g, dg)) * cAlpha >> 7) + pickRGB(dsel, g, dg));
|
||||
b = clampU8(((pickRGB(asel, b, db) - pickRGB(bsel, b, db)) * cAlpha >> 7) + pickRGB(dsel, b, db));
|
||||
}
|
||||
else
|
||||
{
|
||||
r = srcR;
|
||||
g = srcG;
|
||||
b = srcB;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (writeMask.writeAlpha && (ctx.fba & 0x1ull) != 0ull && ctx.frame.psm != GS_PSM_CT24)
|
||||
{
|
||||
r = srcR;
|
||||
g = srcG;
|
||||
b = srcB;
|
||||
a = static_cast<uint8_t>(a | 0x80u);
|
||||
}
|
||||
|
||||
u32 pixel = pack32(r, g, b, a);
|
||||
|
||||
if (ctx.frame.fbmsk != 0)
|
||||
{
|
||||
pixel = (pixel & ~ctx.frame.fbmsk) | (fbrgba & ctx.frame.fbmsk);
|
||||
}
|
||||
|
||||
if (preserveDestinationAlpha)
|
||||
{
|
||||
pixel = (pixel & 0x00FFFFFFu) | (fbrgba & 0xFF000000u);
|
||||
}
|
||||
|
||||
// format conversion
|
||||
if (bitsPerPixel(fpsm) == 16)
|
||||
{
|
||||
pixel = Rgba8888ToRgba5551(pixel);
|
||||
}
|
||||
|
||||
gs->WriteVram(fpsm, fbp, fbw, x, y, pixel);
|
||||
}
|
||||
|
||||
u32 fbmask = ctx.frame.fbmsk;
|
||||
bool zmask = ctx.zbuf.zmask;
|
||||
|
||||
if (!alphaTest.preserveDestinationAlpha &&
|
||||
(ctx.fba & 0x1ull) != 0ull &&
|
||||
ctx.frame.psm != GS_PSM_CT24)
|
||||
{
|
||||
a = static_cast<uint8_t>(a | 0x80u);
|
||||
}
|
||||
|
||||
u32 pixel = pack32(r, g, b, a);
|
||||
|
||||
if (fbmask != 0)
|
||||
{
|
||||
pixel = (pixel & ~fbmask) | (fbrgba & fbmask);
|
||||
}
|
||||
|
||||
if (alphaTest.preserveDestinationAlpha)
|
||||
{
|
||||
pixel = (pixel & 0x00FFFFFFu) | (fbrgba & 0xFF000000u);
|
||||
}
|
||||
|
||||
// format conversion
|
||||
if (bitsPerPixel(fpsm) == 16)
|
||||
{
|
||||
pixel = Rgba8888ToRgba5551(pixel);
|
||||
}
|
||||
|
||||
gs->WriteVram(fpsm, fbp, fbw, x, y, pixel);
|
||||
|
||||
if (!zmask)
|
||||
if (writeMask.writeDepth && !ctx.zbuf.zmask)
|
||||
{
|
||||
gs->WriteVram(zpsm, zbp, fbw, x, y, z);
|
||||
}
|
||||
@@ -560,12 +656,11 @@ uint32_t GSRasterizer::lookupCLUT(GS *gs,
|
||||
uint8_t csa,
|
||||
uint8_t sourcePsm)
|
||||
{
|
||||
const uint32_t clutIndex = resolveClutIndex(index, csm, csa, sourcePsm);
|
||||
const uint32_t clutIndex = resolveClutIndex(index, cpsm, csm, csa, sourcePsm);
|
||||
const uint32_t clutWidth = (gs->m_texclut.cbw != 0u) ? static_cast<uint32_t>(gs->m_texclut.cbw) : 1u;
|
||||
const uint32_t clutX = static_cast<uint32_t>(gs->m_texclut.cou) + (clutIndex & 0x0Fu);
|
||||
const uint32_t clutY = static_cast<uint32_t>(gs->m_texclut.cov) + (clutIndex >> 4);
|
||||
|
||||
|
||||
switch (cpsm)
|
||||
{
|
||||
case GS_PSM_CT32:
|
||||
@@ -588,8 +683,15 @@ uint32_t GSRasterizer::sampleTexture(GS *gs, float s, float t, float q, uint16_t
|
||||
const auto &ctx = gs->activeContext();
|
||||
const auto &tex = ctx.tex0;
|
||||
|
||||
int texW = 1 << tex.tw;
|
||||
int texH = 1 << tex.th;
|
||||
const int texW = textureDimension(tex.tw);
|
||||
const int texH = textureDimension(tex.th);
|
||||
const uint64_t clamp = ctx.clamp;
|
||||
const uint8_t wrapU = static_cast<uint8_t>(clamp & 0x3u);
|
||||
const uint8_t wrapV = static_cast<uint8_t>((clamp >> 2) & 0x3u);
|
||||
const uint16_t minU = static_cast<uint16_t>((clamp >> 4) & 0x3FFu);
|
||||
const uint16_t maxU = static_cast<uint16_t>((clamp >> 14) & 0x3FFu);
|
||||
const uint16_t minV = static_cast<uint16_t>((clamp >> 24) & 0x3FFu);
|
||||
const uint16_t maxV = static_cast<uint16_t>((clamp >> 34) & 0x3FFu);
|
||||
|
||||
float texUf, texVf;
|
||||
if (gs->m_prim.fst)
|
||||
@@ -606,8 +708,8 @@ uint32_t GSRasterizer::sampleTexture(GS *gs, float s, float t, float q, uint16_t
|
||||
|
||||
auto samplePoint = [&](int sampleU, int sampleV) -> uint32_t
|
||||
{
|
||||
sampleU = clampInt(sampleU, 0, texW - 1);
|
||||
sampleV = clampInt(sampleV, 0, texH - 1);
|
||||
sampleU = wrapTextureCoordinate(sampleU, texW, wrapU, minU, maxU);
|
||||
sampleV = wrapTextureCoordinate(sampleV, texH, wrapV, minV, maxV);
|
||||
|
||||
u32 out = gs->ReadVram(tex.psm, tex.tbp0, tex.tbw, sampleU, sampleV);
|
||||
|
||||
@@ -747,12 +849,8 @@ void GSRasterizer::drawSprite(GS *gs)
|
||||
if (gs->m_prim.tme)
|
||||
{
|
||||
const auto &tex = ctx.tex0;
|
||||
int texW = 1 << tex.tw;
|
||||
int texH = 1 << tex.th;
|
||||
if (texW == 0)
|
||||
texW = 1;
|
||||
if (texH == 0)
|
||||
texH = 1;
|
||||
const int texW = textureDimension(tex.tw);
|
||||
const int texH = textureDimension(tex.th);
|
||||
|
||||
float u0f, v0f, u1f, v1f;
|
||||
if (gs->m_prim.fst)
|
||||
@@ -799,10 +897,7 @@ void GSRasterizer::drawSprite(GS *gs)
|
||||
}
|
||||
else
|
||||
{
|
||||
texel = sampleTexture(gs,
|
||||
texUf / static_cast<float>(texW),
|
||||
texVf / static_cast<float>(texH),
|
||||
1.0f, 0u, 0u);
|
||||
texel = sampleTexture(gs, texUf / static_cast<float>(texW), texVf / static_cast<float>(texH), 1.0f, 0u, 0u);
|
||||
}
|
||||
|
||||
uint8_t tr = static_cast<uint8_t>(texel & 0xFF);
|
||||
@@ -811,7 +906,7 @@ void GSRasterizer::drawSprite(GS *gs)
|
||||
uint8_t ta = static_cast<uint8_t>((texel >> 24) & 0xFF);
|
||||
|
||||
const TextureCombineResult color = combineTexture(tex, r, g, b, a, tr, tg, tb, ta);
|
||||
writePixel(gs, x, y, z1, color.r, color.g, color.b, color.a);
|
||||
writePixel(gs, x, y, z1, color.r, color.g, color.b, color.a, v1.fog);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -819,7 +914,7 @@ void GSRasterizer::drawSprite(GS *gs)
|
||||
{
|
||||
for (int y = drawY0; y <= drawY1; ++y)
|
||||
for (int x = drawX0; x <= drawX1; ++x)
|
||||
writePixel(gs, x, y, z1, r, g, b, a);
|
||||
writePixel(gs, x, y, z1, r, g, b, a, v1.fog);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,15 +999,12 @@ void GSRasterizer::drawTriangle(GS *gs)
|
||||
}
|
||||
else
|
||||
{
|
||||
const float invQ0 = 1.0f / fabsQ(v0.q);
|
||||
const float invQ1 = 1.0f / fabsQ(v1.q);
|
||||
const float invQ2 = 1.0f / fabsQ(v2.q);
|
||||
const float sOverQ = (v0.s * invQ0) * w0 + (v1.s * invQ1) * w1 + (v2.s * invQ2) * w2;
|
||||
const float tOverQ = (v0.t * invQ0) * w0 + (v1.t * invQ1) * w1 + (v2.t * invQ2) * w2;
|
||||
const float invQ = invQ0 * w0 + invQ1 * w1 + invQ2 * w2;
|
||||
iq = (std::fabs(invQ) > 1.0e-8f) ? (1.0f / invQ) : 1.0f;
|
||||
is = sOverQ * iq;
|
||||
it = tOverQ * iq;
|
||||
// The GS DDA interpolates the homogeneous S, T and Q
|
||||
// values. Texel coordinates are calculated from S/Q and
|
||||
// T/Q only after interpolation.
|
||||
is = v0.s * w0 + v1.s * w1 + v2.s * w2;
|
||||
it = v0.t * w0 + v1.t * w1 + v2.t * w2;
|
||||
iq = v0.q * w0 + v1.q * w1 + v2.q * w2;
|
||||
iu = 0;
|
||||
iv = 0;
|
||||
}
|
||||
@@ -937,7 +1029,8 @@ void GSRasterizer::drawTriangle(GS *gs)
|
||||
a = color.a;
|
||||
}
|
||||
|
||||
writePixel(gs, x, y, static_cast<u32>(z + 0.5), r, g, b, a);
|
||||
const uint8_t fog = clampU8(static_cast<int>(v0.fog * w0 + v1.fog * w1 + v2.fog * w2));
|
||||
writePixel(gs, x, y, static_cast<u32>(z + 0.5), r, g, b, a, fog);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -987,8 +1080,8 @@ void GSRasterizer::drawLine(GS *gs)
|
||||
}
|
||||
|
||||
double z = (v0.z + (v1.z - v0.z) * t);
|
||||
|
||||
writePixel(gs, x0, y0, static_cast<u32>(z), r, g, b, a);
|
||||
const uint8_t fog = clampU8(static_cast<int>(v0.fog + (v1.fog - v0.fog) * t));
|
||||
writePixel(gs, x0, y0, static_cast<u32>(z), r, g, b, a, fog);
|
||||
|
||||
if (x0 == x1 && y0 == y1)
|
||||
break;
|
||||
|
||||
@@ -351,6 +351,7 @@ bool PS2Memory::initialize(size_t ramSize)
|
||||
m_vu1Data = new uint8_t[PS2_VU1_DATA_SIZE];
|
||||
std::memset(m_vu1Code, 0, PS2_VU1_CODE_SIZE);
|
||||
std::memset(m_vu1Data, 0, PS2_VU1_DATA_SIZE);
|
||||
markVU0CodeModified();
|
||||
markVU1CodeModified();
|
||||
|
||||
// Initialize VIF registers
|
||||
@@ -765,7 +766,9 @@ void PS2Memory::write8(uint32_t address, uint8_t value)
|
||||
{
|
||||
(void)vuLimit;
|
||||
vuMem[vuOffset] = value;
|
||||
if (vuMem == m_vu1Code)
|
||||
if (vuMem == m_vu0Code)
|
||||
markVU0CodeModified();
|
||||
else if (vuMem == m_vu1Code)
|
||||
markVU1CodeModified();
|
||||
return;
|
||||
}
|
||||
@@ -806,7 +809,9 @@ void PS2Memory::write16(uint32_t address, uint16_t value)
|
||||
if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint16_t), vuOffset, vuLimit))
|
||||
{
|
||||
storeScalar<uint16_t>(vuMem, vuOffset, vuLimit, value, "write16 vu", address);
|
||||
if (vuMem == m_vu1Code)
|
||||
if (vuMem == m_vu0Code)
|
||||
markVU0CodeModified();
|
||||
else if (vuMem == m_vu1Code)
|
||||
markVU1CodeModified();
|
||||
return;
|
||||
}
|
||||
@@ -868,7 +873,9 @@ void PS2Memory::write32(uint32_t address, uint32_t value)
|
||||
if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint32_t), vuOffset, vuLimit))
|
||||
{
|
||||
storeScalar<uint32_t>(vuMem, vuOffset, vuLimit, value, "write32 vu", address);
|
||||
if (vuMem == m_vu1Code)
|
||||
if (vuMem == m_vu0Code)
|
||||
markVU0CodeModified();
|
||||
else if (vuMem == m_vu1Code)
|
||||
markVU1CodeModified();
|
||||
return;
|
||||
}
|
||||
@@ -921,7 +928,9 @@ void PS2Memory::write64(uint32_t address, uint64_t value)
|
||||
if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint64_t), vuOffset, vuLimit))
|
||||
{
|
||||
storeScalar<uint64_t>(vuMem, vuOffset, vuLimit, value, "write64 vu", address);
|
||||
if (vuMem == m_vu1Code)
|
||||
if (vuMem == m_vu0Code)
|
||||
markVU0CodeModified();
|
||||
else if (vuMem == m_vu1Code)
|
||||
markVU1CodeModified();
|
||||
return;
|
||||
}
|
||||
@@ -962,7 +971,9 @@ void PS2Memory::write128(uint32_t address, __m128i value)
|
||||
{
|
||||
inRange(vuOffset, sizeof(__m128i), vuLimit, "write128 vu", address);
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i *>(vuMem + vuOffset), value);
|
||||
if (vuMem == m_vu1Code)
|
||||
if (vuMem == m_vu0Code)
|
||||
markVU0CodeModified();
|
||||
else if (vuMem == m_vu1Code)
|
||||
markVU1CodeModified();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -207,6 +207,7 @@ namespace
|
||||
ctx->vu0_mac_flags = 0;
|
||||
ctx->vu0_status = 0;
|
||||
ctx->vu0_q = 1.0f;
|
||||
ctx->vu0_r = _mm_castsi128_ps(_mm_set1_epi32(0x3F800000));
|
||||
ctx->vu0_vpu_stat = 0;
|
||||
ctx->vu0_vpu_stat2 = 0;
|
||||
}
|
||||
@@ -228,11 +229,16 @@ namespace
|
||||
state.q = ctx->vu0_q;
|
||||
state.p = ctx->vu0_p;
|
||||
state.i = ctx->vu0_i;
|
||||
alignas(16) uint32_t rWords[4]{};
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i *>(rWords), _mm_castps_si128(ctx->vu0_r));
|
||||
state.r = 0x3F800000u | (rWords[0] & 0x007FFFFFu);
|
||||
state.pc = ctx->vu0_pc;
|
||||
state.mac = ctx->vu0_mac_flags;
|
||||
state.clip = ctx->vu0_clip_flags;
|
||||
state.status = ctx->vu0_status;
|
||||
state.itop = ctx->vu0_itop;
|
||||
state.dBitEnabled = (ctx->vu0_fbrst & (1u << 2)) != 0u;
|
||||
state.tBitEnabled = (ctx->vu0_fbrst & (1u << 3)) != 0u;
|
||||
|
||||
state.vf[0][0] = 0.0f;
|
||||
state.vf[0][1] = 0.0f;
|
||||
@@ -256,6 +262,7 @@ namespace
|
||||
ctx->vu0_q = state.q;
|
||||
ctx->vu0_p = state.p;
|
||||
ctx->vu0_i = state.i;
|
||||
ctx->vu0_r = _mm_castsi128_ps(_mm_set1_epi32(static_cast<int32_t>(state.r)));
|
||||
ctx->vu0_mac_flags = state.mac;
|
||||
ctx->vu0_clip_flags = state.clip;
|
||||
ctx->vu0_clip_flags2 = state.clip;
|
||||
@@ -263,7 +270,7 @@ namespace
|
||||
ctx->vu0_itop = state.itop;
|
||||
ctx->vu0_pc = state.pc;
|
||||
ctx->vu0_tpc = state.pc;
|
||||
ctx->vu0_vpu_stat = 0;
|
||||
ctx->vu0_vpu_stat = (ctx->vu0_vpu_stat & 0xFF00u) | (state.stoppedByD ? (1u << 1) : 0u) | (state.stoppedByT ? (1u << 2) : 0u);
|
||||
ctx->vu0_vpu_stat2 = 0;
|
||||
|
||||
ctx->vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
@@ -488,6 +495,9 @@ PS2Runtime::PS2Runtime()
|
||||
|
||||
// R0 is always zero in MIPS
|
||||
m_cpuContext.r[0] = _mm_set1_epi32(0);
|
||||
m_cpuContext.vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
m_cpuContext.vu0_q = 1.0f;
|
||||
m_cpuContext.vu0_r = _mm_castsi128_ps(_mm_set1_epi32(0x3F800000));
|
||||
|
||||
// Stack pointer (SP) and global pointer (GP) will be set by the loaded ELF
|
||||
|
||||
@@ -611,13 +621,31 @@ bool PS2Runtime::syncCoreSubsystems()
|
||||
{ m_gs.processGIFPacket(data, size); });
|
||||
m_memory.setGifArbiter(&m_gifArbiter);
|
||||
m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t top, uint32_t itop)
|
||||
{ m_vu1.execute(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE,
|
||||
m_memory.getVU1Data(), PS2_VU1_DATA_SIZE,
|
||||
m_gs, &m_memory, startPC, top, itop, 65536); });
|
||||
{
|
||||
m_vu1.state().dBitEnabled =
|
||||
(m_cpuContext.vu0_fbrst & (1u << 10)) != 0u;
|
||||
m_vu1.state().tBitEnabled =
|
||||
(m_cpuContext.vu0_fbrst & (1u << 11)) != 0u;
|
||||
m_vu1.execute(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE,
|
||||
m_memory.getVU1Data(), PS2_VU1_DATA_SIZE,
|
||||
m_gs, &m_memory, startPC, top, itop, 65536);
|
||||
m_cpuContext.vu0_vpu_stat =
|
||||
(m_cpuContext.vu0_vpu_stat & ~0x0600u) |
|
||||
(m_vu1.state().stoppedByD ? 0x0200u : 0u) |
|
||||
(m_vu1.state().stoppedByT ? 0x0400u : 0u); });
|
||||
m_memory.setVu1MscntCallback([this](uint32_t top, uint32_t itop)
|
||||
{ m_vu1.resume(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE,
|
||||
m_memory.getVU1Data(), PS2_VU1_DATA_SIZE,
|
||||
m_gs, &m_memory, top, itop, 65536); });
|
||||
{
|
||||
m_vu1.state().dBitEnabled =
|
||||
(m_cpuContext.vu0_fbrst & (1u << 10)) != 0u;
|
||||
m_vu1.state().tBitEnabled =
|
||||
(m_cpuContext.vu0_fbrst & (1u << 11)) != 0u;
|
||||
m_vu1.resume(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE,
|
||||
m_memory.getVU1Data(), PS2_VU1_DATA_SIZE,
|
||||
m_gs, &m_memory, top, itop, 65536);
|
||||
m_cpuContext.vu0_vpu_stat =
|
||||
(m_cpuContext.vu0_vpu_stat & ~0x0600u) |
|
||||
(m_vu1.state().stoppedByD ? 0x0200u : 0u) |
|
||||
(m_vu1.state().stoppedByT ? 0x0400u : 0u); });
|
||||
resetIop();
|
||||
m_vu0.reset();
|
||||
m_vu1.reset();
|
||||
@@ -1121,6 +1149,10 @@ void PS2Runtime::reportMissingFunction(uint8_t *rdram,
|
||||
const uint32_t gp = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[28], 0));
|
||||
const uint32_t a0 = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[4], 0));
|
||||
const uint32_t a1 = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[5], 0));
|
||||
const uint32_t a2 = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[6], 0));
|
||||
const uint32_t a3 = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[7], 0));
|
||||
const uint32_t s0 = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[16], 0));
|
||||
const uint32_t s1 = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[17], 0));
|
||||
const uint32_t v0 = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[2], 0));
|
||||
const uint32_t v1 = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[3], 0));
|
||||
|
||||
@@ -1158,6 +1190,27 @@ void PS2Runtime::reportMissingFunction(uint8_t *rdram,
|
||||
readGuestU32Offset(a0, 0x08u, a0Word8) &&
|
||||
readGuestU32Offset(a0, 0x0cu, a0WordC);
|
||||
|
||||
uint32_t s0Word0 = 0u;
|
||||
uint32_t s0Word4 = 0u;
|
||||
uint32_t s0Word8 = 0u;
|
||||
uint32_t s0WordC = 0u;
|
||||
const bool s0Readable =
|
||||
readGuestU32Offset(s0, 0x00u, s0Word0) &&
|
||||
readGuestU32Offset(s0, 0x04u, s0Word4) &&
|
||||
readGuestU32Offset(s0, 0x08u, s0Word8) &&
|
||||
readGuestU32Offset(s0, 0x0cu, s0WordC);
|
||||
|
||||
uint32_t recordWord0 = 0u;
|
||||
uint32_t recordWord4 = 0u;
|
||||
uint32_t recordWord8 = 0u;
|
||||
uint32_t recordWordC = 0u;
|
||||
const bool recordReadable =
|
||||
s0Readable && s0Word4 != 0u &&
|
||||
readGuestU32Offset(s0Word4, 0x00u, recordWord0) &&
|
||||
readGuestU32Offset(s0Word4, 0x04u, recordWord4) &&
|
||||
readGuestU32Offset(s0Word4, 0x08u, recordWord8) &&
|
||||
readGuestU32Offset(s0Word4, 0x0cu, recordWordC);
|
||||
|
||||
uint32_t vtableSlot0 = 0u;
|
||||
uint32_t vtableSlot4 = 0u;
|
||||
uint32_t vtableSlot8 = 0u;
|
||||
@@ -1182,6 +1235,10 @@ void PS2Runtime::reportMissingFunction(uint8_t *rdram,
|
||||
<< " gp=0x" << gp
|
||||
<< " a0=0x" << a0
|
||||
<< " a1=0x" << a1
|
||||
<< " a2=0x" << a2
|
||||
<< " a3=0x" << a3
|
||||
<< " s0=0x" << s0
|
||||
<< " s1=0x" << s1
|
||||
<< " v0=0x" << v0
|
||||
<< " v1=0x" << v1
|
||||
<< " a0Readable=" << (a0Readable ? "yes" : "no")
|
||||
@@ -1189,6 +1246,16 @@ void PS2Runtime::reportMissingFunction(uint8_t *rdram,
|
||||
<< " a0[4]=0x" << a0Word4
|
||||
<< " a0[8]=0x" << a0Word8
|
||||
<< " a0[c]=0x" << a0WordC
|
||||
<< " s0Readable=" << (s0Readable ? "yes" : "no")
|
||||
<< " s0[0]=0x" << s0Word0
|
||||
<< " s0[4]=0x" << s0Word4
|
||||
<< " s0[8]=0x" << s0Word8
|
||||
<< " s0[c]=0x" << s0WordC
|
||||
<< " recordReadable=" << (recordReadable ? "yes" : "no")
|
||||
<< " record[0]=0x" << recordWord0
|
||||
<< " record[4]=0x" << recordWord4
|
||||
<< " record[8]=0x" << recordWord8
|
||||
<< " record[c]=0x" << recordWordC
|
||||
<< " vtableReadable=" << (vtableReadable ? "yes" : "no")
|
||||
<< " vtbl[0]=0x" << vtableSlot0
|
||||
<< " vtbl[4]=0x" << vtableSlot4
|
||||
|
||||
@@ -144,7 +144,10 @@ void PS2Memory::processVIF0Data(const uint8_t *data, uint32_t sizeBytes)
|
||||
if (destAddr + copyBytes > PS2_VU0_CODE_SIZE)
|
||||
copyBytes = PS2_VU0_CODE_SIZE - destAddr;
|
||||
if (pos + copyBytes <= sizeBytes)
|
||||
{
|
||||
std::memcpy(m_vu0Code + destAddr, data + pos, copyBytes);
|
||||
markVU0CodeModified();
|
||||
}
|
||||
}
|
||||
|
||||
pos += mpgBytes;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,132 +26,4 @@ static inline int16_t IMM15(uint32_t i)
|
||||
return (int16_t)(int32_t)((int32_t)(raw << 17) >> 17);
|
||||
}
|
||||
|
||||
|
||||
static inline uint8_t vuUpperVfWriteReg(uint32_t upper)
|
||||
{
|
||||
const uint8_t op = upper & 0x3Fu;
|
||||
const uint8_t dest = DEST(upper);
|
||||
const uint8_t ft = FT(upper);
|
||||
const uint8_t fd = FD(upper);
|
||||
|
||||
if (dest == 0u)
|
||||
return 0u;
|
||||
|
||||
if (op <= 0x2Fu)
|
||||
return fd;
|
||||
|
||||
if (op >= 0x3Cu)
|
||||
{
|
||||
const uint8_t specialOp = static_cast<uint8_t>((upper & 0x3u) | ((upper >> 4) & 0x7Cu));
|
||||
switch (specialOp)
|
||||
{
|
||||
// Upper special ops that write a VF register use FT as destination.
|
||||
case 0x10: // ITOF0
|
||||
case 0x11: // ITOF4
|
||||
case 0x12: // ITOF12
|
||||
case 0x13: // ITOF15
|
||||
case 0x14: // FTOI0
|
||||
case 0x15: // FTOI4
|
||||
case 0x16: // FTOI12
|
||||
case 0x17: // FTOI15
|
||||
case 0x1D: // ABS
|
||||
return ft;
|
||||
default:
|
||||
return 0u; // ACC/NOP/CLIP/etc.
|
||||
}
|
||||
}
|
||||
|
||||
return 0u;
|
||||
}
|
||||
|
||||
static inline void vuSetRegBit(uint32_t &mask, uint8_t reg)
|
||||
{
|
||||
if (reg != 0u && reg < 32u)
|
||||
mask |= (1u << reg);
|
||||
}
|
||||
|
||||
static inline void vuLowerVfReadWriteMasks(uint32_t lower, uint32_t &readMask, uint32_t &writeMask)
|
||||
{
|
||||
readMask = 0u;
|
||||
writeMask = 0u;
|
||||
|
||||
if (lower == 0u || lower == 0x8000033Cu)
|
||||
return;
|
||||
|
||||
const uint8_t opHi = static_cast<uint8_t>((lower >> 25) & 0x7Fu);
|
||||
const uint8_t it = LIT(lower);
|
||||
const uint8_t is = LIS(lower);
|
||||
|
||||
if ((lower & 0x80000000u) != 0u)
|
||||
{
|
||||
const uint8_t funct = lower & 0x3Fu;
|
||||
if (funct >= 0x3Cu && funct <= 0x3Fu)
|
||||
{
|
||||
const uint8_t specialOp = static_cast<uint8_t>((lower & 0x3u) | ((lower >> 4) & 0x7Cu));
|
||||
switch (specialOp)
|
||||
{
|
||||
case 0x30: // MOVE
|
||||
case 0x31: // MR32
|
||||
vuSetRegBit(readMask, is);
|
||||
vuSetRegBit(writeMask, it);
|
||||
return;
|
||||
case 0x34: // LQI
|
||||
case 0x36: // LQD
|
||||
vuSetRegBit(writeMask, it);
|
||||
return;
|
||||
case 0x35: // SQI
|
||||
case 0x37: // SQD
|
||||
vuSetRegBit(readMask, is);
|
||||
return;
|
||||
case 0x38: // DIV
|
||||
case 0x3A: // RSQRT
|
||||
vuSetRegBit(readMask, is);
|
||||
vuSetRegBit(readMask, it);
|
||||
return;
|
||||
case 0x39: // SQRT
|
||||
vuSetRegBit(readMask, it);
|
||||
return;
|
||||
case 0x3C: // MTIR
|
||||
case 0x3E: // ILWR source base is integer, but field source is VF for MTIR only.
|
||||
if (specialOp == 0x3C)
|
||||
vuSetRegBit(readMask, is);
|
||||
return;
|
||||
case 0x3D: // MFIR
|
||||
case 0x64: // MFP
|
||||
vuSetRegBit(writeMask, it);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (opHi)
|
||||
{
|
||||
case 0x00: // LQ
|
||||
vuSetRegBit(writeMask, it);
|
||||
return;
|
||||
case 0x01: // SQ
|
||||
vuSetRegBit(readMask, is);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool vuLowerShouldRunBeforeUpper(uint32_t upper, uint32_t lower)
|
||||
{
|
||||
const uint8_t upperWrite = vuUpperVfWriteReg(upper);
|
||||
if (upperWrite == 0u)
|
||||
return false;
|
||||
|
||||
uint32_t lowerReads = 0u;
|
||||
uint32_t lowerWrites = 0u;
|
||||
vuLowerVfReadWriteMasks(lower, lowerReads, lowerWrites);
|
||||
|
||||
const uint32_t upperBit = (1u << upperWrite);
|
||||
return ((lowerReads | lowerWrites) & upperBit) != 0u;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -7,7 +7,64 @@
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
float vuEatan(float value)
|
||||
{
|
||||
constexpr float coefficients[] = {
|
||||
0.999999344348907f,
|
||||
-0.333298563957214f,
|
||||
0.199465364217758f,
|
||||
-0.13085337519646f,
|
||||
0.096420042216778f,
|
||||
-0.055909886956215f,
|
||||
0.021861229091883f,
|
||||
-0.004054057877511f};
|
||||
constexpr float quarterPi = 0.785398185253143f;
|
||||
|
||||
const float squared = value * value;
|
||||
float polynomial = coefficients[7];
|
||||
for (int index = 6; index >= 0; --index)
|
||||
polynomial = coefficients[index] + squared * polynomial;
|
||||
return quarterPi + value * polynomial;
|
||||
}
|
||||
|
||||
float vuEsin(float value)
|
||||
{
|
||||
constexpr float coefficients[] = {
|
||||
1.0f,
|
||||
-0.166666567325592f,
|
||||
0.008333025500178f,
|
||||
-0.000198074136279f,
|
||||
0.000002601886990f};
|
||||
|
||||
const float squared = value * value;
|
||||
float polynomial = coefficients[4];
|
||||
for (int index = 3; index >= 0; --index)
|
||||
polynomial = coefficients[index] + squared * polynomial;
|
||||
return value * polynomial;
|
||||
}
|
||||
|
||||
float vuEexp(float value)
|
||||
{
|
||||
constexpr float coefficients[] = {
|
||||
0.249998688697815f,
|
||||
0.031257584691048f,
|
||||
0.002591371303424f,
|
||||
0.000171562001924f,
|
||||
0.000005430199963f,
|
||||
0.000000690600018f};
|
||||
|
||||
float polynomial = coefficients[5];
|
||||
for (int index = 4; index >= 0; --index)
|
||||
polynomial = coefficients[index] + value * polynomial;
|
||||
polynomial = 1.0f + value * polynomial;
|
||||
polynomial *= polynomial;
|
||||
polynomial *= polynomial;
|
||||
return polynomial != 0.0f ? 1.0f / polynomial : std::numeric_limits<float>::max();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Lower instructions
|
||||
@@ -19,14 +76,15 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
return;
|
||||
|
||||
uint8_t opHi = (instr >> 25) & 0x7F;
|
||||
const uint32_t pcMask = microAddressMask();
|
||||
|
||||
// The lower instruction encoding uses bits 31:25 for the primary opcode
|
||||
switch (opHi)
|
||||
{
|
||||
case 0x00: // LQ (Load Quadword from VU data memory)
|
||||
{
|
||||
uint8_t it = FT(instr); // VF destination
|
||||
uint8_t is = VIS(instr); // VI base
|
||||
uint8_t it = FT(instr); // VF destination
|
||||
uint8_t is = VIS(instr); // VI base
|
||||
uint8_t dest = (instr >> 21) & 0xF;
|
||||
int16_t imm = IMM11(instr);
|
||||
uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u;
|
||||
@@ -41,32 +99,24 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
}
|
||||
case 0x01: // SQ (Store Quadword to VU data memory)
|
||||
{
|
||||
uint8_t is = FS(instr); // VF source
|
||||
uint8_t it = VIT(instr); // VI base
|
||||
uint8_t is = FS(instr); // VF source
|
||||
uint8_t it = VIT(instr); // VI base
|
||||
uint8_t dest = (instr >> 21) & 0xF;
|
||||
int16_t imm = IMM11(instr);
|
||||
uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[it] + imm)) * 16u;
|
||||
addr &= (dataSize - 1);
|
||||
if (addr + 16 <= dataSize)
|
||||
{
|
||||
float tmp[4];
|
||||
std::memcpy(tmp, vuData + addr, 16);
|
||||
if (dest & 0x8)
|
||||
tmp[0] = m_state.vf[is][0];
|
||||
if (dest & 0x4)
|
||||
tmp[1] = m_state.vf[is][1];
|
||||
if (dest & 0x2)
|
||||
tmp[2] = m_state.vf[is][2];
|
||||
if (dest & 0x1)
|
||||
tmp[3] = m_state.vf[is][3];
|
||||
std::memcpy(vuData + addr, tmp, 16);
|
||||
uint32_t words[4]{};
|
||||
std::memcpy(words, m_state.vf[is], sizeof(words));
|
||||
queueStore(addr, words, dest);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x04: // ILW (Integer Load Word from VU data memory)
|
||||
{
|
||||
uint8_t it = VIT(instr); // VI destination
|
||||
uint8_t is = VIS(instr); // VI base
|
||||
uint8_t it = VIT(instr); // VI destination
|
||||
uint8_t is = VIS(instr); // VI base
|
||||
uint8_t dest = (instr >> 21) & 0xF;
|
||||
int16_t imm = IMM11(instr);
|
||||
uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u;
|
||||
@@ -91,23 +141,17 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
}
|
||||
case 0x05: // ISW (Integer Store Word to VU data memory)
|
||||
{
|
||||
uint8_t it = VIT(instr); // VI source
|
||||
uint8_t is = VIS(instr); // VI base
|
||||
uint8_t it = VIT(instr); // VI source
|
||||
uint8_t is = VIS(instr); // VI base
|
||||
uint8_t dest = (instr >> 21) & 0xF;
|
||||
int16_t imm = IMM11(instr);
|
||||
uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u;
|
||||
addr &= (dataSize - 1);
|
||||
if (addr + 16 <= dataSize)
|
||||
{
|
||||
uint32_t val = (uint32_t)(uint16_t)(m_state.vi[it] & 0xFFFF);
|
||||
if (dest & 0x8)
|
||||
std::memcpy(vuData + addr + 0, &val, 4);
|
||||
if (dest & 0x4)
|
||||
std::memcpy(vuData + addr + 4, &val, 4);
|
||||
if (dest & 0x2)
|
||||
std::memcpy(vuData + addr + 8, &val, 4);
|
||||
if (dest & 0x1)
|
||||
std::memcpy(vuData + addr + 12, &val, 4);
|
||||
const uint32_t val = static_cast<uint32_t>(static_cast<uint16_t>(m_state.vi[it] & 0xFFFF));
|
||||
const uint32_t words[4] = {val, val, val, val};
|
||||
queueStore(addr, words, dest);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -138,7 +182,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
}
|
||||
case 0x11: // FCSET
|
||||
{
|
||||
m_state.clip = instr & 0xFFFFFF;
|
||||
queueFcset(instr & 0xFFFFFFu);
|
||||
return;
|
||||
}
|
||||
case 0x12: // FCAND
|
||||
@@ -157,39 +201,35 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
}
|
||||
case 0x14: // FSEQ
|
||||
{
|
||||
uint16_t imm12 = instr & 0xFFF;
|
||||
if (1 != 0)
|
||||
m_state.vi[1] = ((m_state.status & 0xFFF) == imm12) ? 1 : 0;
|
||||
const uint8_t it = VIT(instr);
|
||||
const uint16_t imm12 = static_cast<uint16_t>((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu));
|
||||
if (it != 0)
|
||||
m_state.vi[it] = ((m_state.status & 0xFFFu) == imm12) ? 1 : 0;
|
||||
return;
|
||||
}
|
||||
case 0x15: // FSSET
|
||||
{
|
||||
m_state.status = (instr >> 6) & 0xFC0;
|
||||
const uint16_t imm12 = static_cast<uint16_t>((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu));
|
||||
queueFsset(imm12);
|
||||
return;
|
||||
}
|
||||
case 0x16: // FSAND
|
||||
{
|
||||
uint16_t imm12 = instr & 0xFFF;
|
||||
if (1 != 0)
|
||||
m_state.vi[1] = (int32_t)(m_state.status & imm12);
|
||||
const uint8_t it = VIT(instr);
|
||||
const uint16_t imm12 = static_cast<uint16_t>((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu));
|
||||
if (it != 0)
|
||||
m_state.vi[it] = static_cast<int32_t>((m_state.status & 0xFFFu) & imm12);
|
||||
return;
|
||||
}
|
||||
case 0x17: // FSOR
|
||||
{
|
||||
uint16_t imm12 = instr & 0xFFF;
|
||||
if (1 != 0)
|
||||
m_state.vi[1] = ((m_state.status | imm12) == 0xFFF) ? 1 : 0;
|
||||
return;
|
||||
}
|
||||
case 0x18: // FMAND
|
||||
{
|
||||
uint8_t it = VIT(instr);
|
||||
uint8_t is = VIS(instr);
|
||||
const uint8_t it = VIT(instr);
|
||||
const uint16_t imm12 = static_cast<uint16_t>((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu));
|
||||
if (it != 0)
|
||||
m_state.vi[it] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]);
|
||||
m_state.vi[it] = static_cast<int32_t>((m_state.status & 0xFFFu) | imm12);
|
||||
return;
|
||||
}
|
||||
case 0x1A: // FMEQ
|
||||
case 0x18: // FMEQ
|
||||
{
|
||||
uint8_t it = VIT(instr);
|
||||
uint8_t is = VIS(instr);
|
||||
@@ -197,7 +237,15 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
m_state.vi[it] = ((m_state.mac & 0xFFFF) == (uint32_t)(uint16_t)m_state.vi[is]) ? 1 : 0;
|
||||
return;
|
||||
}
|
||||
case 0x1C: // FMOR
|
||||
case 0x1A: // FMAND
|
||||
{
|
||||
uint8_t it = VIT(instr);
|
||||
uint8_t is = VIS(instr);
|
||||
if (it != 0)
|
||||
m_state.vi[it] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]);
|
||||
return;
|
||||
}
|
||||
case 0x1B: // FMOR
|
||||
{
|
||||
uint8_t it = VIT(instr);
|
||||
uint8_t is = VIS(instr);
|
||||
@@ -205,10 +253,17 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
m_state.vi[it] = (int32_t)(m_state.mac | (uint32_t)(uint16_t)m_state.vi[is]);
|
||||
return;
|
||||
}
|
||||
case 0x1C: // FCGET
|
||||
{
|
||||
const uint8_t it = VIT(instr);
|
||||
if (it != 0)
|
||||
m_state.vi[it] = static_cast<int32_t>(m_state.clip & 0x0FFFu);
|
||||
return;
|
||||
}
|
||||
case 0x20: // B (unconditional branch)
|
||||
{
|
||||
int16_t imm = IMM11(instr);
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF;
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask;
|
||||
m_state.branchPending = true;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
@@ -218,7 +273,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
{
|
||||
uint8_t it = VIT(instr);
|
||||
int16_t imm = IMM11(instr);
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF;
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask;
|
||||
if (it != 0)
|
||||
m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8);
|
||||
m_state.branchPending = true;
|
||||
@@ -229,7 +284,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
case 0x24: // JR
|
||||
{
|
||||
uint8_t is = VIS(instr);
|
||||
uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF;
|
||||
uint32_t target = ((uint32_t)(uint16_t)readBranchVi(is) * 8u) & pcMask;
|
||||
m_state.branchPending = true;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
@@ -239,7 +294,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
{
|
||||
uint8_t it = VIT(instr);
|
||||
uint8_t is = VIS(instr);
|
||||
uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF;
|
||||
uint32_t target = ((uint32_t)(uint16_t)readBranchVi(is) * 8u) & pcMask;
|
||||
if (it != 0)
|
||||
m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8);
|
||||
m_state.branchPending = true;
|
||||
@@ -252,12 +307,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
uint8_t it = VIT(instr);
|
||||
uint8_t is = VIS(instr);
|
||||
int16_t imm = IMM11(instr);
|
||||
if ((int16_t)m_state.vi[is] == (int16_t)m_state.vi[it])
|
||||
if ((int16_t)readBranchVi(is) == (int16_t)readBranchVi(it))
|
||||
{
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF;
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask;
|
||||
m_state.branchPending = true;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -266,12 +321,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
uint8_t it = VIT(instr);
|
||||
uint8_t is = VIS(instr);
|
||||
int16_t imm = IMM11(instr);
|
||||
if ((int16_t)m_state.vi[is] != (int16_t)m_state.vi[it])
|
||||
if ((int16_t)readBranchVi(is) != (int16_t)readBranchVi(it))
|
||||
{
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF;
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask;
|
||||
m_state.branchPending = true;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -279,12 +334,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
{
|
||||
uint8_t is = VIS(instr);
|
||||
int16_t imm = IMM11(instr);
|
||||
if ((int16_t)m_state.vi[is] < 0)
|
||||
if ((int16_t)readBranchVi(is) < 0)
|
||||
{
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF;
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask;
|
||||
m_state.branchPending = true;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -292,12 +347,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
{
|
||||
uint8_t is = VIS(instr);
|
||||
int16_t imm = IMM11(instr);
|
||||
if ((int16_t)m_state.vi[is] > 0)
|
||||
if ((int16_t)readBranchVi(is) > 0)
|
||||
{
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF;
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask;
|
||||
m_state.branchPending = true;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -305,12 +360,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
{
|
||||
uint8_t is = VIS(instr);
|
||||
int16_t imm = IMM11(instr);
|
||||
if ((int16_t)m_state.vi[is] <= 0)
|
||||
if ((int16_t)readBranchVi(is) <= 0)
|
||||
{
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF;
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask;
|
||||
m_state.branchPending = true;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -318,12 +373,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
{
|
||||
uint8_t is = VIS(instr);
|
||||
int16_t imm = IMM11(instr);
|
||||
if ((int16_t)m_state.vi[is] >= 0)
|
||||
if ((int16_t)readBranchVi(is) >= 0)
|
||||
{
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF;
|
||||
uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask;
|
||||
m_state.branchPending = true;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
m_state.branchTarget = target;
|
||||
m_state.branchDelay = 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -338,95 +393,6 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
const uint8_t viD = VID(instr);
|
||||
const uint8_t dest = (instr >> 21) & 0xF;
|
||||
|
||||
auto doXgkick = [&]()
|
||||
{
|
||||
if (!vuData || dataSize < 16u)
|
||||
return;
|
||||
|
||||
auto wrapOffset = [&](uint32_t off) -> uint32_t
|
||||
{
|
||||
return off % dataSize;
|
||||
};
|
||||
|
||||
auto read64Wrap = [&](uint32_t off) -> uint64_t
|
||||
{
|
||||
uint8_t bytes[8];
|
||||
for (uint32_t i = 0; i < 8u; ++i)
|
||||
{
|
||||
bytes[i] = vuData[wrapOffset(off + i)];
|
||||
}
|
||||
uint64_t value = 0;
|
||||
std::memcpy(&value, bytes, sizeof(value));
|
||||
return value;
|
||||
};
|
||||
|
||||
uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u;
|
||||
addr = wrapOffset(addr);
|
||||
uint32_t pktOff = addr;
|
||||
uint32_t totalBytes = 0u;
|
||||
bool done = false;
|
||||
|
||||
for (int safety = 0; safety < 256 && !done; ++safety)
|
||||
{
|
||||
uint64_t tagLo = read64Wrap(pktOff);
|
||||
uint32_t nloop = (uint32_t)(tagLo & 0x7FFFu);
|
||||
uint8_t flg = (uint8_t)((tagLo >> 58) & 0x3u);
|
||||
uint32_t nreg = (uint32_t)((tagLo >> 60) & 0xFu);
|
||||
if (nreg == 0u)
|
||||
nreg = 16u;
|
||||
bool eop = ((tagLo >> 15) & 0x1ull) != 0ull;
|
||||
|
||||
uint32_t pktSize = 16u;
|
||||
if (flg == 0u)
|
||||
{
|
||||
pktSize += nloop * nreg * 16u;
|
||||
}
|
||||
else if (flg == 1u)
|
||||
{
|
||||
uint32_t regs = nloop * nreg;
|
||||
pktSize += regs * 8u;
|
||||
if ((regs & 1u) != 0u)
|
||||
pktSize += 8u;
|
||||
}
|
||||
else if (flg == 2u)
|
||||
{
|
||||
pktSize += nloop * 16u;
|
||||
}
|
||||
|
||||
if (pktSize == 0u)
|
||||
break;
|
||||
|
||||
totalBytes += pktSize;
|
||||
pktOff = wrapOffset(pktOff + pktSize);
|
||||
if (eop)
|
||||
done = true;
|
||||
}
|
||||
|
||||
if (totalBytes == 0u)
|
||||
return;
|
||||
|
||||
if (addr + totalBytes <= dataSize)
|
||||
{
|
||||
if (memory)
|
||||
memory->submitGifPacket(GifPathId::Path1, vuData + addr, totalBytes);
|
||||
else
|
||||
gs.processGIFPacket(vuData + addr, totalBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<uint8_t> wrappedPacket(totalBytes);
|
||||
for (uint32_t i = 0; i < totalBytes; ++i)
|
||||
{
|
||||
wrappedPacket[i] = vuData[wrapOffset(addr + i)];
|
||||
}
|
||||
|
||||
if (memory)
|
||||
memory->submitGifPacket(GifPathId::Path1, wrappedPacket.data(), totalBytes);
|
||||
else
|
||||
gs.processGIFPacket(wrappedPacket.data(), totalBytes);
|
||||
}
|
||||
};
|
||||
|
||||
switch (funct)
|
||||
{
|
||||
case 0x30: // IADD
|
||||
@@ -494,17 +460,9 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
addr &= (dataSize - 1);
|
||||
if (addr + 16 <= dataSize)
|
||||
{
|
||||
float tmp[4];
|
||||
std::memcpy(tmp, vuData + addr, 16);
|
||||
if (dest & 0x8)
|
||||
tmp[0] = m_state.vf[vfS][0];
|
||||
if (dest & 0x4)
|
||||
tmp[1] = m_state.vf[vfS][1];
|
||||
if (dest & 0x2)
|
||||
tmp[2] = m_state.vf[vfS][2];
|
||||
if (dest & 0x1)
|
||||
tmp[3] = m_state.vf[vfS][3];
|
||||
std::memcpy(vuData + addr, tmp, 16);
|
||||
uint32_t words[4]{};
|
||||
std::memcpy(words, m_state.vf[vfS], sizeof(words));
|
||||
queueStore(addr, words, dest);
|
||||
}
|
||||
if (viT != 0)
|
||||
m_state.vi[viT] = (int16_t)(m_state.vi[viT] + 1);
|
||||
@@ -532,17 +490,9 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
addr &= (dataSize - 1);
|
||||
if (addr + 16 <= dataSize)
|
||||
{
|
||||
float tmp[4];
|
||||
std::memcpy(tmp, vuData + addr, 16);
|
||||
if (dest & 0x8)
|
||||
tmp[0] = m_state.vf[vfS][0];
|
||||
if (dest & 0x4)
|
||||
tmp[1] = m_state.vf[vfS][1];
|
||||
if (dest & 0x2)
|
||||
tmp[2] = m_state.vf[vfS][2];
|
||||
if (dest & 0x1)
|
||||
tmp[3] = m_state.vf[vfS][3];
|
||||
std::memcpy(vuData + addr, tmp, 16);
|
||||
uint32_t words[4]{};
|
||||
std::memcpy(words, m_state.vf[vfS], sizeof(words));
|
||||
queueStore(addr, words, dest);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -550,46 +500,64 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
{
|
||||
int fsf = (instr >> 21) & 0x3;
|
||||
int ftf = (instr >> 23) & 0x3;
|
||||
float num = m_state.vf[vfS][fsf];
|
||||
float den = m_state.vf[vfT][ftf];
|
||||
if (den != 0.0f)
|
||||
m_state.q = num / den;
|
||||
const float num = normalizeOperand(m_state.vf[vfS][fsf]);
|
||||
const float den = normalizeOperand(m_state.vf[vfT][ftf]);
|
||||
uint32_t statusDi = 0u;
|
||||
float result = 0.0f;
|
||||
if (den == 0.0f)
|
||||
{
|
||||
statusDi = num == 0.0f ? 0x10u : 0x20u;
|
||||
result = std::signbit(num) != std::signbit(den)
|
||||
? -std::numeric_limits<float>::max()
|
||||
: std::numeric_limits<float>::max();
|
||||
}
|
||||
else
|
||||
m_state.q = (num >= 0.0f) ? std::numeric_limits<float>::max() : -std::numeric_limits<float>::max();
|
||||
{
|
||||
result = num / den;
|
||||
}
|
||||
uint32_t ignoredFlags = 0u;
|
||||
result = normalizeResult(result, ignoredFlags);
|
||||
queueQ(result, 7u, statusDi);
|
||||
return;
|
||||
}
|
||||
case 0x39: // SQRT
|
||||
{
|
||||
int ftf = (instr >> 23) & 0x3;
|
||||
float val = m_state.vf[vfT][ftf];
|
||||
m_state.q = std::sqrt(std::fabs(val));
|
||||
const float val = normalizeOperand(m_state.vf[vfT][ftf]);
|
||||
queueQ(std::sqrt(std::fabs(val)), 7u,
|
||||
val < 0.0f ? 0x10u : 0u);
|
||||
return;
|
||||
}
|
||||
case 0x3A: // RSQRT
|
||||
{
|
||||
int fsf = (instr >> 21) & 0x3;
|
||||
int ftf = (instr >> 23) & 0x3;
|
||||
float num = m_state.vf[vfS][fsf];
|
||||
float den = std::sqrt(std::fabs(m_state.vf[vfT][ftf]));
|
||||
const float num = normalizeOperand(m_state.vf[vfS][fsf]);
|
||||
const float radicand = normalizeOperand(m_state.vf[vfT][ftf]);
|
||||
const float den = std::sqrt(std::fabs(radicand));
|
||||
uint32_t statusDi = radicand < 0.0f ? 0x10u : 0u;
|
||||
float result = 0.0f;
|
||||
if (den != 0.0f)
|
||||
m_state.q = num / den;
|
||||
result = num / den;
|
||||
else
|
||||
m_state.q = std::numeric_limits<float>::max();
|
||||
{
|
||||
statusDi = num == 0.0f ? 0x10u : 0x20u;
|
||||
result = std::signbit(num)
|
||||
? -std::numeric_limits<float>::max()
|
||||
: std::numeric_limits<float>::max();
|
||||
}
|
||||
uint32_t ignoredFlags = 0u;
|
||||
result = normalizeResult(result, ignoredFlags);
|
||||
queueQ(result, 13u, statusDi);
|
||||
return;
|
||||
}
|
||||
case 0x3B: // WAITQ
|
||||
return;
|
||||
case 0x3C: // MTIR (Move To Integer Register)
|
||||
{
|
||||
int comp = 0;
|
||||
if (dest & 0x8)
|
||||
comp = 0;
|
||||
else if (dest & 0x4)
|
||||
comp = 1;
|
||||
else if (dest & 0x2)
|
||||
comp = 2;
|
||||
else
|
||||
comp = 3;
|
||||
// MTIR encodes a two-bit fsf component selector in bits
|
||||
// 22:21. It is not a four-bit destination mask.
|
||||
const uint32_t comp = (instr >> 21) & 0x3u;
|
||||
uint32_t fval;
|
||||
std::memcpy(&fval, &m_state.vf[vfS][comp], 4);
|
||||
if (viT != 0)
|
||||
@@ -635,26 +603,49 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
addr &= (dataSize - 1);
|
||||
if (addr + 16 <= dataSize)
|
||||
{
|
||||
uint32_t val = (uint32_t)(uint16_t)(m_state.vi[viT] & 0xFFFF);
|
||||
if (dest & 0x8)
|
||||
std::memcpy(vuData + addr + 0, &val, 4);
|
||||
if (dest & 0x4)
|
||||
std::memcpy(vuData + addr + 4, &val, 4);
|
||||
if (dest & 0x2)
|
||||
std::memcpy(vuData + addr + 8, &val, 4);
|
||||
if (dest & 0x1)
|
||||
std::memcpy(vuData + addr + 12, &val, 4);
|
||||
const uint32_t val =
|
||||
static_cast<uint32_t>(static_cast<uint16_t>(m_state.vi[viT] & 0xFFFF));
|
||||
const uint32_t words[4] = {val, val, val, val};
|
||||
queueStore(addr, words, dest);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x40: // RNEXT
|
||||
{
|
||||
const uint32_t x = (m_state.r >> 4) & 1u;
|
||||
const uint32_t y = (m_state.r >> 22) & 1u;
|
||||
m_state.r = ((m_state.r << 1) ^ x ^ y) & 0x007FFFFFu;
|
||||
m_state.r |= 0x3F800000u;
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &m_state.r, sizeof(value));
|
||||
const float result[4] = {value, value, value, value};
|
||||
applyDest(m_state.vf[vfT], result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x41: // RGET
|
||||
{
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, &m_state.r, sizeof(value));
|
||||
const float result[4] = {value, value, value, value};
|
||||
applyDest(m_state.vf[vfT], result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x42: // RINIT
|
||||
{
|
||||
const uint32_t component = (instr >> 21) & 3u;
|
||||
uint32_t bits = 0u;
|
||||
std::memcpy(&bits, &m_state.vf[vfS][component], sizeof(bits));
|
||||
m_state.r = 0x3F800000u | (bits & 0x007FFFFFu);
|
||||
return;
|
||||
}
|
||||
case 0x43: // RXOR
|
||||
{
|
||||
const uint32_t component = (instr >> 21) & 3u;
|
||||
uint32_t bits = 0u;
|
||||
std::memcpy(&bits, &m_state.vf[vfS][component], sizeof(bits));
|
||||
m_state.r = 0x3F800000u | ((m_state.r ^ bits) & 0x007FFFFFu);
|
||||
return;
|
||||
}
|
||||
case 0x64: // MFP (Move From P register)
|
||||
{
|
||||
float result[4] = {m_state.p, m_state.p, m_state.p, m_state.p};
|
||||
@@ -674,45 +665,125 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz
|
||||
return;
|
||||
}
|
||||
case 0x6C: // XGKICK - send GIF packet from VU1 data memory
|
||||
doXgkick();
|
||||
startXgkick(static_cast<uint32_t>(static_cast<uint16_t>(m_state.vi[viS])));
|
||||
return;
|
||||
case 0x70: // ESADD
|
||||
{
|
||||
const float x = normalizeOperand(m_state.vf[vfS][0]);
|
||||
const float y = normalizeOperand(m_state.vf[vfS][1]);
|
||||
const float z = normalizeOperand(m_state.vf[vfS][2]);
|
||||
queueP(x * x + y * y + z * z, 11u);
|
||||
return;
|
||||
}
|
||||
case 0x71: // ERSADD
|
||||
{
|
||||
const float x = normalizeOperand(m_state.vf[vfS][0]);
|
||||
const float y = normalizeOperand(m_state.vf[vfS][1]);
|
||||
const float z = normalizeOperand(m_state.vf[vfS][2]);
|
||||
const float sum = x * x + y * y + z * z;
|
||||
queueP(sum != 0.0f ? 1.0f / sum : sum, 18u);
|
||||
return;
|
||||
}
|
||||
case 0x72: // ELENG
|
||||
{
|
||||
float s = m_state.vf[vfS][0] * m_state.vf[vfS][0] + m_state.vf[vfS][1] * m_state.vf[vfS][1] + m_state.vf[vfS][2] * m_state.vf[vfS][2];
|
||||
m_state.p = std::sqrt(s);
|
||||
const float x = normalizeOperand(m_state.vf[vfS][0]);
|
||||
const float y = normalizeOperand(m_state.vf[vfS][1]);
|
||||
const float z = normalizeOperand(m_state.vf[vfS][2]);
|
||||
queueP(std::sqrt(x * x + y * y + z * z), 18u);
|
||||
return;
|
||||
}
|
||||
case 0x73: // ERLENG
|
||||
{
|
||||
float s = m_state.vf[vfS][0] * m_state.vf[vfS][0] + m_state.vf[vfS][1] * m_state.vf[vfS][1] + m_state.vf[vfS][2] * m_state.vf[vfS][2];
|
||||
float len = std::sqrt(s);
|
||||
m_state.p = (len != 0.0f) ? (1.0f / len) : std::numeric_limits<float>::max();
|
||||
const float x = normalizeOperand(m_state.vf[vfS][0]);
|
||||
const float y = normalizeOperand(m_state.vf[vfS][1]);
|
||||
const float z = normalizeOperand(m_state.vf[vfS][2]);
|
||||
const float len = std::sqrt(x * x + y * y + z * z);
|
||||
queueP(len != 0.0f ? 1.0f / len : len, 24u);
|
||||
return;
|
||||
}
|
||||
case 0x74: // EATANxy
|
||||
{
|
||||
const float x = normalizeOperand(m_state.vf[vfS][0]);
|
||||
const float y = normalizeOperand(m_state.vf[vfS][1]);
|
||||
queueP(x != 0.0f ? vuEatan(y / x) : 0.0f, 54u);
|
||||
return;
|
||||
}
|
||||
case 0x75: // EATANxz
|
||||
{
|
||||
const float x = normalizeOperand(m_state.vf[vfS][0]);
|
||||
const float z = normalizeOperand(m_state.vf[vfS][2]);
|
||||
queueP(x != 0.0f ? vuEatan(z / x) : 0.0f, 54u);
|
||||
return;
|
||||
}
|
||||
case 0x76: // ESUM
|
||||
{
|
||||
float sum = 0.0f;
|
||||
for (uint32_t component = 0; component < 4u; ++component)
|
||||
sum += normalizeOperand(m_state.vf[vfS][component]);
|
||||
queueP(sum, 12u);
|
||||
return;
|
||||
}
|
||||
case 0x77: // ERSQRT
|
||||
{
|
||||
const uint32_t component = (instr >> 21) & 3u;
|
||||
const float value = normalizeOperand(m_state.vf[vfS][component]);
|
||||
float result = value;
|
||||
if (result >= 0.0f)
|
||||
{
|
||||
result = std::sqrt(result);
|
||||
if (result != 0.0f)
|
||||
result = 1.0f / result;
|
||||
}
|
||||
queueP(result, 18u);
|
||||
return;
|
||||
}
|
||||
case 0x78: // ESQRT
|
||||
{
|
||||
const uint32_t component = (instr >> 21) & 3u;
|
||||
const float value = normalizeOperand(m_state.vf[vfS][component]);
|
||||
queueP(value >= 0.0f ? std::sqrt(value) : value, 12u);
|
||||
return;
|
||||
}
|
||||
case 0x79: // ESIN
|
||||
{
|
||||
const uint32_t component = (instr >> 21) & 3u;
|
||||
const float value = normalizeOperand(m_state.vf[vfS][component]);
|
||||
queueP(vuEsin(value), 29u);
|
||||
return;
|
||||
}
|
||||
case 0x7A: // ERCPR
|
||||
{
|
||||
int fsf = (instr >> 21) & 0x3;
|
||||
float val = m_state.vf[vfS][fsf];
|
||||
m_state.p = (val != 0.0f) ? (1.0f / val) : std::numeric_limits<float>::max();
|
||||
const uint32_t component = (instr >> 21) & 3u;
|
||||
const float value = normalizeOperand(m_state.vf[vfS][component]);
|
||||
queueP(value != 0.0f ? 1.0f / value : value, 12u);
|
||||
return;
|
||||
}
|
||||
case 0x7B: // WAITP
|
||||
return;
|
||||
case 0x7D: // EATAN / EATANxy / EATANxz placeholder
|
||||
case 0x7C: // EATAN
|
||||
{
|
||||
const uint32_t component = (instr >> 21) & 3u;
|
||||
queueP(vuEatan(normalizeOperand(m_state.vf[vfS][component])), 54u);
|
||||
return;
|
||||
}
|
||||
case 0x7D: // EEXP
|
||||
{
|
||||
const uint32_t component = (instr >> 21) & 3u;
|
||||
queueP(vuEexp(normalizeOperand(m_state.vf[vfS][component])), 44u);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
reportReservedInstruction(false, instr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
default:
|
||||
reportReservedInstruction(false, instr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
default:
|
||||
reportReservedInstruction(false, instr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,27 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
int32_t vuFloatToInt(float value, float scale)
|
||||
{
|
||||
const double scaled = static_cast<double>(value) * static_cast<double>(scale);
|
||||
if (scaled >= static_cast<double>(std::numeric_limits<int32_t>::max()))
|
||||
return std::numeric_limits<int32_t>::max();
|
||||
if (scaled <= static_cast<double>(std::numeric_limits<int32_t>::min()))
|
||||
return std::numeric_limits<int32_t>::min();
|
||||
return static_cast<int32_t>(scaled);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Upper instructions (FMAC pipeline)
|
||||
// ============================================================================
|
||||
void VU1Interpreter::execUpper(uint32_t instr)
|
||||
{
|
||||
m_currentUpperInstruction = instr;
|
||||
uint8_t dest = DEST(instr);
|
||||
uint8_t ft = FT(instr);
|
||||
uint8_t fs = FS(instr);
|
||||
@@ -16,8 +31,20 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
uint8_t op = instr & 0x3F;
|
||||
|
||||
float *vd = m_state.vf[fd];
|
||||
const float *vs = m_state.vf[fs];
|
||||
const float *vt = m_state.vf[ft];
|
||||
float normalizedVs[4];
|
||||
float normalizedVt[4];
|
||||
float normalizedAcc[4];
|
||||
for (uint32_t component = 0; component < 4u; ++component)
|
||||
{
|
||||
normalizedVs[component] = normalizeOperand(m_state.vf[fs][component]);
|
||||
normalizedVt[component] = normalizeOperand(m_state.vf[ft][component]);
|
||||
normalizedAcc[component] = normalizeOperand(m_state.acc[component]);
|
||||
}
|
||||
const float *vs = normalizedVs;
|
||||
const float *vt = normalizedVt;
|
||||
const float *acc = normalizedAcc;
|
||||
const float q = normalizeOperand(m_state.q);
|
||||
const float i = normalizeOperand(m_state.i);
|
||||
float result[4];
|
||||
|
||||
// Upper opcode decoding (bits 5:0 of upper word)
|
||||
@@ -31,7 +58,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
float bc = broadcast(vt, op & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] + bc;
|
||||
applyDest(vd, result, dest);
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x04:
|
||||
@@ -42,7 +69,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
float bc = broadcast(vt, op & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] - bc;
|
||||
applyDest(vd, result, dest);
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x08:
|
||||
@@ -52,8 +79,8 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
{
|
||||
float bc = broadcast(vt, op & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] + vs[c] * bc;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = acc[c] + vs[c] * bc;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x0C:
|
||||
@@ -63,8 +90,8 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
{
|
||||
float bc = broadcast(vt, op & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] - vs[c] * bc;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = acc[c] - vs[c] * bc;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x10:
|
||||
@@ -97,83 +124,83 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
float bc = broadcast(vt, op & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] * bc;
|
||||
applyDest(vd, result, dest);
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x1C: // MULq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] * m_state.q;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = vs[c] * q;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x1D: // MAXi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = (vs[c] > m_state.i) ? vs[c] : m_state.i;
|
||||
result[c] = (vs[c] > i) ? vs[c] : i;
|
||||
applyDest(vd, result, dest);
|
||||
return;
|
||||
case 0x1E: // MULi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] * m_state.i;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = vs[c] * i;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x1F: // MINIi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = (vs[c] < m_state.i) ? vs[c] : m_state.i;
|
||||
result[c] = (vs[c] < i) ? vs[c] : i;
|
||||
applyDest(vd, result, dest);
|
||||
return;
|
||||
case 0x20: // ADDq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] + m_state.q;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = vs[c] + q;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x21: // MADDq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] + vs[c] * m_state.q;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = acc[c] + vs[c] * q;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x22: // ADDi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] + m_state.i;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = vs[c] + i;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x23: // MADDi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] + vs[c] * m_state.i;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = acc[c] + vs[c] * i;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x24: // SUBq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] - m_state.q;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = vs[c] - q;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x25: // MSUBq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] - vs[c] * m_state.q;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = acc[c] - vs[c] * q;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x26: // SUBi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] - m_state.i;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = vs[c] - i;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x27: // MSUBi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] - vs[c] * m_state.i;
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = acc[c] - vs[c] * i;
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x28: // ADD
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] + vt[c];
|
||||
applyDest(vd, result, dest);
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x29: // MADD
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] + vs[c] * vt[c];
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = acc[c] + vs[c] * vt[c];
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x2A: // MUL
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] * vt[c];
|
||||
applyDest(vd, result, dest);
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x2B: // MAX
|
||||
for (int c = 0; c < 4; c++)
|
||||
@@ -183,19 +210,19 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
case 0x2C: // SUB
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] - vt[c];
|
||||
applyDest(vd, result, dest);
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x2D: // MSUB
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] - vs[c] * vt[c];
|
||||
applyDest(vd, result, dest);
|
||||
result[c] = acc[c] - vs[c] * vt[c];
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x2E: // OPMSUB
|
||||
result[0] = m_state.acc[0] - vs[1] * vt[2];
|
||||
result[1] = m_state.acc[1] - vs[2] * vt[0];
|
||||
result[2] = m_state.acc[2] - vs[0] * vt[1];
|
||||
result[0] = acc[0] - vs[1] * vt[2];
|
||||
result[1] = acc[1] - vs[2] * vt[0];
|
||||
result[2] = acc[2] - vs[0] * vt[1];
|
||||
result[3] = 0.0f;
|
||||
applyDest(vd, result, dest);
|
||||
applyFmacDest(vd, result, dest);
|
||||
return;
|
||||
case 0x2F: // MINI
|
||||
for (int c = 0; c < 4; c++)
|
||||
@@ -225,7 +252,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
float bc = broadcast(vt, specialOp & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] + bc;
|
||||
applyDestAcc(result, dest);
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x04:
|
||||
@@ -236,7 +263,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
float bc = broadcast(vt, specialOp & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] - bc;
|
||||
applyDestAcc(result, dest);
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x08:
|
||||
@@ -246,8 +273,8 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
{
|
||||
float bc = broadcast(vt, specialOp & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] + vs[c] * bc;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = acc[c] + vs[c] * bc;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x0C:
|
||||
@@ -257,15 +284,15 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
{
|
||||
float bc = broadcast(vt, specialOp & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] - vs[c] * bc;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = acc[c] - vs[c] * bc;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x10: // ITOF0
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int32_t iv;
|
||||
std::memcpy(&iv, &vs[c], 4);
|
||||
std::memcpy(&iv, &m_state.vf[fs][c], 4);
|
||||
result[c] = static_cast<float>(iv);
|
||||
}
|
||||
applyDest(vtDest, result, dest);
|
||||
@@ -274,7 +301,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int32_t iv;
|
||||
std::memcpy(&iv, &vs[c], 4);
|
||||
std::memcpy(&iv, &m_state.vf[fs][c], 4);
|
||||
result[c] = static_cast<float>(iv) / 16.0f;
|
||||
}
|
||||
applyDest(vtDest, result, dest);
|
||||
@@ -283,7 +310,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int32_t iv;
|
||||
std::memcpy(&iv, &vs[c], 4);
|
||||
std::memcpy(&iv, &m_state.vf[fs][c], 4);
|
||||
result[c] = static_cast<float>(iv) / 4096.0f;
|
||||
}
|
||||
applyDest(vtDest, result, dest);
|
||||
@@ -292,7 +319,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int32_t iv;
|
||||
std::memcpy(&iv, &vs[c], 4);
|
||||
std::memcpy(&iv, &m_state.vf[fs][c], 4);
|
||||
result[c] = static_cast<float>(iv) / 32768.0f;
|
||||
}
|
||||
applyDest(vtDest, result, dest);
|
||||
@@ -300,7 +327,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
case 0x14: // FTOI0
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int32_t iv = static_cast<int32_t>(vs[c]);
|
||||
int32_t iv = vuFloatToInt(vs[c], 1.0f);
|
||||
std::memcpy(&result[c], &iv, 4);
|
||||
}
|
||||
applyDest(vtDest, result, dest);
|
||||
@@ -308,7 +335,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
case 0x15: // FTOI4
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int32_t iv = static_cast<int32_t>(vs[c] * 16.0f);
|
||||
int32_t iv = vuFloatToInt(vs[c], 16.0f);
|
||||
std::memcpy(&result[c], &iv, 4);
|
||||
}
|
||||
applyDest(vtDest, result, dest);
|
||||
@@ -316,7 +343,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
case 0x16: // FTOI12
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int32_t iv = static_cast<int32_t>(vs[c] * 4096.0f);
|
||||
int32_t iv = vuFloatToInt(vs[c], 4096.0f);
|
||||
std::memcpy(&result[c], &iv, 4);
|
||||
}
|
||||
applyDest(vtDest, result, dest);
|
||||
@@ -324,7 +351,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
case 0x17: // FTOI15
|
||||
for (int c = 0; c < 4; c++)
|
||||
{
|
||||
int32_t iv = static_cast<int32_t>(vs[c] * 32768.0f);
|
||||
int32_t iv = vuFloatToInt(vs[c], 32768.0f);
|
||||
std::memcpy(&result[c], &iv, 4);
|
||||
}
|
||||
applyDest(vtDest, result, dest);
|
||||
@@ -337,13 +364,13 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
float bc = broadcast(vt, specialOp & 3);
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] * bc;
|
||||
applyDestAcc(result, dest);
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
}
|
||||
case 0x1C: // MULAq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] * m_state.q;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = vs[c] * q;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x1D: // ABS
|
||||
for (int c = 0; c < 4; c++)
|
||||
@@ -352,98 +379,118 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
return;
|
||||
case 0x1E: // MULAi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] * m_state.i;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = vs[c] * i;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x1F: // CLIP
|
||||
{
|
||||
float w = std::fabs(vt[3]);
|
||||
uint32_t flags = 0;
|
||||
if (vs[0] > +w) flags |= 0x01;
|
||||
if (vs[0] < -w) flags |= 0x02;
|
||||
if (vs[1] > +w) flags |= 0x04;
|
||||
if (vs[1] < -w) flags |= 0x08;
|
||||
if (vs[2] > +w) flags |= 0x10;
|
||||
if (vs[2] < -w) flags |= 0x20;
|
||||
m_state.clip = (m_state.clip << 6) | flags;
|
||||
uint32_t wBits = 0u;
|
||||
std::memcpy(&wBits, &m_state.vf[ft][3], sizeof(wBits));
|
||||
const int32_t limit = (wBits & 0x7F800000u) != 0u ? static_cast<int32_t>(wBits & 0x7FFFFFFFu) : 0x007FFFFF;
|
||||
|
||||
const auto exceedsClipPlane = [limit](float value, uint32_t signMask)
|
||||
{
|
||||
uint32_t bits = 0u;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
bits ^= signMask;
|
||||
int32_t orderedBits = 0;
|
||||
std::memcpy(&orderedBits, &bits, sizeof(orderedBits));
|
||||
return orderedBits > limit;
|
||||
};
|
||||
|
||||
uint32_t flags = 0u;
|
||||
if (exceedsClipPlane(m_state.vf[fs][0], 0x00000000u))
|
||||
flags |= 0x01u;
|
||||
if (exceedsClipPlane(m_state.vf[fs][0], 0x80000000u))
|
||||
flags |= 0x02u;
|
||||
if (exceedsClipPlane(m_state.vf[fs][1], 0x00000000u))
|
||||
flags |= 0x04u;
|
||||
if (exceedsClipPlane(m_state.vf[fs][1], 0x80000000u))
|
||||
flags |= 0x08u;
|
||||
if (exceedsClipPlane(m_state.vf[fs][2], 0x00000000u))
|
||||
flags |= 0x10u;
|
||||
if (exceedsClipPlane(m_state.vf[fs][2], 0x80000000u))
|
||||
flags |= 0x20u;
|
||||
queueClip(flags);
|
||||
return;
|
||||
}
|
||||
case 0x20: // ADDAq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] + m_state.q;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = vs[c] + q;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x21: // MADDAq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] + vs[c] * m_state.q;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = acc[c] + vs[c] * q;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x22: // ADDAi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] + m_state.i;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = vs[c] + i;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x23: // MADDAi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] + vs[c] * m_state.i;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = acc[c] + vs[c] * i;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x24: // SUBAq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] - m_state.q;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = vs[c] - q;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x25: // MSUBAq
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] - vs[c] * m_state.q;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = acc[c] - vs[c] * q;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x26: // SUBAi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] - m_state.i;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = vs[c] - i;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x27: // MSUBAi
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] - vs[c] * m_state.i;
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = acc[c] - vs[c] * i;
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x28: // ADDA
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] + vt[c];
|
||||
applyDestAcc(result, dest);
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x29: // MADDA
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] + vs[c] * vt[c];
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = acc[c] + vs[c] * vt[c];
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x2A: // MULA
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] * vt[c];
|
||||
applyDestAcc(result, dest);
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x2C: // SUBA
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = vs[c] - vt[c];
|
||||
applyDestAcc(result, dest);
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x2D: // MSUBA
|
||||
for (int c = 0; c < 4; c++)
|
||||
result[c] = m_state.acc[c] - vs[c] * vt[c];
|
||||
applyDestAcc(result, dest);
|
||||
result[c] = acc[c] - vs[c] * vt[c];
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x2E: // OPMULA
|
||||
result[0] = vs[1] * vt[2];
|
||||
result[1] = vs[2] * vt[0];
|
||||
result[2] = vs[0] * vt[1];
|
||||
result[3] = 0.0f;
|
||||
applyDestAcc(result, dest);
|
||||
applyFmacDestAcc(result, dest);
|
||||
return;
|
||||
case 0x2F:
|
||||
case 0x30: // NOP
|
||||
return;
|
||||
default:
|
||||
reportReservedInstruction(true, instr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -453,6 +500,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
||||
case 0x32:
|
||||
case 0x33:
|
||||
default:
|
||||
reportReservedInstruction(true, instr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ add_library(ps2_test_lib STATIC
|
||||
src/ps2_runtime_interrupt_tests.cpp
|
||||
src/ps2_memory_tests.cpp
|
||||
src/ps2_vu1_tests.cpp
|
||||
src/ps2_vu_tests.cpp
|
||||
src/ps2_gs_tests.cpp
|
||||
src/ps2_iop_tests.cpp
|
||||
src/ps2_sif_rpc_tests.cpp
|
||||
@@ -116,6 +117,15 @@ target_link_libraries(ps2x_tests PRIVATE
|
||||
ps2_runtime
|
||||
)
|
||||
|
||||
# VU/GS tests intentionally instantiate large interpreter state.
|
||||
# Windows executables default to a much smaller stack than Linux, and
|
||||
# sanitizer/debug instrumentation can push these test frames over 1 MiB.
|
||||
if(MSVC)
|
||||
target_link_options(ps2x_tests PRIVATE "/STACK:8388608")
|
||||
elseif(MINGW)
|
||||
target_link_options(ps2x_tests PRIVATE "--stack,8388608")
|
||||
endif()
|
||||
|
||||
if(COMMAND ps2x_stage_ffmpeg_runtime_dlls)
|
||||
ps2x_stage_ffmpeg_runtime_dlls(ps2x_tests)
|
||||
endif()
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <fstream>
|
||||
#include <regex>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
using namespace ps2recomp;
|
||||
|
||||
@@ -851,32 +852,71 @@ void register_code_generator_tests()
|
||||
t.IsTrue(ctc1Code.find("ignored") == std::string::npos, "CTC1 FCR31 should not be ignored");
|
||||
});
|
||||
|
||||
tc.Run("VU CReg access uses CFC2/CTC2", [](TestCase &t) {
|
||||
CodeGenerator gen({}, {});
|
||||
tc.Run("VU CFC2/CTC2 access VI registers directly", [](TestCase& t)
|
||||
{
|
||||
CodeGenerator gen({}, {});
|
||||
|
||||
Instruction cfc2{};
|
||||
cfc2.opcode = OPCODE_COP2;
|
||||
cfc2.rs = COP2_CFC2;
|
||||
cfc2.rt = 2;
|
||||
cfc2.rd = VU0_CR_STATUS;
|
||||
Instruction cfc2{};
|
||||
cfc2.opcode = OPCODE_COP2;
|
||||
cfc2.rs = COP2_CFC2;
|
||||
cfc2.rt = 2;
|
||||
cfc2.rd = 11;
|
||||
|
||||
std::string cfc2Code = gen.translateInstruction(cfc2);
|
||||
printGeneratedCode("VU CReg access uses CFC2/CTC2 (CFC2)", cfc2Code);
|
||||
t.IsTrue(cfc2Code.find("SET_GPR_U32(ctx, 2") != std::string::npos, "CFC2 should write to rt");
|
||||
t.IsTrue(cfc2Code.find("ctx->vu0_status") != std::string::npos, "CFC2 STATUS should read vu0_status");
|
||||
t.IsTrue(cfc2Code.find("Unimplemented CFC2 VU CReg") == std::string::npos, "CFC2 should not hit unimplemented CReg path");
|
||||
std::string cfc2Code = gen.translateInstruction(cfc2);
|
||||
printGeneratedCode("VU CFC2/CTC2 access VI registers directly (CFC2)", cfc2Code);
|
||||
|
||||
Instruction ctc2{};
|
||||
ctc2.opcode = OPCODE_COP2;
|
||||
ctc2.rs = COP2_CTC2;
|
||||
ctc2.rt = 3;
|
||||
ctc2.rd = VU0_CR_ITOP;
|
||||
t.IsTrue(cfc2Code.find("SET_GPR_U32(ctx, 2") != std::string::npos, "CFC2 should write to rt");
|
||||
|
||||
std::string ctc2Code = gen.translateInstruction(ctc2);
|
||||
printGeneratedCode("VU CReg access uses CFC2/CTC2 (CTC2)", ctc2Code);
|
||||
t.IsTrue(ctc2Code.find("ctx->vu0_itop") != std::string::npos, "CTC2 ITOP should write vu0_itop");
|
||||
t.IsTrue(ctc2Code.find("GPR_U32(ctx, 3) & 0x3FF") != std::string::npos, "CTC2 ITOP should mask to 10 bits");
|
||||
t.IsTrue(ctc2Code.find("Unimplemented CTC2 VU CReg") == std::string::npos, "CTC2 should not hit unimplemented CReg path");
|
||||
t.IsTrue(cfc2Code.find("ctx->vi[11]") != std::string::npos, "CFC2 VI11 should read VI11");
|
||||
|
||||
t.IsTrue(cfc2Code.find("vu0_cmsar1") == std::string::npos, "CFC2 VI11 must not read CMSAR1");
|
||||
|
||||
t.IsTrue(cfc2Code.find("Unimplemented") == std::string::npos, "CFC2 VI11 should be implemented");
|
||||
|
||||
Instruction ctc2{};
|
||||
ctc2.opcode = OPCODE_COP2;
|
||||
ctc2.rs = COP2_CTC2;
|
||||
ctc2.rt = 3;
|
||||
ctc2.rd = 4;
|
||||
|
||||
std::string ctc2Code = gen.translateInstruction(ctc2);
|
||||
printGeneratedCode("VU CFC2/CTC2 access VI registers directly (CTC2)", ctc2Code);
|
||||
|
||||
t.IsTrue(ctc2Code.find("ctx->vi[4]") != std::string::npos, "CTC2 VI4 should write VI4");
|
||||
t.IsTrue(ctc2Code.find("static_cast<uint16_t>(GPR_U32(ctx, 3))") != std::string::npos, "CTC2 VI4 should store the low 16 bits");
|
||||
t.IsTrue(ctc2Code.find("vu0_i") == std::string::npos, "CTC2 VI4 must not write the I register");
|
||||
t.IsTrue(ctc2Code.find("Unimplemented") == std::string::npos, "CTC2 VI4 should be implemented");
|
||||
});
|
||||
|
||||
tc.Run("VU special control registers use hardware indices", [](TestCase& t)
|
||||
{
|
||||
CodeGenerator gen({}, {});
|
||||
|
||||
Instruction cfc2{};
|
||||
cfc2.opcode = OPCODE_COP2;
|
||||
cfc2.rs = COP2_CFC2;
|
||||
cfc2.rt = 2;
|
||||
cfc2.rd = VU0_CR_STATUS;
|
||||
|
||||
std::string cfc2Code = gen.translateInstruction(cfc2);
|
||||
printGeneratedCode("VU special control registers use hardware indices (STATUS)", cfc2Code);
|
||||
|
||||
t.IsTrue(cfc2Code.find("SET_GPR_U32(ctx, 2") != std::string::npos,"CFC2 should write to rt");
|
||||
t.IsTrue(cfc2Code.find("ctx->vu0_status") != std::string::npos, "CFC2 STATUS should read vu0_status");
|
||||
t.IsTrue(cfc2Code.find("Unimplemented") == std::string::npos,"CFC2 STATUS should be implemented");
|
||||
|
||||
Instruction ctc2{};
|
||||
ctc2.opcode = OPCODE_COP2;
|
||||
ctc2.rs = COP2_CTC2;
|
||||
ctc2.rt = 3;
|
||||
ctc2.rd = VU0_CR_FBRST;
|
||||
|
||||
std::string ctc2Code = gen.translateInstruction(ctc2);
|
||||
printGeneratedCode("VU special control registers use hardware indices (FBRST)", ctc2Code);
|
||||
|
||||
t.IsTrue(ctc2Code.find("ctx->vu0_fbrst") != std::string::npos, "CTC2 register 28 should write FBRST");
|
||||
t.IsTrue(ctc2Code.find("vu0_itop") == std::string::npos, "CTC2 register 28 must not write ITOP");
|
||||
t.IsTrue(ctc2Code.find("Unimplemented") == std::string::npos, "CTC2 FBRST should be implemented");
|
||||
});
|
||||
|
||||
tc.Run("scalar logical immediates emit low64 operations", [](TestCase &t) {
|
||||
@@ -1123,6 +1163,70 @@ void register_code_generator_tests()
|
||||
t.IsTrue(out.find("ctx->vu0_vf[25]") == std::string::npos, "S1 q/i must not use rs(format) as register index");
|
||||
});
|
||||
|
||||
tc.Run("VU0 destination MADD and MSUB forms preserve ACC", [](TestCase &t) {
|
||||
Instruction inst{};
|
||||
inst.rt = 7;
|
||||
inst.rd = 11;
|
||||
inst.sa = 3;
|
||||
inst.function = 0;
|
||||
inst.vectorInfo.vectorField = 0xE;
|
||||
|
||||
CodeGenerator gen({}, {});
|
||||
const std::vector<std::pair<const char *, std::string>> emitted = {
|
||||
{"MADD field", gen.translateVU_VMADD_Field(inst)},
|
||||
{"MADD", gen.translateVU_VMADD(inst)},
|
||||
{"MADDq", gen.translateVU_VMADDq(inst)},
|
||||
{"MADDi", gen.translateVU_VMADDi(inst)},
|
||||
{"MSUB field", gen.translateVU_VMSUB_Field(inst)},
|
||||
{"MSUB", gen.translateVU_VMSUB(inst)},
|
||||
{"MSUBq", gen.translateVU_VMSUBq(inst)},
|
||||
{"MSUBi", gen.translateVU_VMSUBi(inst)},
|
||||
{"OPMSUB", gen.translateVU_VOPMSUB(inst)},
|
||||
};
|
||||
|
||||
for (const auto &[name, code] : emitted)
|
||||
{
|
||||
const std::string message =
|
||||
std::string(name) + " writes VF and must not overwrite ACC";
|
||||
t.IsTrue(code.find("ctx->vu0_acc = res") == std::string::npos,
|
||||
message.c_str());
|
||||
t.IsTrue(code.find("PS2_VADD(ctx->vu0_acc") != std::string::npos ||
|
||||
code.find("PS2_VSUB(ctx->vu0_acc") != std::string::npos,
|
||||
(std::string(name) + " must still read ACC").c_str());
|
||||
}
|
||||
|
||||
const std::string madda = gen.translateVU_VMADDA(inst);
|
||||
t.IsTrue(madda.find("ctx->vu0_acc =") != std::string::npos,
|
||||
"MADDA must continue writing ACC");
|
||||
});
|
||||
|
||||
tc.Run("VU0 OPMULA and OPMSUB use cross-product lane permutations", [](TestCase &t) {
|
||||
Instruction inst{};
|
||||
inst.rt = 7;
|
||||
inst.rd = 11;
|
||||
inst.sa = 3;
|
||||
inst.vectorInfo.vectorField = 0xE;
|
||||
|
||||
CodeGenerator gen({}, {});
|
||||
const std::string opmula = gen.translateVU_VOPMULA(inst);
|
||||
const std::string opmsub = gen.translateVU_VOPMSUB(inst);
|
||||
|
||||
for (const std::string *code : {&opmula, &opmsub})
|
||||
{
|
||||
t.IsTrue(code->find("_MM_SHUFFLE(3,0,2,1)") != std::string::npos,
|
||||
"OPM source Fs must be permuted to y,z,x");
|
||||
t.IsTrue(code->find("_MM_SHUFFLE(3,1,0,2)") != std::string::npos,
|
||||
"OPM source Ft must be permuted to z,x,y");
|
||||
t.IsTrue(code->find("PS2_VMUL(fs_yzx, ft_zxy)") != std::string::npos,
|
||||
"OPM product must use the permuted operands");
|
||||
}
|
||||
|
||||
t.IsTrue(opmula.find("ctx->vu0_acc =") != std::string::npos,
|
||||
"OPMULA must write the permuted product to ACC");
|
||||
t.IsTrue(opmsub.find("ctx->vu0_acc = res") == std::string::npos,
|
||||
"OPMSUB must preserve ACC after producing the cross product");
|
||||
});
|
||||
|
||||
tc.Run("VU0 S2 vector ops use rd as source and rt as destination", [](TestCase &t) {
|
||||
Instruction inst{};
|
||||
inst.opcode = OPCODE_COP2;
|
||||
|
||||
@@ -11,6 +11,7 @@ void register_ps2_runtime_kernel_tests();
|
||||
void register_ps2_runtime_interrupt_tests();
|
||||
void register_ps2_memory_tests();
|
||||
void register_ps2_vu1_tests();
|
||||
void register_ps2_vu_tests();
|
||||
void register_ps2_gs_tests();
|
||||
void register_ps2_iop_tests();
|
||||
void register_ps2_sif_rpc_tests();
|
||||
@@ -32,6 +33,7 @@ int main()
|
||||
register_ps2_runtime_interrupt_tests();
|
||||
register_ps2_memory_tests();
|
||||
register_ps2_vu1_tests();
|
||||
register_ps2_vu_tests();
|
||||
register_ps2_gs_tests();
|
||||
register_ps2_iop_tests();
|
||||
register_ps2_sif_rpc_tests();
|
||||
|
||||
+625
-69
@@ -6,6 +6,7 @@
|
||||
#include "runtime/ps2_gs_gpu.h"
|
||||
#include "runtime/ee_scheduler.h"
|
||||
#include "runtime/ps2_gs_memory.h"
|
||||
#include "runtime/ps2_gs_rasterizer.h"
|
||||
#include "runtime/ps2_gs_psmct32.h"
|
||||
#include "runtime/ps2_gs_psmt4.h"
|
||||
#include "runtime/ps2_gs_psmt8.h"
|
||||
@@ -357,6 +358,57 @@ namespace
|
||||
t.Equals(probe, expectedBase, message);
|
||||
runtime.guestFree(probe);
|
||||
}
|
||||
|
||||
struct GsPixelTestResult
|
||||
{
|
||||
uint32_t framebuffer = 0u;
|
||||
uint32_t depth = 0u;
|
||||
};
|
||||
|
||||
GsPixelTestResult drawGsPixelForTests(uint8_t framePsm,
|
||||
uint64_t testReg,
|
||||
bool zmask,
|
||||
uint32_t initialFramebuffer,
|
||||
uint32_t initialDepth,
|
||||
uint8_t sourceAlpha)
|
||||
{
|
||||
constexpr uint32_t kFrameBlock = 0u;
|
||||
constexpr uint32_t kDepthBlock = 32u;
|
||||
constexpr uint32_t kSourceDepth = 0x22222222u;
|
||||
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
gs.WriteVram(framePsm, kFrameBlock, 1u, 0u, 0u, initialFramebuffer);
|
||||
gs.WriteVram(GS_PSM_Z32, kDepthBlock, 1u, 0u, 0u, initialDepth);
|
||||
|
||||
const uint64_t frame =
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(framePsm) << 24);
|
||||
const uint64_t zbuf =
|
||||
1ull |
|
||||
(static_cast<uint64_t>(zmask ? 1u : 0u) << 32);
|
||||
const uint64_t rgbaq =
|
||||
(0x12ull << 0) |
|
||||
(0x34ull << 8) |
|
||||
(0x56ull << 16) |
|
||||
(static_cast<uint64_t>(sourceAlpha) << 24) |
|
||||
(0x3F800000ull << 32);
|
||||
|
||||
gs.writeRegister(GS_REG_FRAME_1, frame);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, zbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEST_1, testReg);
|
||||
gs.writeRegister(GS_REG_PRIM, static_cast<uint64_t>(GS_PRIM_POINT));
|
||||
gs.writeRegister(GS_REG_RGBAQ, rgbaq);
|
||||
gs.writeRegister(GS_REG_XYZ2, static_cast<uint64_t>(kSourceDepth) << 32);
|
||||
|
||||
return {
|
||||
gs.ReadVram(framePsm, kFrameBlock, 1u, 0u, 0u),
|
||||
gs.ReadVram(GS_PSM_Z32, kDepthBlock, 1u, 0u, 0u),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void register_ps2_gs_tests()
|
||||
@@ -683,6 +735,126 @@ void register_ps2_gs_tests()
|
||||
"context-targeted clear should leave the other context framebuffer untouched");
|
||||
});
|
||||
|
||||
tc.Run("XYZ3 culls a triangle strip primitive without desynchronizing the vertex queue", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
constexpr uint32_t kColor = 0xFF0000FFu;
|
||||
constexpr uint64_t kFrame =
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
|
||||
constexpr uint64_t kZbuf = (1ull << 32);
|
||||
constexpr uint64_t kScissor =
|
||||
(6ull << 16) |
|
||||
(6ull << 48);
|
||||
|
||||
auto xyz = [](uint32_t x, uint32_t y) -> uint64_t
|
||||
{
|
||||
return static_cast<uint64_t>(x * 16u) |
|
||||
(static_cast<uint64_t>(y * 16u) << 16);
|
||||
};
|
||||
|
||||
gs.writeRegister(GS_REG_FRAME_1, kFrame);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, kScissor);
|
||||
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
|
||||
gs.writeRegister(GS_REG_PRIM, static_cast<uint64_t>(GS_PRIM_TRISTRIP));
|
||||
gs.writeRegister(GS_REG_RGBAQ, kColor);
|
||||
|
||||
// ABC is rejected by XYZ3. D must then draw BCD, not stale ABC.
|
||||
gs.writeRegister(GS_REG_XYZ2, xyz(0u, 0u));
|
||||
gs.writeRegister(GS_REG_XYZ2, xyz(6u, 0u));
|
||||
gs.writeRegister(GS_REG_XYZ3, xyz(0u, 6u));
|
||||
gs.writeRegister(GS_REG_XYZ2, xyz(6u, 6u));
|
||||
|
||||
t.Equals(readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 1u), 0u,
|
||||
"XYZ3 should suppress the completed ABC triangle");
|
||||
t.Equals(readReferencePSMCT32Pixel(vram, 0u, 1u, 4u, 4u), kColor,
|
||||
"the next XYZ2 should draw BCD from the advanced strip queue");
|
||||
});
|
||||
|
||||
tc.Run("GS fog blends the shaded color toward FOGCOL before framebuffer blending", [](TestCase &t)
|
||||
{
|
||||
auto renderFoggedPoint = [](bool fogEnabled, uint8_t fog, uint32_t fogColor = 0u) -> uint32_t
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
constexpr uint64_t kFrame =
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
|
||||
constexpr uint64_t kZbuf = (1ull << 32);
|
||||
constexpr uint64_t kWhite = 0x80FFFFFFull;
|
||||
|
||||
gs.writeRegister(GS_REG_FRAME_1, kFrame);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, 0ull);
|
||||
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
|
||||
gs.writeRegister(GS_REG_FOGCOL, fogColor);
|
||||
gs.writeRegister(
|
||||
GS_REG_PRIM,
|
||||
static_cast<uint64_t>(GS_PRIM_POINT) |
|
||||
(static_cast<uint64_t>(fogEnabled ? 1u : 0u) << 5));
|
||||
gs.writeRegister(GS_REG_RGBAQ, kWhite);
|
||||
gs.writeRegister(GS_REG_FOG, static_cast<uint64_t>(fog) << 56);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
|
||||
return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u);
|
||||
};
|
||||
|
||||
t.Equals(renderFoggedPoint(false, 0x80u), 0x80FFFFFFu,
|
||||
"FOG and FOGCOL must not affect primitives with FGE disabled");
|
||||
t.Equals(renderFoggedPoint(true, 0x80u), 0x807F7F7Fu,
|
||||
"F=0x80 over black FOGCOL should halve the point RGB and preserve alpha");
|
||||
t.Equals(renderFoggedPoint(true, 0x00u), 0x80000000u,
|
||||
"F=0 should replace the point RGB with black FOGCOL");
|
||||
t.Equals(renderFoggedPoint(true, 0x00u, 0x00302010u), 0x802F1F0Fu,
|
||||
"F=0 should replace point RGB with the programmed FOGCOL");
|
||||
});
|
||||
|
||||
tc.Run("PRMODE supplies primitive attributes while PRMODECONT AC is clear", [](TestCase &t)
|
||||
{
|
||||
auto renderPoint = [](bool usePrmodeAttributes) -> uint32_t
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
constexpr uint64_t kFrame =
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
|
||||
constexpr uint64_t kZbuf = (1ull << 32);
|
||||
|
||||
gs.writeRegister(GS_REG_FRAME_1, kFrame);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, 0ull);
|
||||
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
|
||||
gs.writeRegister(GS_REG_FOGCOL, 0ull);
|
||||
gs.writeRegister(GS_REG_RGBAQ, 0x80FFFFFFull);
|
||||
gs.writeRegister(GS_REG_FOG, 0ull);
|
||||
gs.writeRegister(GS_REG_PRMODE, 1ull << 5);
|
||||
gs.writeRegister(GS_REG_PRMODECONT, usePrmodeAttributes ? 0ull : 1ull);
|
||||
|
||||
// FGE is clear in PRIM. AC decides whether that clear bit or
|
||||
// PRMODE's set bit supplies the effective fog enable.
|
||||
gs.writeRegister(GS_REG_PRIM, static_cast<uint64_t>(GS_PRIM_POINT));
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
|
||||
return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u);
|
||||
};
|
||||
|
||||
t.Equals(renderPoint(true), 0x80000000u,
|
||||
"AC=0 should retain FGE from PRMODE across a PRIM write");
|
||||
t.Equals(renderPoint(false), 0x80FFFFFFu,
|
||||
"AC=1 should source FGE from PRIM instead of PRMODE");
|
||||
});
|
||||
|
||||
tc.Run("PABE bypasses alpha blend for low-alpha source pixels", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
@@ -1617,6 +1789,20 @@ void register_ps2_gs_tests()
|
||||
t.Equals(regs.display2, display2, "A+D should write GS DISPLAY2");
|
||||
});
|
||||
|
||||
tc.Run("reserved PSM 0x3F uses null VRAM handlers", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0xA5u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
t.Equals(gs.ReadVram(0x3Fu, 0u, 1u, 0u, 0u), 0u,
|
||||
"reserved PSM reads should use the null handler");
|
||||
|
||||
gs.WriteVram(0x3Fu, 0u, 1u, 0u, 0u, 0x0005180Bu);
|
||||
t.Equals(static_cast<uint32_t>(vram[0]), 0xA5u,
|
||||
"reserved PSM writes should leave VRAM unchanged");
|
||||
});
|
||||
|
||||
tc.Run("PSMT4 address mapping matches GS manual layout", [](TestCase &t)
|
||||
{
|
||||
constexpr uint32_t kBaseBlock = 0u;
|
||||
@@ -2548,6 +2734,161 @@ void register_ps2_gs_tests()
|
||||
"T8 CSM1 CLUT sampling should read CT32-uploaded palette entries through GS swizzled addressing");
|
||||
});
|
||||
|
||||
tc.Run("GS T8 CSM1 applies CSA and masks CSA bit 4 for CT32 CLUTs", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
constexpr uint32_t kTexTbp = 64u;
|
||||
constexpr uint32_t kClutCbp = 128u;
|
||||
constexpr uint64_t kFrameReg =
|
||||
(0ull << 0) |
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
|
||||
constexpr uint64_t kZbuf = (1ull << 32);
|
||||
constexpr uint64_t kTex0 =
|
||||
(static_cast<uint64_t>(kTexTbp) << 0) |
|
||||
(1ull << 14) |
|
||||
(static_cast<uint64_t>(GS_PSM_T8) << 20) |
|
||||
(0ull << 26) |
|
||||
(0ull << 30) |
|
||||
(1ull << 34) |
|
||||
(1ull << 35) |
|
||||
(static_cast<uint64_t>(kClutCbp) << 37) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 51) |
|
||||
(17ull << 56);
|
||||
constexpr uint64_t kPrim =
|
||||
static_cast<uint64_t>(GS_PRIM_SPRITE) |
|
||||
(1ull << 4) |
|
||||
(1ull << 8);
|
||||
constexpr uint32_t kExpectedColor = 0xFF204080u;
|
||||
constexpr uint32_t kWrongNoCsaColor = 0xFF00FF00u;
|
||||
constexpr uint32_t kWrongBit4Color = 0xFFFF0000u;
|
||||
|
||||
const uint32_t texOff = GSPSMT8::addrPSMT8(kTexTbp, 1u, 0u, 0u);
|
||||
vram[texOff] = 0u;
|
||||
|
||||
// CSA=17 is CSA=1 for a CT32 CLUT. Logical entry 16 is at
|
||||
// physical CSM1 entry 8 after address bits 3 and 4 are swapped.
|
||||
gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 0u, 0u, kWrongNoCsaColor);
|
||||
gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 8u, 0u, kExpectedColor);
|
||||
gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 8u, 16u, kWrongBit4Color);
|
||||
|
||||
gs.writeRegister(GS_REG_FRAME_1, kFrameReg);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, 0ull);
|
||||
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
|
||||
gs.writeRegister(GS_REG_ALPHA_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEX0_1, kTex0);
|
||||
gs.writeRegister(GS_REG_PRIM, kPrim);
|
||||
gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull);
|
||||
gs.writeRegister(GS_REG_UV, 0ull);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
gs.writeRegister(GS_REG_UV, 0ull);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
|
||||
uint32_t pixel = 0u;
|
||||
std::memcpy(&pixel, vram.data(), sizeof(pixel));
|
||||
t.Equals(pixel, kExpectedColor,
|
||||
"T8 CSM1 should offset by CSA while CT32 ignores the fifth CSA bit");
|
||||
});
|
||||
|
||||
tc.Run("GS T4 CSM1 preserves CSA bit 4 for CT16 CLUTs", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
constexpr uint32_t kTexTbp = 64u;
|
||||
constexpr uint32_t kClutCbp = 128u;
|
||||
constexpr uint64_t kFrameReg =
|
||||
(0ull << 0) |
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
|
||||
constexpr uint64_t kZbuf = (1ull << 32);
|
||||
constexpr uint64_t kTex0 =
|
||||
(static_cast<uint64_t>(kTexTbp) << 0) |
|
||||
(1ull << 14) |
|
||||
(static_cast<uint64_t>(GS_PSM_T4) << 20) |
|
||||
(0ull << 26) |
|
||||
(0ull << 30) |
|
||||
(1ull << 34) |
|
||||
(1ull << 35) |
|
||||
(static_cast<uint64_t>(kClutCbp) << 37) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT16) << 51) |
|
||||
(16ull << 56);
|
||||
constexpr uint64_t kTexa = (0x80ull << 32);
|
||||
constexpr uint64_t kPrim =
|
||||
static_cast<uint64_t>(GS_PRIM_SPRITE) |
|
||||
(1ull << 4) |
|
||||
(1ull << 8);
|
||||
constexpr uint16_t kExpectedRed = 0x801Fu;
|
||||
constexpr uint16_t kWrongGreen = 0x83E0u;
|
||||
constexpr uint32_t kExpectedColor = 0x800000F8u;
|
||||
|
||||
writePSMT4Texel(vram, kTexTbp, 1u, 0u, 0u, 1u);
|
||||
|
||||
// CSA=16 selects the upper half of a CT16 CLUT. CSM1 swaps bits
|
||||
// 3 and 4 but must preserve address bit 8.
|
||||
gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 0u, kWrongGreen);
|
||||
gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 16u, kExpectedRed);
|
||||
|
||||
gs.writeRegister(GS_REG_FRAME_1, kFrameReg);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, 0ull);
|
||||
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
|
||||
gs.writeRegister(GS_REG_ALPHA_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEX0_1, kTex0);
|
||||
gs.writeRegister(GS_REG_TEXA, kTexa);
|
||||
gs.writeRegister(GS_REG_PRIM, kPrim);
|
||||
gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull);
|
||||
gs.writeRegister(GS_REG_UV, 0ull);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
gs.writeRegister(GS_REG_UV, 0ull);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
|
||||
uint32_t pixel = 0u;
|
||||
std::memcpy(&pixel, vram.data(), sizeof(pixel));
|
||||
t.Equals(pixel, kExpectedColor,
|
||||
"CT16 CSM1 should retain CSA[4] instead of aliasing the upper palette onto the lower one");
|
||||
});
|
||||
|
||||
tc.Run("GS TEX0 dimensions saturate at 1024 pixels", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
GSRasterizer rasterizer;
|
||||
|
||||
constexpr uint32_t kTexTbp = 64u;
|
||||
constexpr uint64_t kTex0 =
|
||||
(static_cast<uint64_t>(kTexTbp) << 0) |
|
||||
(16ull << 14) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 20) |
|
||||
(15ull << 26) |
|
||||
(15ull << 30) |
|
||||
(1ull << 34) |
|
||||
(1ull << 35);
|
||||
constexpr uint64_t kPrim =
|
||||
static_cast<uint64_t>(GS_PRIM_TRIANGLE) |
|
||||
(1ull << 4);
|
||||
constexpr uint32_t kExpectedColor = 0xFF3366CCu;
|
||||
constexpr uint32_t kUnsaturatedColor = 0xFF00FF00u;
|
||||
|
||||
gs.WriteVram(GS_PSM_CT32, kTexTbp, 16u, 1u, 0u, kExpectedColor);
|
||||
gs.WriteVram(GS_PSM_CT32, kTexTbp, 16u, 32u, 0u, kUnsaturatedColor);
|
||||
gs.writeRegister(GS_REG_TEX0_1, kTex0);
|
||||
gs.writeRegister(GS_REG_PRIM, kPrim);
|
||||
|
||||
const uint32_t sampled =
|
||||
rasterizer.sampleTexture(&gs, 1.0f / 1024.0f, 0.0f, 1.0f, 0u, 0u);
|
||||
t.Equals(sampled, kExpectedColor,
|
||||
"TW/TH values above 10 should address a 1024-pixel texture instead of growing beyond GS limits");
|
||||
});
|
||||
|
||||
tc.Run("GS TEX2 updates CLUT state independently from TEX0", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
@@ -3035,97 +3376,312 @@ void register_ps2_gs_tests()
|
||||
"linear filtering should preserve the shared opaque alpha from the CLUT entries");
|
||||
});
|
||||
|
||||
tc.Run("GS alpha test AFAIL framebuffer-only still writes the pixel", [](TestCase &t)
|
||||
tc.Run("GS CLAMP modes transform texture coordinates before sampling", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
auto renderConstantUv = [](uint64_t clampReg,
|
||||
uint16_t fixedU,
|
||||
uint16_t fixedV) -> uint32_t
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
constexpr uint64_t kFrame =
|
||||
(0ull << 0) |
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
|
||||
constexpr uint64_t kZbuf = (1ull << 32);
|
||||
constexpr uint64_t kScissor =
|
||||
(0ull << 0) |
|
||||
(0ull << 16) |
|
||||
(0ull << 32) |
|
||||
(0ull << 48);
|
||||
constexpr uint64_t kTest =
|
||||
1ull | // ATE
|
||||
(5ull << 1) | // ATST = GEQUAL
|
||||
(0x80ull << 4) | // AREF
|
||||
(1ull << 12) | // AFAIL = FB_ONLY
|
||||
(1ull << 17); // ZTST = ALWAYS
|
||||
constexpr uint64_t kPrim =
|
||||
static_cast<uint64_t>(GS_PRIM_POINT);
|
||||
constexpr uint64_t kRgbaq =
|
||||
(0x12ull << 0) |
|
||||
(0x34ull << 8) |
|
||||
(0x56ull << 16) |
|
||||
(0x00ull << 24) |
|
||||
(0x3F800000ull << 32); // q = 1.0f
|
||||
constexpr uint32_t kTexTbp = 64u;
|
||||
constexpr uint32_t kTexel0 = 0x800000FFu;
|
||||
constexpr uint32_t kTexel1 = 0x8000FF00u;
|
||||
constexpr uint32_t kTexel2 = 0x80FF0000u;
|
||||
constexpr uint32_t kTexel3 = 0x80FFFFFFu;
|
||||
constexpr uint32_t kTexelV3 = 0x80FFFF00u;
|
||||
constexpr uint64_t kFrame =
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
|
||||
constexpr uint64_t kZbuf = (1ull << 32);
|
||||
constexpr uint64_t kTex0 =
|
||||
(static_cast<uint64_t>(kTexTbp) << 0) |
|
||||
(1ull << 14) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 20) |
|
||||
(2ull << 26) |
|
||||
(2ull << 30) |
|
||||
(1ull << 34) |
|
||||
(1ull << 35);
|
||||
constexpr uint64_t kPrim =
|
||||
static_cast<uint64_t>(GS_PRIM_TRIANGLE) |
|
||||
(1ull << 4) |
|
||||
(1ull << 8);
|
||||
constexpr uint64_t kRgbaq = 0x3F80000080808080ull;
|
||||
|
||||
gs.writeRegister(GS_REG_FRAME_1, kFrame);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, kScissor);
|
||||
gs.writeRegister(GS_REG_TEST_1, kTest);
|
||||
gs.writeRegister(GS_REG_PRIM, kPrim);
|
||||
gs.writeRegister(GS_REG_RGBAQ, kRgbaq);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 0u, 0u, kTexel0);
|
||||
writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 1u, 0u, kTexel1);
|
||||
writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 2u, 0u, kTexel2);
|
||||
writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 3u, 0u, kTexel3);
|
||||
writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 0u, 3u, kTexelV3);
|
||||
|
||||
uint32_t pixel = 0u;
|
||||
std::memcpy(&pixel, vram.data(), sizeof(pixel));
|
||||
t.Equals(pixel, 0x00563412u,
|
||||
"AFAIL=FB_ONLY should still update the framebuffer when the alpha test fails");
|
||||
gs.writeRegister(GS_REG_FRAME_1, kFrame);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, (3ull << 16) | (3ull << 48));
|
||||
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
|
||||
gs.writeRegister(GS_REG_TEX0_1, kTex0);
|
||||
gs.writeRegister(GS_REG_CLAMP_1, clampReg);
|
||||
gs.writeRegister(GS_REG_PRIM, kPrim);
|
||||
gs.writeRegister(GS_REG_RGBAQ, kRgbaq);
|
||||
|
||||
const uint64_t uv =
|
||||
static_cast<uint64_t>(fixedU) |
|
||||
(static_cast<uint64_t>(fixedV) << 16);
|
||||
gs.writeRegister(GS_REG_UV, uv);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
gs.writeRegister(GS_REG_UV, uv);
|
||||
gs.writeRegister(GS_REG_XYZ2, 32ull);
|
||||
gs.writeRegister(GS_REG_UV, uv);
|
||||
gs.writeRegister(GS_REG_XYZ2, (32ull << 16));
|
||||
|
||||
return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u);
|
||||
};
|
||||
|
||||
constexpr uint64_t kClamp = 1ull;
|
||||
constexpr uint64_t kRegionClamp =
|
||||
2ull |
|
||||
(1ull << 4) |
|
||||
(2ull << 14);
|
||||
constexpr uint64_t kRegionRepeat =
|
||||
3ull |
|
||||
(1ull << 4) |
|
||||
(2ull << 14);
|
||||
|
||||
t.Equals(renderConstantUv(0ull, 4u * 16u, 0u), 0x800000FFu,
|
||||
"REPEAT should wrap texel 4 to texel 0 for a four-wide texture");
|
||||
t.Equals(renderConstantUv(0ull, 0u, 4u * 16u), 0x800000FFu,
|
||||
"REPEAT should wrap texel row 4 to row 0 for a four-high texture");
|
||||
t.Equals(renderConstantUv(kClamp, 4u * 16u, 0u), 0x80FFFFFFu,
|
||||
"CLAMP should hold texel 4 at the last texel");
|
||||
t.Equals(renderConstantUv(kRegionClamp, 3u * 16u, 0u), 0x80FF0000u,
|
||||
"REGION_CLAMP should hold texel 3 at MAXU=2");
|
||||
t.Equals(renderConstantUv(kRegionRepeat, 4u * 16u, 0u), 0x80FF0000u,
|
||||
"REGION_REPEAT should calculate (U & UMSK) | UFIX");
|
||||
});
|
||||
|
||||
tc.Run("GS alpha test AFAIL RGB-only preserves destination alpha", [](TestCase &t)
|
||||
tc.Run("GS STQ triangle interpolation divides homogeneous coordinates after DDA", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
GS gs;
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
|
||||
constexpr uint32_t kTexTbp = 64u;
|
||||
constexpr uint64_t kFrame =
|
||||
(0ull << 0) |
|
||||
(1ull << 16) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 24);
|
||||
constexpr uint64_t kZbuf = (1ull << 32);
|
||||
constexpr uint64_t kScissor =
|
||||
(0ull << 0) |
|
||||
(0ull << 16) |
|
||||
(0ull << 32) |
|
||||
(0ull << 48);
|
||||
constexpr uint64_t kTest =
|
||||
1ull | // ATE
|
||||
(5ull << 1) | // ATST = GEQUAL
|
||||
(0x80ull << 4) | // AREF
|
||||
(3ull << 12) | // AFAIL = RGB_ONLY
|
||||
(1ull << 17); // ZTST = ALWAYS
|
||||
constexpr uint64_t kTex0 =
|
||||
(static_cast<uint64_t>(kTexTbp) << 0) |
|
||||
(1ull << 14) |
|
||||
(static_cast<uint64_t>(GS_PSM_CT32) << 20) |
|
||||
(2ull << 26) |
|
||||
(1ull << 34) |
|
||||
(1ull << 35);
|
||||
constexpr uint64_t kPrim =
|
||||
static_cast<uint64_t>(GS_PRIM_POINT);
|
||||
constexpr uint64_t kRgbaq =
|
||||
(0x12ull << 0) |
|
||||
(0x34ull << 8) |
|
||||
(0x56ull << 16) |
|
||||
(0x00ull << 24) |
|
||||
(0x3F800000ull << 32); // q = 1.0f
|
||||
constexpr uint32_t kExisting = 0xAB030201u;
|
||||
static_cast<uint64_t>(GS_PRIM_TRIANGLE) |
|
||||
(1ull << 4);
|
||||
constexpr uint32_t kAffineTexel = 0x800000FFu;
|
||||
constexpr uint32_t kHomogeneousTexel = 0x8000FF00u;
|
||||
|
||||
std::memcpy(vram.data(), &kExisting, sizeof(kExisting));
|
||||
auto packFloat = [](float value) -> uint32_t
|
||||
{
|
||||
uint32_t bits = 0u;
|
||||
std::memcpy(&bits, &value, sizeof(bits));
|
||||
return bits;
|
||||
};
|
||||
auto packSt = [&](float s, float tVal) -> uint64_t
|
||||
{
|
||||
return static_cast<uint64_t>(packFloat(s)) |
|
||||
(static_cast<uint64_t>(packFloat(tVal)) << 32);
|
||||
};
|
||||
auto packRgbaq = [&](float q) -> uint64_t
|
||||
{
|
||||
return 0x80808080ull |
|
||||
(static_cast<uint64_t>(packFloat(q)) << 32);
|
||||
};
|
||||
|
||||
writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 1u, 0u, kAffineTexel);
|
||||
writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 2u, 0u, kHomogeneousTexel);
|
||||
|
||||
gs.writeRegister(GS_REG_FRAME_1, kFrame);
|
||||
gs.writeRegister(GS_REG_ZBUF_1, kZbuf);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, kScissor);
|
||||
gs.writeRegister(GS_REG_TEST_1, kTest);
|
||||
gs.writeRegister(GS_REG_SCISSOR_1, (4ull << 16) | (4ull << 48));
|
||||
gs.writeRegister(GS_REG_XYOFFSET_1, 0ull);
|
||||
gs.writeRegister(GS_REG_TEST_1, 0x30000ull);
|
||||
gs.writeRegister(GS_REG_TEX0_1, kTex0);
|
||||
gs.writeRegister(GS_REG_CLAMP_1, 1ull);
|
||||
gs.writeRegister(GS_REG_PRIM, kPrim);
|
||||
gs.writeRegister(GS_REG_RGBAQ, kRgbaq);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
|
||||
uint32_t pixel = 0u;
|
||||
std::memcpy(&pixel, vram.data(), sizeof(pixel));
|
||||
t.Equals(pixel, 0xAB563412u,
|
||||
"AFAIL=RGB_ONLY should update RGB while preserving destination alpha");
|
||||
gs.writeRegister(GS_REG_ST, packSt(0.0f, 0.0f));
|
||||
gs.writeRegister(GS_REG_RGBAQ, packRgbaq(1.0f));
|
||||
gs.writeRegister(GS_REG_XYZ2, 0ull);
|
||||
gs.writeRegister(GS_REG_ST, packSt(2.0f, 0.0f));
|
||||
gs.writeRegister(GS_REG_RGBAQ, packRgbaq(2.0f));
|
||||
gs.writeRegister(GS_REG_XYZ2, 64ull);
|
||||
gs.writeRegister(GS_REG_ST, packSt(0.0f, 0.0f));
|
||||
gs.writeRegister(GS_REG_RGBAQ, packRgbaq(1.0f));
|
||||
gs.writeRegister(GS_REG_XYZ2, (64ull << 16));
|
||||
|
||||
const uint32_t pixel =
|
||||
readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 1u);
|
||||
t.Equals(pixel, kHomogeneousTexel,
|
||||
"the DDA should interpolate S=0.75 and Q=1.375, selecting texel 2 after S/Q");
|
||||
});
|
||||
|
||||
tc.Run("GS alpha-test AFAIL independently masks framebuffer and depth", [](TestCase &t)
|
||||
{
|
||||
constexpr uint32_t kInitialFramebuffer = 0xAB030201u;
|
||||
constexpr uint32_t kInitialDepth = 0x11111111u;
|
||||
constexpr uint64_t kTestBase =
|
||||
1ull | // ATE
|
||||
(5ull << 1) | // ATST = GEQUAL
|
||||
(0x80ull << 4) | // AREF
|
||||
(1ull << 16) | // ZTE
|
||||
(1ull << 17); // ZTST = ALWAYS
|
||||
|
||||
const GsPixelTestResult keep =
|
||||
drawGsPixelForTests(GS_PSM_CT32, kTestBase | (0ull << 12), false,
|
||||
kInitialFramebuffer, kInitialDepth, 0x00u);
|
||||
t.Equals(keep.framebuffer, kInitialFramebuffer,
|
||||
"AFAIL=KEEP should preserve the framebuffer");
|
||||
t.Equals(keep.depth, kInitialDepth,
|
||||
"AFAIL=KEEP should preserve depth");
|
||||
|
||||
const GsPixelTestResult framebufferOnly =
|
||||
drawGsPixelForTests(GS_PSM_CT32, kTestBase | (1ull << 12), false,
|
||||
kInitialFramebuffer, kInitialDepth, 0x00u);
|
||||
t.Equals(framebufferOnly.framebuffer, 0x00563412u,
|
||||
"AFAIL=FB_ONLY should update RGBA");
|
||||
t.Equals(framebufferOnly.depth, kInitialDepth,
|
||||
"AFAIL=FB_ONLY should preserve depth");
|
||||
|
||||
const GsPixelTestResult depthOnly =
|
||||
drawGsPixelForTests(GS_PSM_CT32, kTestBase | (2ull << 12), false,
|
||||
kInitialFramebuffer, kInitialDepth, 0x00u);
|
||||
t.Equals(depthOnly.framebuffer, kInitialFramebuffer,
|
||||
"AFAIL=ZB_ONLY should preserve the framebuffer");
|
||||
t.Equals(depthOnly.depth, 0x22222222u,
|
||||
"AFAIL=ZB_ONLY should update depth");
|
||||
|
||||
const GsPixelTestResult rgbOnly =
|
||||
drawGsPixelForTests(GS_PSM_CT32, kTestBase | (3ull << 12), false,
|
||||
kInitialFramebuffer, kInitialDepth, 0x00u);
|
||||
t.Equals(rgbOnly.framebuffer, 0xAB563412u,
|
||||
"AFAIL=RGB_ONLY should preserve destination alpha on CT32");
|
||||
t.Equals(rgbOnly.depth, kInitialDepth,
|
||||
"AFAIL=RGB_ONLY should preserve depth");
|
||||
});
|
||||
|
||||
tc.Run("GS RGB_ONLY falls back to FB_ONLY outside CT32", [](TestCase &t)
|
||||
{
|
||||
constexpr uint32_t kInitialDepth = 0x11111111u;
|
||||
constexpr uint64_t kTest =
|
||||
1ull |
|
||||
(5ull << 1) |
|
||||
(0x80ull << 4) |
|
||||
(3ull << 12) |
|
||||
(1ull << 16) |
|
||||
(1ull << 17);
|
||||
|
||||
const GsPixelTestResult ct24 =
|
||||
drawGsPixelForTests(GS_PSM_CT24, kTest, false,
|
||||
0x00030201u, kInitialDepth, 0x00u);
|
||||
t.Equals(ct24.framebuffer, 0x00563412u,
|
||||
"RGB_ONLY should write the full CT24 framebuffer pixel");
|
||||
t.Equals(ct24.depth, kInitialDepth,
|
||||
"RGB_ONLY-as-FB_ONLY should preserve CT24 depth");
|
||||
|
||||
const GsPixelTestResult ct16 =
|
||||
drawGsPixelForTests(GS_PSM_CT16, kTest, false,
|
||||
0x8001u, kInitialDepth, 0x00u);
|
||||
t.Equals(ct16.framebuffer, 0x28C2u,
|
||||
"RGB_ONLY should write RGB and alpha for CT16");
|
||||
t.Equals(ct16.depth, kInitialDepth,
|
||||
"RGB_ONLY-as-FB_ONLY should preserve CT16 depth");
|
||||
});
|
||||
|
||||
tc.Run("GS ZMSK suppresses depth without suppressing framebuffer writes", [](TestCase &t)
|
||||
{
|
||||
constexpr uint64_t kTest =
|
||||
1ull |
|
||||
(5ull << 1) |
|
||||
(0x80ull << 4) |
|
||||
(1ull << 16) |
|
||||
(1ull << 17);
|
||||
const GsPixelTestResult result =
|
||||
drawGsPixelForTests(GS_PSM_CT32, kTest, true,
|
||||
0xAB030201u, 0x11111111u, 0x80u);
|
||||
|
||||
t.Equals(result.framebuffer, 0x80563412u,
|
||||
"a passing alpha test should write the framebuffer");
|
||||
t.Equals(result.depth, 0x11111111u,
|
||||
"ZMSK should preserve depth");
|
||||
});
|
||||
|
||||
tc.Run("GS DATE and DATM inspect the framebuffer-format alpha bit", [](TestCase &t)
|
||||
{
|
||||
constexpr uint32_t kInitialDepth = 0x11111111u;
|
||||
constexpr uint64_t kTestBase =
|
||||
(1ull << 14) | // DATE
|
||||
(1ull << 16) | // ZTE
|
||||
(1ull << 17); // ZTST = ALWAYS
|
||||
|
||||
const GsPixelTestResult ct32ZeroPass =
|
||||
drawGsPixelForTests(GS_PSM_CT32, kTestBase, false,
|
||||
0x00030201u, kInitialDepth, 0x80u);
|
||||
t.Equals(ct32ZeroPass.framebuffer, 0x80563412u,
|
||||
"DATM=0 should accept a clear CT32 alpha bit");
|
||||
t.Equals(ct32ZeroPass.depth, 0x22222222u,
|
||||
"a passing CT32 DATE should allow depth");
|
||||
|
||||
const GsPixelTestResult ct32OneFail =
|
||||
drawGsPixelForTests(GS_PSM_CT32, kTestBase, false,
|
||||
0x80030201u, kInitialDepth, 0x80u);
|
||||
t.Equals(ct32OneFail.framebuffer, 0x80030201u,
|
||||
"DATM=0 should reject a set CT32 alpha bit");
|
||||
t.Equals(ct32OneFail.depth, kInitialDepth,
|
||||
"a failing CT32 DATE should reject depth");
|
||||
|
||||
const GsPixelTestResult ct32OnePass =
|
||||
drawGsPixelForTests(GS_PSM_CT32, kTestBase | (1ull << 15), false,
|
||||
0x80030201u, kInitialDepth, 0x80u);
|
||||
t.Equals(ct32OnePass.framebuffer, 0x80563412u,
|
||||
"DATM=1 should accept a set CT32 alpha bit");
|
||||
|
||||
const GsPixelTestResult ct16ZeroPass =
|
||||
drawGsPixelForTests(GS_PSM_CT16, kTestBase, false,
|
||||
0x0001u, kInitialDepth, 0x80u);
|
||||
t.Equals(ct16ZeroPass.framebuffer, 0xA8C2u,
|
||||
"DATM=0 should accept a clear CT16 alpha bit");
|
||||
|
||||
const GsPixelTestResult ct16OneFail =
|
||||
drawGsPixelForTests(GS_PSM_CT16, kTestBase, false,
|
||||
0x8001u, kInitialDepth, 0x80u);
|
||||
t.Equals(ct16OneFail.framebuffer, 0x8001u,
|
||||
"DATM=0 should reject a set CT16 alpha bit");
|
||||
t.Equals(ct16OneFail.depth, kInitialDepth,
|
||||
"a failing CT16 DATE should reject depth");
|
||||
|
||||
const GsPixelTestResult ct16OnePass =
|
||||
drawGsPixelForTests(GS_PSM_CT16, kTestBase | (1ull << 15), false,
|
||||
0x8001u, kInitialDepth, 0x80u);
|
||||
t.Equals(ct16OnePass.framebuffer, 0xA8C2u,
|
||||
"DATM=1 should accept a set CT16 alpha bit");
|
||||
|
||||
const GsPixelTestResult ct24DatmZero =
|
||||
drawGsPixelForTests(GS_PSM_CT24, kTestBase, false,
|
||||
0x00030201u, kInitialDepth, 0x80u);
|
||||
const GsPixelTestResult ct24DatmOne =
|
||||
drawGsPixelForTests(GS_PSM_CT24, kTestBase | (1ull << 15), false,
|
||||
0x00030201u, kInitialDepth, 0x80u);
|
||||
t.Equals(ct24DatmZero.framebuffer, 0x00563412u,
|
||||
"CT24 DATE should pass for DATM=0");
|
||||
t.Equals(ct24DatmOne.framebuffer, 0x00563412u,
|
||||
"CT24 DATE should pass for DATM=1");
|
||||
t.Equals(ct24DatmOne.depth, 0x22222222u,
|
||||
"CT24 DATE should not block depth");
|
||||
});
|
||||
|
||||
tc.Run("GS triangle fan subpixel quad fills rows without interior holes", [](TestCase &t)
|
||||
|
||||
@@ -440,6 +440,69 @@ void register_ps2_iop_tests()
|
||||
"reset should restore per-instance service state");
|
||||
});
|
||||
|
||||
tc.Run("LotR sound update completes queued PlayStream slots", [](TestCase &t)
|
||||
{
|
||||
FakeIopHost host;
|
||||
ps2x::iop::IopSubsystem subsystem(host);
|
||||
std::string error;
|
||||
t.IsTrue(subsystem.configure({"SLUS_205.78", 0u, 0u}, &error),
|
||||
"LotR profile should configure");
|
||||
|
||||
constexpr uint32_t kSendAddress = 0x0800u;
|
||||
constexpr uint32_t kReceiveAddress = 0x1000u;
|
||||
constexpr uint16_t kStreamSlot = 7u;
|
||||
const std::array<uint16_t, 10> playStreamPacket = {
|
||||
1u, // command count
|
||||
1u, // PlayStream
|
||||
7u, // argument count
|
||||
0u,
|
||||
static_cast<uint16_t>(kStreamSlot << 8u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
t.IsTrue(host.writeGuest(kSendAddress,
|
||||
playStreamPacket.data(),
|
||||
sizeof(playStreamPacket)),
|
||||
"PlayStream command packet should fit in guest memory");
|
||||
|
||||
ps2x::iop::RpcRequest request{};
|
||||
request.sid = 0x00012345u;
|
||||
request.send = {kSendAddress, sizeof(playStreamPacket)};
|
||||
request.receive = {kReceiveAddress, 0x100u};
|
||||
|
||||
t.IsTrue(subsystem.handleRpc(request).handled,
|
||||
"LotR sound service should handle PlayStream");
|
||||
t.Equals(host.readWord(kReceiveAddress), 1u,
|
||||
"PlayStream response should expose one active record");
|
||||
const uint32_t packedStream = host.readWord(kReceiveAddress + 4u);
|
||||
t.Equals((packedStream >> 4u) & 0x3Fu,
|
||||
static_cast<uint32_t>(kStreamSlot),
|
||||
"active record should identify the queued EE stream slot");
|
||||
t.Equals(host.readWord(kReceiveAddress + 0x24u), 1u,
|
||||
"response counter should follow the active record");
|
||||
|
||||
const std::array<uint16_t, 5> statusPacket = {
|
||||
1u, // command count
|
||||
9u, // GetStatus
|
||||
2u, // argument count
|
||||
kStreamSlot,
|
||||
0u,
|
||||
};
|
||||
t.IsTrue(host.writeGuest(kSendAddress, statusPacket.data(), sizeof(statusPacket)),
|
||||
"GetStatus command packet should fit in guest memory");
|
||||
request.send.size = sizeof(statusPacket);
|
||||
|
||||
t.IsTrue(subsystem.handleRpc(request).handled,
|
||||
"LotR sound service should handle the following status update");
|
||||
t.Equals(host.readWord(kReceiveAddress), 0u,
|
||||
"the update after PlayStream should report no active records");
|
||||
t.Equals(host.readWord(kReceiveAddress + 4u), 2u,
|
||||
"empty response counter should return to the base offset");
|
||||
});
|
||||
|
||||
tc.Run("TSNDDRV uses profile checksum bindings without writing invalid ports", [](TestCase &t)
|
||||
{
|
||||
FakeIopHost host(0x02000000u);
|
||||
|
||||
@@ -155,6 +155,102 @@ static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path
|
||||
return writer.save(elfPath.string());
|
||||
}
|
||||
|
||||
static bool writeMinimalMipsElfWithInitializer(const std::filesystem::path &elfPath,
|
||||
const std::string &functionName,
|
||||
uint32_t initializerTarget)
|
||||
{
|
||||
ELFIO::elfio writer;
|
||||
writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB);
|
||||
writer.set_os_abi(ELFIO::ELFOSABI_NONE);
|
||||
writer.set_type(ELFIO::ET_EXEC);
|
||||
writer.set_machine(ELFIO::EM_MIPS);
|
||||
writer.set_entry(0x00100000u);
|
||||
|
||||
ELFIO::section *text = writer.sections.add(".text");
|
||||
text->set_type(ELFIO::SHT_PROGBITS);
|
||||
text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR);
|
||||
text->set_addr_align(4);
|
||||
text->set_address(0x00100000u);
|
||||
const std::array<uint32_t, 2> textWords = {
|
||||
0x03E00008u, // jr $ra
|
||||
0x00000000u, // nop
|
||||
};
|
||||
text->set_data(reinterpret_cast<const char *>(textWords.data()),
|
||||
static_cast<ELFIO::Elf_Word>(textWords.size() * sizeof(uint32_t)));
|
||||
|
||||
ELFIO::section *ctors = writer.sections.add(".ctors");
|
||||
ctors->set_type(ELFIO::SHT_PROGBITS);
|
||||
ctors->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_WRITE);
|
||||
ctors->set_addr_align(4);
|
||||
ctors->set_address(0x00200000u);
|
||||
ctors->set_data(reinterpret_cast<const char *>(&initializerTarget),
|
||||
static_cast<ELFIO::Elf_Word>(sizeof(initializerTarget)));
|
||||
|
||||
ELFIO::section *strtab = writer.sections.add(".strtab");
|
||||
strtab->set_type(ELFIO::SHT_STRTAB);
|
||||
strtab->set_addr_align(1);
|
||||
|
||||
ELFIO::section *symtab = writer.sections.add(".symtab");
|
||||
symtab->set_type(ELFIO::SHT_SYMTAB);
|
||||
symtab->set_info(1);
|
||||
symtab->set_link(strtab->get_index());
|
||||
symtab->set_addr_align(4);
|
||||
symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB));
|
||||
|
||||
ELFIO::symbol_section_accessor symbols(writer, symtab);
|
||||
ELFIO::string_section_accessor strings(strtab);
|
||||
symbols.add_symbol(strings, "", 0, 0,
|
||||
ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF);
|
||||
symbols.add_symbol(strings, functionName.c_str(), text->get_address(), text->get_size(),
|
||||
ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index());
|
||||
|
||||
ELFIO::segment *textSegment = writer.segments.add();
|
||||
textSegment->set_type(ELFIO::PT_LOAD);
|
||||
textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X);
|
||||
textSegment->set_align(0x1000);
|
||||
textSegment->add_section_index(text->get_index(), text->get_addr_align());
|
||||
|
||||
ELFIO::segment *dataSegment = writer.segments.add();
|
||||
dataSegment->set_type(ELFIO::PT_LOAD);
|
||||
dataSegment->set_flags(ELFIO::PF_R | ELFIO::PF_W);
|
||||
dataSegment->set_align(0x1000);
|
||||
dataSegment->add_section_index(ctors->get_index(), ctors->get_addr_align());
|
||||
|
||||
return writer.save(elfPath.string());
|
||||
}
|
||||
|
||||
static bool writeRecompilerTestConfig(const std::filesystem::path &configPath,
|
||||
const std::filesystem::path &elfPath,
|
||||
const std::filesystem::path &outputPath,
|
||||
const std::vector<std::string> &skip,
|
||||
const std::vector<std::string> &stubs = {})
|
||||
{
|
||||
std::ofstream config(configPath);
|
||||
if (!config)
|
||||
return false;
|
||||
|
||||
config << "[general]\n";
|
||||
config << "input = \"" << elfPath.generic_string() << "\"\n";
|
||||
config << "output = \"" << outputPath.generic_string() << "\"\n";
|
||||
config << "skip = [";
|
||||
for (size_t i = 0; i < skip.size(); ++i)
|
||||
{
|
||||
if (i != 0u)
|
||||
config << ", ";
|
||||
config << '"' << skip[i] << '"';
|
||||
}
|
||||
config << "]\n";
|
||||
config << "stubs = [";
|
||||
for (size_t i = 0; i < stubs.size(); ++i)
|
||||
{
|
||||
if (i != 0u)
|
||||
config << ", ";
|
||||
config << '"' << stubs[i] << '"';
|
||||
}
|
||||
config << "]\n";
|
||||
return static_cast<bool>(config);
|
||||
}
|
||||
|
||||
void register_ps2_recompiler_tests()
|
||||
{
|
||||
MiniTest::Case("PS2Recompiler", [](TestCase &tc)
|
||||
@@ -890,6 +986,98 @@ void register_ps2_recompiler_tests()
|
||||
"__sbprintf should be left for recompilation");
|
||||
});
|
||||
|
||||
tc.Run("initializer skips fall back to guest recompilation", [](TestCase &t) {
|
||||
const std::string uniqueSuffix =
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
|
||||
const std::filesystem::path tempRoot =
|
||||
std::filesystem::temp_directory_path() / ("ps2recomp-initializer-" + uniqueSuffix);
|
||||
const std::filesystem::path elfPath = tempRoot / "initializer.elf";
|
||||
const std::filesystem::path configPath = tempRoot / "initializer.toml";
|
||||
const std::filesystem::path outputPath = tempRoot / "output";
|
||||
std::filesystem::create_directories(tempRoot);
|
||||
|
||||
const bool elfWritten =
|
||||
writeMinimalMipsElfWithInitializer(elfPath, "__sinit_test.cpp", 0x00100000u);
|
||||
const bool configWritten =
|
||||
writeRecompilerTestConfig(configPath, elfPath, outputPath, {"__sinit_test.cpp"});
|
||||
t.IsTrue(elfWritten && configWritten,
|
||||
"initializer regression inputs should be generated");
|
||||
|
||||
if (elfWritten && configWritten)
|
||||
{
|
||||
PS2Recompiler recompiler(configPath.string());
|
||||
t.IsTrue(recompiler.initialize(),
|
||||
"initializer regression config should initialize");
|
||||
t.IsTrue(recompiler.recompile(),
|
||||
"a decodable skipped initializer should use guest fallback");
|
||||
const RecompilerReporter::Counters &counters = recompiler.reportCounters();
|
||||
t.Equals(counters.correctnessCriticalGuestFallbacks, static_cast<size_t>(1u),
|
||||
"the ignored initializer skip should be reported");
|
||||
t.Equals(counters.correctnessCriticalFailures, static_cast<size_t>(0u),
|
||||
"guest fallback should avoid a correctness-critical failure");
|
||||
t.Equals(counters.functionsSkipped, static_cast<size_t>(0u),
|
||||
"the initializer should not remain skipped");
|
||||
t.Equals(counters.functionsRecompiled, static_cast<size_t>(1u),
|
||||
"the original initializer body should be recompiled");
|
||||
}
|
||||
|
||||
std::error_code removeError;
|
||||
std::filesystem::remove_all(tempRoot, removeError);
|
||||
});
|
||||
|
||||
tc.Run("missing constructor-table targets fail recompilation", [](TestCase &t) {
|
||||
const std::string uniqueSuffix =
|
||||
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
|
||||
const std::filesystem::path tempRoot =
|
||||
std::filesystem::temp_directory_path() / ("ps2recomp-missing-initializer-" + uniqueSuffix);
|
||||
const std::filesystem::path elfPath = tempRoot / "initializer.elf";
|
||||
const std::filesystem::path configPath = tempRoot / "initializer.toml";
|
||||
const std::filesystem::path outputPath = tempRoot / "output";
|
||||
std::filesystem::create_directories(tempRoot);
|
||||
|
||||
const bool elfWritten =
|
||||
writeMinimalMipsElfWithInitializer(elfPath, "ordinary_entry", 0x00100040u);
|
||||
const bool configWritten =
|
||||
writeRecompilerTestConfig(configPath, elfPath, outputPath, {});
|
||||
t.IsTrue(elfWritten && configWritten,
|
||||
"missing-initializer regression inputs should be generated");
|
||||
|
||||
if (elfWritten && configWritten)
|
||||
{
|
||||
{
|
||||
PS2Recompiler recompiler(configPath.string());
|
||||
t.IsTrue(recompiler.initialize(),
|
||||
"missing-initializer regression config should initialize");
|
||||
t.IsFalse(recompiler.recompile(),
|
||||
"an unresolved .ctors target should be correctness-fatal");
|
||||
t.Equals(recompiler.reportCounters().correctnessCriticalFailures,
|
||||
static_cast<size_t>(1u),
|
||||
"the unresolved constructor target should appear in the report");
|
||||
}
|
||||
|
||||
const bool overrideWritten =
|
||||
writeRecompilerTestConfig(
|
||||
configPath, elfPath, outputPath, {},
|
||||
{"memclr@0x00100040"});
|
||||
t.IsTrue(overrideWritten,
|
||||
"manual initializer override config should be generated");
|
||||
if (overrideWritten)
|
||||
{
|
||||
PS2Recompiler overridden(configPath.string());
|
||||
t.IsTrue(overridden.initialize(),
|
||||
"manual initializer override should initialize");
|
||||
t.IsTrue(overridden.recompile(),
|
||||
"a resolved address-bound handler should satisfy the constructor target");
|
||||
t.Equals(overridden.reportCounters().functionsStubbed,
|
||||
static_cast<size_t>(1u),
|
||||
"the resolved manual initializer should be emitted as a stub binding");
|
||||
}
|
||||
}
|
||||
|
||||
std::error_code removeError;
|
||||
std::filesystem::remove_all(tempRoot, removeError);
|
||||
});
|
||||
|
||||
tc.Run("respect max length for .cpp filenames", [](TestCase& t) {
|
||||
|
||||
t.IsTrue(PS2Recompiler::ClampFilenameLength("ReallyLongFunctionNameReallyLongFunctionNameReallyLongFunctionName_0x12345678",".cpp",50).length() <= 50,"Function name must be max 50 characters");
|
||||
|
||||
@@ -81,12 +81,38 @@ namespace
|
||||
0x28u;
|
||||
}
|
||||
|
||||
uint32_t makeVuIaddiu(uint8_t it, uint8_t is, int16_t immediate)
|
||||
{
|
||||
return (0x08u << 25) |
|
||||
(static_cast<uint32_t>(it & 0xFu) << 16) |
|
||||
(static_cast<uint32_t>(is & 0xFu) << 11) |
|
||||
(static_cast<uint32_t>(immediate) & 0x7FFu);
|
||||
}
|
||||
|
||||
uint32_t makeVuLowerSpecial(uint8_t specialOp, uint8_t is,
|
||||
uint8_t it = 0u, uint8_t dest = 0u)
|
||||
{
|
||||
return (0x40u << 25) |
|
||||
(static_cast<uint32_t>(dest & 0xFu) << 21) |
|
||||
(static_cast<uint32_t>(it & 0x1Fu) << 16) |
|
||||
(static_cast<uint32_t>(is & 0x1Fu) << 11) |
|
||||
(static_cast<uint32_t>(specialOp & 0x7Cu) << 4) |
|
||||
static_cast<uint32_t>(specialOp & 0x3u) |
|
||||
0x3Cu;
|
||||
}
|
||||
|
||||
void writeVuInstructionPair(uint8_t *code, uint32_t pc, uint32_t lower, uint32_t upper)
|
||||
{
|
||||
std::memcpy(code + pc, &lower, sizeof(lower));
|
||||
std::memcpy(code + pc + sizeof(lower), &upper, sizeof(upper));
|
||||
}
|
||||
|
||||
uint64_t packVuInstructionPair(uint32_t lower, uint32_t upper)
|
||||
{
|
||||
return static_cast<uint64_t>(lower) |
|
||||
(static_cast<uint64_t>(upper) << 32);
|
||||
}
|
||||
|
||||
bool hasSignedRdWrite(const std::string &generated, uint8_t rd)
|
||||
{
|
||||
if (rd == 0u)
|
||||
@@ -151,10 +177,6 @@ namespace
|
||||
std::atomic<uint32_t> gMpegWaitStage{0u};
|
||||
std::atomic<uint32_t> gMpegNoDuplicateStage{0u};
|
||||
std::atomic<uint32_t> gMpegNoDuplicateProducerStage{0u};
|
||||
std::atomic<uint32_t> gMpegPacingStage{0u};
|
||||
std::atomic<uint32_t> gMpegPacingFirstProducerStage{0u};
|
||||
std::atomic<uint32_t> gMpegPacingSecondProducerStage{0u};
|
||||
std::atomic<uint64_t> gMpegPacingResumeTick{0u};
|
||||
|
||||
constexpr uint32_t kMpegWaitMainPc = 0x00125000u;
|
||||
constexpr uint32_t kMpegWaitResumePc = 0x00125010u;
|
||||
@@ -166,13 +188,6 @@ namespace
|
||||
constexpr uint32_t kMpegNoDuplicateProducerPc = 0x00125050u;
|
||||
constexpr uint32_t kMpegNoDuplicateHandle = 0x00124000u;
|
||||
constexpr uint32_t kMpegNoDuplicateImage = 0x00131000u;
|
||||
constexpr uint32_t kMpegPacingMainPc = 0x00125060u;
|
||||
constexpr uint32_t kMpegPacingAfterFirstPc = 0x00125070u;
|
||||
constexpr uint32_t kMpegPacingAfterSecondPc = 0x00125080u;
|
||||
constexpr uint32_t kMpegPacingFirstProducerPc = 0x00125090u;
|
||||
constexpr uint32_t kMpegPacingSecondProducerPc = 0x001250A0u;
|
||||
constexpr uint32_t kMpegPacingHandle = 0x00124800u;
|
||||
constexpr uint32_t kMpegPacingImage = 0x00132000u;
|
||||
constexpr uint32_t kIpuInitMainPc = 0x00125100u;
|
||||
constexpr uint32_t kIpuInitResumePc = 0x00125104u;
|
||||
constexpr uint32_t kIpuSetD4Pc = 0x00126428u;
|
||||
@@ -262,51 +277,6 @@ namespace
|
||||
ctx->pc = 0u;
|
||||
}
|
||||
|
||||
void testMpegPacingMain(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
setRegU32(*ctx, 4, kMpegPacingHandle);
|
||||
setRegU32(*ctx, 5, kMpegPacingImage);
|
||||
ctx->pc = kMpegPacingAfterFirstPc;
|
||||
ps2_stubs::sceMpegGetPicture(rdram, ctx, runtime);
|
||||
}
|
||||
|
||||
void testMpegPacingAfterFirst(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
gMpegPacingStage.store(1u, std::memory_order_release);
|
||||
setRegU32(*ctx, 4, kMpegPacingHandle);
|
||||
setRegU32(*ctx, 5, kMpegPacingImage);
|
||||
ctx->pc = kMpegPacingAfterSecondPc;
|
||||
ps2_stubs::sceMpegGetPicture(rdram, ctx, runtime);
|
||||
}
|
||||
|
||||
void testMpegPacingAfterSecond(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
gMpegPacingResumeTick.store(
|
||||
runtime->eeScheduler().currentVSyncTick(),
|
||||
std::memory_order_release);
|
||||
gMpegPacingStage.store(3u, std::memory_order_release);
|
||||
ctx->pc = 0u;
|
||||
runtime->requestStop();
|
||||
}
|
||||
|
||||
void testMpegPacingFirstProducer(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
gMpegPacingFirstProducerStage.store(
|
||||
gMpegPacingStage.load(std::memory_order_acquire),
|
||||
std::memory_order_release);
|
||||
runtime->eeScheduler().postEvent(EeEvent{EeEventType::VBlankStart, 0u, 0u});
|
||||
ctx->pc = 0u;
|
||||
}
|
||||
|
||||
void testMpegPacingSecondProducer(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
gMpegPacingSecondProducerStage.store(
|
||||
gMpegPacingStage.load(std::memory_order_acquire),
|
||||
std::memory_order_release);
|
||||
runtime->eeScheduler().postEvent(EeEvent{EeEventType::VBlankStart, 0u, 0u});
|
||||
ctx->pc = 0u;
|
||||
}
|
||||
|
||||
void testRecordMpegStreamCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
if (!rdram || !ctx)
|
||||
@@ -750,66 +720,6 @@ void register_ps2_runtime_expansion_tests()
|
||||
"resumed GetPicture should publish the configured height");
|
||||
});
|
||||
|
||||
tc.Run("CD EOF becomes visible only after the final guest MPEG demux", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
|
||||
ps2_stubs::resetMpegStubState();
|
||||
ps2_stubs::notifyMpegCdStreamStart();
|
||||
|
||||
constexpr uint32_t kMpegAddr = 0x00126000u;
|
||||
constexpr uint32_t kPacketAddr = 0x00127000u;
|
||||
const std::vector<uint8_t> finalPacket = {
|
||||
0x00u, 0x00u, 0x01u, 0xE0u,
|
||||
0x00u, 0x04u,
|
||||
0x80u, 0x00u, 0x00u,
|
||||
0x00u};
|
||||
std::memcpy(rdram.data() + kPacketAddr, finalPacket.data(), finalPacket.size());
|
||||
|
||||
ps2_stubs::notifyMpegCdStreamDataProduced(
|
||||
static_cast<uint32_t>(finalPacket.size()), true);
|
||||
|
||||
R5900Context beforeDemuxCtx{};
|
||||
setRegU32(beforeDemuxCtx, 4, kMpegAddr);
|
||||
ps2_stubs::sceMpegIsEnd(rdram.data(), &beforeDemuxCtx, &runtime);
|
||||
t.Equals(getRegS32(beforeDemuxCtx, 2), 0,
|
||||
"physical CD EOF must not end MPEG before the final guest buffer is demuxed");
|
||||
|
||||
constexpr uint32_t kFirstPartSize = 5u;
|
||||
R5900Context firstDemuxCtx{};
|
||||
setRegU32(firstDemuxCtx, 4, kMpegAddr);
|
||||
setRegU32(firstDemuxCtx, 5, kPacketAddr);
|
||||
setRegU32(firstDemuxCtx, 6, kFirstPartSize);
|
||||
setRegU32(firstDemuxCtx, 7, kPacketAddr);
|
||||
setRegU32(firstDemuxCtx, 8, static_cast<uint32_t>(finalPacket.size()));
|
||||
ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &firstDemuxCtx, &runtime);
|
||||
t.Equals(getRegS32(firstDemuxCtx, 2), static_cast<int32_t>(kFirstPartSize),
|
||||
"the partial final MPEG buffer should be consumed");
|
||||
|
||||
R5900Context midwayCtx{};
|
||||
setRegU32(midwayCtx, 4, kMpegAddr);
|
||||
ps2_stubs::sceMpegIsEnd(rdram.data(), &midwayCtx, &runtime);
|
||||
t.Equals(getRegS32(midwayCtx, 2), 0,
|
||||
"a partial final MPEG demux must keep physical CD EOF pending");
|
||||
|
||||
const uint32_t remainingSize = static_cast<uint32_t>(finalPacket.size()) - kFirstPartSize;
|
||||
R5900Context finalDemuxCtx{};
|
||||
setRegU32(finalDemuxCtx, 4, kMpegAddr);
|
||||
setRegU32(finalDemuxCtx, 5, kPacketAddr + kFirstPartSize);
|
||||
setRegU32(finalDemuxCtx, 6, remainingSize);
|
||||
setRegU32(finalDemuxCtx, 7, kPacketAddr);
|
||||
setRegU32(finalDemuxCtx, 8, static_cast<uint32_t>(finalPacket.size()));
|
||||
ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &finalDemuxCtx, &runtime);
|
||||
t.Equals(getRegS32(finalDemuxCtx, 2), static_cast<int32_t>(remainingSize),
|
||||
"the final guest MPEG bytes should be consumed before EOF is committed");
|
||||
|
||||
R5900Context afterDemuxCtx{};
|
||||
setRegU32(afterDemuxCtx, 4, kMpegAddr);
|
||||
ps2_stubs::sceMpegIsEnd(rdram.data(), &afterDemuxCtx, &runtime);
|
||||
t.Equals(getRegS32(afterDemuxCtx, 2), 1,
|
||||
"MPEG should end after the pending final guest buffer is demuxed");
|
||||
});
|
||||
|
||||
tc.Run("sceMpegGetPicture waits for new decoder output instead of duplicating the last frame", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
@@ -842,76 +752,6 @@ void register_ps2_runtime_expansion_tests()
|
||||
"only the injected decoder frame should be counted as served");
|
||||
});
|
||||
|
||||
tc.Run("sceMpegGetPicture resumes its HLE operation at the MPEG frame cadence", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
|
||||
ps2_stubs::resetMpegStubState();
|
||||
ps2_stubs::notifyMpegCdStreamStart();
|
||||
|
||||
constexpr uint32_t kSequencePacketAddr = 0x00127000u;
|
||||
const std::vector<uint8_t> sequencePacket = {
|
||||
0x00u, 0x00u, 0x01u, 0xE0u,
|
||||
0x00u, 0x0Bu,
|
||||
0x80u, 0x00u, 0x00u,
|
||||
0x00u, 0x00u, 0x01u, 0xB3u,
|
||||
0x14u, 0x01u, 0x60u, 0x14u};
|
||||
std::memcpy(
|
||||
rdram.data() + kSequencePacketAddr,
|
||||
sequencePacket.data(),
|
||||
sequencePacket.size());
|
||||
|
||||
R5900Context demuxContext{};
|
||||
runtime.eeScheduler().reset(rdram.data(), demuxContext);
|
||||
setRegU32(demuxContext, 4, kMpegPacingHandle);
|
||||
setRegU32(demuxContext, 5, kSequencePacketAddr);
|
||||
setRegU32(demuxContext, 6, static_cast<uint32_t>(sequencePacket.size()));
|
||||
setRegU32(demuxContext, 7, kSequencePacketAddr);
|
||||
setRegU32(demuxContext, 8, static_cast<uint32_t>(sequencePacket.size()));
|
||||
ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &demuxContext, &runtime);
|
||||
t.Equals(
|
||||
getRegS32(demuxContext, 2),
|
||||
static_cast<int32_t>(sequencePacket.size()),
|
||||
"the 29.97 fps MPEG sequence header should be consumed");
|
||||
|
||||
ps2_stubs::enqueueMpegDecodedFrameForTesting(kMpegPacingHandle);
|
||||
ps2_stubs::enqueueMpegDecodedFrameForTesting(kMpegPacingHandle);
|
||||
runtime.registerFunction(kMpegPacingMainPc, testMpegPacingMain);
|
||||
runtime.registerFunction(kMpegPacingAfterFirstPc, testMpegPacingAfterFirst);
|
||||
runtime.registerFunction(kMpegPacingAfterSecondPc, testMpegPacingAfterSecond);
|
||||
runtime.registerFunction(kMpegPacingFirstProducerPc, testMpegPacingFirstProducer);
|
||||
runtime.registerFunction(kMpegPacingSecondProducerPc, testMpegPacingSecondProducer);
|
||||
gMpegPacingStage.store(0u, std::memory_order_release);
|
||||
gMpegPacingFirstProducerStage.store(0u, std::memory_order_release);
|
||||
gMpegPacingSecondProducerStage.store(0u, std::memory_order_release);
|
||||
gMpegPacingResumeTick.store(0u, std::memory_order_release);
|
||||
|
||||
R5900Context mainContext{};
|
||||
mainContext.pc = kMpegPacingMainPc;
|
||||
EeScheduler &ee = runtime.eeScheduler();
|
||||
ee.reset(rdram.data(), mainContext);
|
||||
const int firstProducerId = ee.createThread(EeThreadCreateParams{
|
||||
0u, kMpegPacingFirstProducerPc, 0u, 0u, 0u, 10, 0u});
|
||||
const int secondProducerId = ee.createThread(EeThreadCreateParams{
|
||||
0u, kMpegPacingSecondProducerPc, 0u, 0u, 0u, 11, 0u});
|
||||
t.IsTrue(firstProducerId > 1 && secondProducerId > firstProducerId,
|
||||
"two VSync producer guest threads should be created");
|
||||
t.Equals(ee.startThread(firstProducerId, 0u, mainContext, false), 0,
|
||||
"the first VSync producer should become ready");
|
||||
t.Equals(ee.startThread(secondProducerId, 0u, mainContext, false), 0,
|
||||
"the second VSync producer should become ready");
|
||||
ee.run();
|
||||
|
||||
t.Equals(gMpegPacingFirstProducerStage.load(std::memory_order_acquire), 1u,
|
||||
"the second GetPicture should wait before the first VSync");
|
||||
t.Equals(gMpegPacingSecondProducerStage.load(std::memory_order_acquire), 1u,
|
||||
"29.97 fps must still be waiting after one 59.94 Hz VSync");
|
||||
t.Equals(gMpegPacingResumeTick.load(std::memory_order_acquire), 2ull,
|
||||
"the pending GetPicture should complete after two VSync ticks");
|
||||
t.Equals(gMpegPacingStage.load(std::memory_order_acquire), 3u,
|
||||
"VSync completion must re-enter GetPicture before its guest continuation");
|
||||
});
|
||||
|
||||
tc.Run("sceSdRemote isolates voice transfers from block streaming state", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
|
||||
@@ -1025,7 +865,7 @@ void register_ps2_runtime_expansion_tests()
|
||||
t.Equals(remote(0x80F0u, 1u, 0u), 1u,
|
||||
"sceSdRemoteInit should restore idle voice status to complete");
|
||||
});
|
||||
|
||||
|
||||
tc.Run("IPU init skips missing optional helper instead of dispatching the default trap", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
@@ -1252,7 +1092,7 @@ void register_ps2_runtime_expansion_tests()
|
||||
writeVuInstructionPair(code, 8u, 0u, makeVuAdd(0xFu, 2u, 1u, 1u));
|
||||
writeVuInstructionPair(code, 16u, makeVuSq(0xFu, 2u, 0u, 1), kVuEndNop);
|
||||
|
||||
R5900Context ctx;
|
||||
R5900Context ctx{};
|
||||
runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u);
|
||||
|
||||
float output[4]{};
|
||||
@@ -1268,6 +1108,102 @@ void register_ps2_runtime_expansion_tests()
|
||||
t.Equals(static_cast<uint32_t>(ctx.vi[0]), 0u, "VU0 VI0 should remain zero");
|
||||
});
|
||||
|
||||
tc.Run("VU0 microprogram preserves the architectural RNG state", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed");
|
||||
t.IsTrue(runtime.syncCoreSubsystems(), "runtime core subsystems should bind");
|
||||
|
||||
uint8_t *const code = runtime.memory().getVU0Code();
|
||||
std::memset(code, 0, PS2_VU0_CODE_SIZE);
|
||||
constexpr uint32_t kVuUpperNop = 0x000002FFu;
|
||||
constexpr uint32_t kVuUpperEndNop = 0x400002FFu;
|
||||
writeVuInstructionPair(
|
||||
code, 0u,
|
||||
makeVuLowerSpecial(0x40u, 0u, 1u, 0x8u),
|
||||
kVuUpperEndNop); // RNEXT.x vf1
|
||||
writeVuInstructionPair(code, 8u, 0u, kVuUpperNop);
|
||||
|
||||
constexpr uint32_t seed = 0x3FC00000u;
|
||||
const uint32_t x = (seed >> 4) & 1u;
|
||||
const uint32_t y = (seed >> 22) & 1u;
|
||||
const uint32_t expected =
|
||||
(((seed << 1) ^ x ^ y) & 0x007FFFFFu) | 0x3F800000u;
|
||||
R5900Context ctx{};
|
||||
ctx.vu0_r = _mm_castsi128_ps(
|
||||
_mm_set1_epi32(static_cast<int32_t>(seed)));
|
||||
runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u);
|
||||
|
||||
alignas(16) uint32_t rWords[4]{};
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i *>(rWords),
|
||||
_mm_castps_si128(ctx.vu0_r));
|
||||
t.Equals(rWords[0], expected, "VU0 micro RNG should advance the imported R seed");
|
||||
t.Equals(rWords[1], expected, "VU0 R should remain replicated for macro-mode access");
|
||||
alignas(16) uint32_t vf1Words[4]{};
|
||||
_mm_storeu_si128(reinterpret_cast<__m128i *>(vf1Words),
|
||||
_mm_castps_si128(ctx.vu0_vf[1]));
|
||||
t.Equals(vf1Words[0], expected, "RNEXT should expose the same R value through VF1.x");
|
||||
});
|
||||
|
||||
tc.Run("VU0 direct MicroMem writes invalidate the fixed decode cache", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed");
|
||||
t.IsTrue(runtime.syncCoreSubsystems(), "runtime core subsystems should bind");
|
||||
|
||||
constexpr uint32_t kVuUpperNop = 0x000002FFu;
|
||||
constexpr uint32_t kVuUpperEndNop = 0x400002FFu;
|
||||
runtime.memory().write64(
|
||||
PS2_VU0_CODE_BASE,
|
||||
packVuInstructionPair(makeVuIaddiu(1u, 0u, 1), kVuUpperEndNop));
|
||||
runtime.memory().write64(
|
||||
PS2_VU0_CODE_BASE + 8u,
|
||||
packVuInstructionPair(0u, kVuUpperNop));
|
||||
|
||||
R5900Context first{};
|
||||
runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &first, 0u);
|
||||
t.Equals(static_cast<uint32_t>(first.vi[1]), 1u,
|
||||
"first cached VU0 microprogram should execute");
|
||||
|
||||
runtime.memory().write64(
|
||||
PS2_VU0_CODE_BASE,
|
||||
packVuInstructionPair(makeVuIaddiu(1u, 0u, 2), kVuUpperEndNop));
|
||||
R5900Context second{};
|
||||
runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &second, 0u);
|
||||
t.Equals(static_cast<uint32_t>(second.vi[1]), 2u,
|
||||
"VU0 cache should rebuild after a direct MicroMem write");
|
||||
});
|
||||
|
||||
tc.Run("VU0 FBRST TE gates a T-bit microprogram stop", [](TestCase &t)
|
||||
{
|
||||
PS2Runtime runtime;
|
||||
t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed");
|
||||
t.IsTrue(runtime.syncCoreSubsystems(), "runtime core subsystems should bind");
|
||||
|
||||
uint8_t *const code = runtime.memory().getVU0Code();
|
||||
std::memset(code, 0, PS2_VU0_CODE_SIZE);
|
||||
constexpr uint32_t kVuUpperNop = 0x000002FFu;
|
||||
writeVuInstructionPair(
|
||||
code, 0u, makeVuIaddiu(1u, 0u, 7),
|
||||
kVuUpperNop | 0x08000000u);
|
||||
writeVuInstructionPair(
|
||||
code, 8u, makeVuIaddiu(2u, 0u, 9),
|
||||
kVuUpperNop);
|
||||
|
||||
R5900Context ctx{};
|
||||
ctx.vu0_fbrst = 1u << 3; // TE0
|
||||
runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u);
|
||||
|
||||
t.Equals(static_cast<uint32_t>(ctx.vi[1]), 7u,
|
||||
"the T-marked instruction should execute");
|
||||
t.Equals(static_cast<uint32_t>(ctx.vi[2]), 0u,
|
||||
"TE0 should stop VU0 before the following instruction");
|
||||
t.IsTrue((ctx.vu0_vpu_stat & (1u << 2)) != 0u,
|
||||
"VPU-STAT should report a VU0 T-bit stop");
|
||||
t.Equals(ctx.vu0_tpc, 8u,
|
||||
"TPC should point at the first instruction not executed");
|
||||
});
|
||||
|
||||
tc.Run("GS sprite draw applies XYOFFSET and fully-outside scissor should not render", [](TestCase &t)
|
||||
{
|
||||
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
|
||||
|
||||
@@ -171,7 +171,7 @@ namespace
|
||||
uint32_t g_schedulerRpcReceive = 0u;
|
||||
uint32_t g_schedulerRpcResult = 0u;
|
||||
|
||||
void lotrSoundEndCallbackShouldNotRun(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
void lotrSoundEndCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||
{
|
||||
(void)rdram;
|
||||
(void)runtime;
|
||||
@@ -667,7 +667,7 @@ void register_ps2_sif_rpc_tests()
|
||||
PS2Runtime::setIoPaths(oldPaths);
|
||||
});
|
||||
|
||||
tc.Run("LotR sound RPC completes HLE callback without invoking guest loop", [](TestCase &t)
|
||||
tc.Run("LotR sound RPC invokes guest callback to consume HLE response", [](TestCase &t)
|
||||
{
|
||||
TestEnv env;
|
||||
configureProfile(env, "SLUS_205.78");
|
||||
@@ -678,7 +678,7 @@ void register_ps2_sif_rpc_tests()
|
||||
constexpr uint32_t kRecvAddr = 0x0003C000u;
|
||||
constexpr uint32_t kEndFunc = 0x001FFD70u;
|
||||
|
||||
env.runtime.registerFunction(kEndFunc, lotrSoundEndCallbackShouldNotRun);
|
||||
env.runtime.registerFunction(kEndFunc, lotrSoundEndCallback);
|
||||
g_lotrSoundCallbackHits = 0u;
|
||||
|
||||
SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime);
|
||||
@@ -711,8 +711,8 @@ void register_ps2_sif_rpc_tests()
|
||||
SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime);
|
||||
|
||||
t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should succeed for LotR sound RPC");
|
||||
t.Equals(g_lotrSoundCallbackHits.load(), 0u,
|
||||
"HLE-completed LotR sound callback should not invoke the guest callback");
|
||||
t.Equals(g_lotrSoundCallbackHits.load(), 1u,
|
||||
"LotR SOUND_JP callback should consume the HLE response");
|
||||
t.Equals(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 0u), 0u,
|
||||
"LotR sound response should report no active stream records");
|
||||
t.IsTrue(readGuestStruct<uint32_t>(env.rdram.data(), kRecvAddr + 4u) != 0u,
|
||||
|
||||
+1168
-11
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user