mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
fix: fix texture caching
feat: wip multi version on dbcman
This commit is contained in:
@@ -68,6 +68,16 @@ if(PS2X_IOP_BUILD_TESTS)
|
||||
target_link_libraries(ps2_iop_import_tests PRIVATE ps2_iop)
|
||||
target_include_directories(ps2_iop_import_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_test(NAME ps2_iop_import_tests COMMAND ps2_iop_import_tests)
|
||||
|
||||
add_executable(ps2_iop_compatibility_tests tests/iop_compatibility_tests.cpp)
|
||||
target_link_libraries(ps2_iop_compatibility_tests PRIVATE ps2_iop)
|
||||
add_test(NAME ps2_iop_compatibility_tests COMMAND ps2_iop_compatibility_tests)
|
||||
|
||||
add_executable(ps2_iop_import_version_tests tests/iop_import_version_tests.cpp)
|
||||
target_link_libraries(ps2_iop_import_version_tests PRIVATE ps2_iop)
|
||||
target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_test(NAME ps2_iop_import_version_tests COMMAND ps2_iop_import_version_tests)
|
||||
|
||||
endif()
|
||||
|
||||
install(TARGETS ps2_iop
|
||||
|
||||
@@ -92,6 +92,7 @@ namespace ps2x::iop::detail
|
||||
return IopImportCall{
|
||||
trimLibraryName(name),
|
||||
static_cast<uint16_t>(delay & 0xFFFFu),
|
||||
m_memory.read16(table + 8u),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -131,20 +132,34 @@ namespace ps2x::iop::detail
|
||||
return m_libraries.erase(IopMemory::physicalAddress(address)) != 0u;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::findTable(std::string_view library) const
|
||||
const IopImportRegistry::ExportLibrary *IopImportRegistry::findLibrary(std::string_view name, std::optional<uint16_t> version) const
|
||||
{
|
||||
const auto found = std::find_if(m_libraries.begin(), m_libraries.end(), [&](const auto &entry)
|
||||
{ return equalsIgnoreCase(entry.second.name, library); });
|
||||
return found != m_libraries.end() ? found->second.tableAddress : 0u;
|
||||
const ExportLibrary *selected = nullptr;
|
||||
for (const auto &[address, library] : m_libraries)
|
||||
{
|
||||
(void)address;
|
||||
if (!equalsIgnoreCase(library.name, name) ||
|
||||
(version && (library.version >> 8u) != (*version >> 8u)))
|
||||
continue;
|
||||
// LOADCORE links by major version; a newer minor supersedes older exports.
|
||||
if (!selected || library.version > selected->version)
|
||||
selected = &library;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::resolve(std::string_view library, uint16_t ordinal) const
|
||||
uint32_t IopImportRegistry::findTable(std::string_view library, std::optional<uint16_t> version) const
|
||||
{
|
||||
const auto found = std::find_if(m_libraries.begin(), m_libraries.end(), [&](const auto &entry)
|
||||
{ return equalsIgnoreCase(entry.second.name, library); });
|
||||
if (found == m_libraries.end() || ordinal >= found->second.functions.size())
|
||||
const ExportLibrary *found = findLibrary(library, version);
|
||||
return found ? found->tableAddress : 0u;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::resolve(std::string_view library, uint16_t ordinal, std::optional<uint16_t> version) const
|
||||
{
|
||||
const ExportLibrary *found = findLibrary(library, version);
|
||||
if (!found || ordinal >= found->functions.size())
|
||||
return 0u;
|
||||
return found->second.functions[ordinal];
|
||||
return found->functions[ordinal];
|
||||
}
|
||||
|
||||
int32_t IopImportRegistry::setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode)
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
std::string library;
|
||||
uint16_t ordinal = 0;
|
||||
uint16_t version = 0;
|
||||
};
|
||||
|
||||
class IopImportRegistry
|
||||
@@ -27,8 +28,8 @@ namespace ps2x::iop::detail
|
||||
[[nodiscard]] std::optional<IopImportCall> decode(uint32_t pc) const;
|
||||
[[nodiscard]] bool registerExportTable(uint32_t address);
|
||||
[[nodiscard]] bool releaseExportTable(uint32_t address);
|
||||
[[nodiscard]] uint32_t findTable(std::string_view library) const;
|
||||
[[nodiscard]] uint32_t resolve(std::string_view library, uint16_t ordinal) const;
|
||||
[[nodiscard]] uint32_t findTable(std::string_view library, std::optional<uint16_t> version = std::nullopt) const;
|
||||
[[nodiscard]] uint32_t resolve(std::string_view library, uint16_t ordinal, std::optional<uint16_t> version = std::nullopt) const;
|
||||
[[nodiscard]] int32_t setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode);
|
||||
void eraseRange(uint32_t base, uint32_t size);
|
||||
|
||||
@@ -41,6 +42,8 @@ namespace ps2x::iop::detail
|
||||
std::vector<uint32_t> functions;
|
||||
};
|
||||
|
||||
[[nodiscard]] const ExportLibrary *findLibrary(std::string_view name, std::optional<uint16_t> version) const;
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<uint32_t, ExportLibrary> m_libraries;
|
||||
};
|
||||
|
||||
@@ -43,9 +43,18 @@ namespace ps2x::iop::detail
|
||||
case 7:
|
||||
setV0(m_imports.releaseExportTable(a0) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 11:
|
||||
setV0(m_imports.findTable(m_memory.readString(a0 + 12u, 8u)));
|
||||
case 11: // QueryLibraryEntryTable returns the function array, not the export header.
|
||||
{
|
||||
const uint32_t address = IopMemory::physicalAddress(a0);
|
||||
if (a0 == 0u || address > IopMemory::RamSize - 20u)
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
const uint32_t table = m_imports.findTable(m_memory.readString(address + 12u, 8u), m_memory.read16(address + 8u));
|
||||
setV0(table != 0u ? table + 20u : 0u);
|
||||
return true;
|
||||
}
|
||||
case 27: // SetRebootTimeLibraryHandlingMode
|
||||
setV0(static_cast<uint32_t>(m_imports.setRebootTimeLibraryHandlingMode(a0, cpu.gpr[5])));
|
||||
return true;
|
||||
|
||||
@@ -342,7 +342,7 @@ namespace ps2x::iop::detail
|
||||
if (iequals(call.library, "heaplib") && heaplib.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
|
||||
const uint32_t target = imports.resolve(call.library, call.ordinal);
|
||||
const uint32_t target = imports.resolve(call.library, call.ordinal, call.version);
|
||||
if (target != 0u)
|
||||
{
|
||||
cpu.pc = target;
|
||||
@@ -351,7 +351,8 @@ namespace ps2x::iop::detail
|
||||
}
|
||||
|
||||
std::ostringstream out;
|
||||
out << "[IOP] unhandled import " << call.library << ':' << call.ordinal << " pc=0x" << std::hex << cpu.pc;
|
||||
out << "[IOP] unhandled import " << call.library << ':' << call.ordinal
|
||||
<< " version=0x" << std::hex << call.version << " pc=0x" << cpu.pc;
|
||||
log(LogLevel::Warning, out.str());
|
||||
setV0(0);
|
||||
return ImportDisposition::Missing;
|
||||
|
||||
@@ -30,9 +30,12 @@ namespace ps2x::iop::detail
|
||||
"vblank",
|
||||
"secrman",
|
||||
"sio2man",
|
||||
"xsio2man",
|
||||
"sio2d",
|
||||
"padman",
|
||||
"xpadman",
|
||||
"mcman",
|
||||
"xmcman",
|
||||
"mcserv",
|
||||
"libsd",
|
||||
"cdvdman",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop
|
||||
@@ -137,6 +138,19 @@ namespace ps2x::iop
|
||||
routesValid = addLayer(coreServices, false) && addLayer(profileServices, true);
|
||||
}
|
||||
|
||||
void recordLoadOutcome(std::string_view path, bool hle)
|
||||
{
|
||||
constexpr size_t maxOutcomes = 32u;
|
||||
if (loadOutcomes.size() >= maxOutcomes || !loggedLoadPaths.emplace(path).second)
|
||||
return;
|
||||
std::string message = hle ? "[IOP:HLE] fallback module='" : "[IOP:load-failed] module='";
|
||||
message.append(path);
|
||||
message += hle ? "' physical IRX unavailable; using registered HLE provider"
|
||||
: "' no HLE provider accepted the module; physical IRX was not loaded";
|
||||
loadOutcomes.push_back(message);
|
||||
host.log(hle ? LogLevel::Info : LogLevel::Warning, message);
|
||||
}
|
||||
|
||||
IopHost &host;
|
||||
detail::PluginCatalog pluginCatalog;
|
||||
detail::ServiceList coreServices;
|
||||
@@ -145,6 +159,8 @@ namespace ps2x::iop
|
||||
std::unordered_map<uint32_t, detail::IopService *> routes;
|
||||
std::vector<std::filesystem::path> pluginSearchPaths;
|
||||
std::vector<std::string> diagnostics;
|
||||
std::vector<std::string> loadOutcomes;
|
||||
std::unordered_set<std::string> loggedLoadPaths;
|
||||
std::string activeProfile;
|
||||
std::string activeProvider;
|
||||
std::string lastError;
|
||||
@@ -174,6 +190,8 @@ namespace ps2x::iop
|
||||
|
||||
bool IopSubsystem::configure(const GameIdentity &identity, std::string *error)
|
||||
{
|
||||
if (error)
|
||||
error->clear();
|
||||
m_impl->profileServices.clear();
|
||||
m_impl->activeProfile.clear();
|
||||
m_impl->activeProvider.clear();
|
||||
@@ -273,6 +291,8 @@ namespace ps2x::iop
|
||||
void IopSubsystem::reset()
|
||||
{
|
||||
m_impl->moduleManager.reset();
|
||||
m_impl->loadOutcomes.clear();
|
||||
m_impl->loggedLoadPaths.clear();
|
||||
for (auto &service : m_impl->coreServices)
|
||||
{
|
||||
if (service)
|
||||
@@ -311,7 +331,15 @@ namespace ps2x::iop
|
||||
|
||||
ModuleLoadResult hle = m_impl->moduleManager.loadHle(path);
|
||||
if (hle.moduleId > 0)
|
||||
{
|
||||
m_impl->rebuildRoutes();
|
||||
if (parsed.device != Ps2PathDevice::Rom0)
|
||||
m_impl->recordLoadOutcome(path, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_impl->recordLoadOutcome(path, false);
|
||||
}
|
||||
return hle;
|
||||
}
|
||||
|
||||
@@ -463,6 +491,7 @@ namespace ps2x::iop
|
||||
snapshot.activeProfile = m_impl->activeProfile;
|
||||
snapshot.activeProvider = m_impl->activeProvider;
|
||||
snapshot.diagnostics = m_impl->diagnostics;
|
||||
snapshot.diagnostics.insert(snapshot.diagnostics.end(), m_impl->loadOutcomes.begin(), m_impl->loadOutcomes.end());
|
||||
if (!m_impl->lastError.empty())
|
||||
{
|
||||
snapshot.diagnostics.push_back(m_impl->lastError);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "module_factories.h"
|
||||
#include "rpc_reply.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -12,9 +13,11 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
constexpr uint32_t kDbcManSid = 0x80001300u;
|
||||
constexpr uint32_t kRpcCheckVersion = 0x80001363u;
|
||||
constexpr uint32_t kDbcManVersion = 0x0320u;
|
||||
constexpr uint32_t kMaxUnknownRpcLogs = 32u;
|
||||
|
||||
constexpr std::array<uint16_t, 2> kSupportedVersions{0x0310u, 0x0320u};
|
||||
constexpr uint16_t kReportedVersion = kSupportedVersions.front();
|
||||
|
||||
class DbcmanService final : public IopService
|
||||
{
|
||||
public:
|
||||
@@ -42,6 +45,8 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_unknownRpcLogCount = 0u;
|
||||
m_versionQueryCount = 0u;
|
||||
m_failedVersionReplies = 0u;
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
@@ -61,12 +66,21 @@ namespace ps2x::iop::detail
|
||||
|
||||
if (request.function == kRpcCheckVersion)
|
||||
{
|
||||
const uint32_t wordCount = request.receive.size / sizeof(uint32_t);
|
||||
const uint32_t count = wordCount < 4u ? wordCount : 4u;
|
||||
for (uint32_t index = 0u; index < count; ++index)
|
||||
const uint32_t version = kReportedVersion;
|
||||
const std::array<uint32_t, 4> reply{version, version, version, version};
|
||||
const bool written = writeRpcWords(m_host, request.receive, reply);
|
||||
bool firstQuery = false;
|
||||
{
|
||||
const uint32_t address = request.receive.address + index * sizeof(uint32_t);
|
||||
(void)m_host.writeGuest(address, &kDbcManVersion, sizeof(kDbcManVersion));
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
firstQuery = m_versionQueryCount++ == 0u;
|
||||
if (!written)
|
||||
++m_failedVersionReplies;
|
||||
}
|
||||
if (firstQuery)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "[DBCMAN:HLE] check-version reply=0x" << std::hex << version;
|
||||
m_host.log(LogLevel::Info, message.str());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -99,6 +113,9 @@ namespace ps2x::iop::detail
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"reported_version", kReportedVersion, true});
|
||||
metrics.push_back({"version_queries", m_versionQueryCount, false});
|
||||
metrics.push_back({"failed_version_replies", m_failedVersionReplies, false});
|
||||
metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false});
|
||||
}
|
||||
|
||||
@@ -109,6 +126,8 @@ namespace ps2x::iop::detail
|
||||
IopHost &m_host;
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_unknownRpcLogCount = 0u;
|
||||
uint64_t m_versionQueryCount = 0u;
|
||||
uint64_t m_failedVersionReplies = 0u;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "../iop_service.h"
|
||||
#include "../rpc_reply.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -189,8 +190,8 @@ namespace ps2x::iop::detail
|
||||
const Operation operation = decodeOperation(request.function, flavor);
|
||||
if (operation == Operation::Init)
|
||||
{
|
||||
(void)call(MemoryCardOperation::Init);
|
||||
writeInitResult(request.receive);
|
||||
const int32_t result = call(MemoryCardOperation::Init);
|
||||
writeInitResult(request.receive, flavor, result);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -206,7 +207,7 @@ namespace ps2x::iop::detail
|
||||
{
|
||||
NameParameter parameter{};
|
||||
if (request.send.address != 0u &&
|
||||
request.send.size >= offsetof(NameParameter, name) &&
|
||||
request.send.size >= sizeof(parameter) &&
|
||||
m_host.readGuest(request.send.address, ¶meter, sizeof(parameter)))
|
||||
{
|
||||
result = handleNameOperation(operation, request.send.address, parameter);
|
||||
@@ -222,22 +223,15 @@ namespace ps2x::iop::detail
|
||||
if (operation == Operation::Write && parameter.origin > 0 &&
|
||||
parameter.origin <= static_cast<int32_t>(sizeof(parameter.data)))
|
||||
{
|
||||
const uint32_t inlineAddress =
|
||||
request.send.address + static_cast<uint32_t>(offsetof(DescriptorParameter, data));
|
||||
const int32_t prefix = call(MemoryCardOperation::Write,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
inlineAddress,
|
||||
static_cast<uint32_t>(parameter.origin));
|
||||
const uint32_t inlineAddress = request.send.address + static_cast<uint32_t>(offsetof(DescriptorParameter, data));
|
||||
const int32_t prefix = call(MemoryCardOperation::Write, static_cast<uint32_t>(parameter.fd), inlineAddress, static_cast<uint32_t>(parameter.origin));
|
||||
if (prefix < 0)
|
||||
{
|
||||
result = prefix;
|
||||
}
|
||||
else
|
||||
{
|
||||
const int32_t body = call(MemoryCardOperation::Write,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
parameter.buffer,
|
||||
static_cast<uint32_t>(std::max(parameter.size, 0)));
|
||||
const int32_t body = call(MemoryCardOperation::Write, static_cast<uint32_t>(parameter.fd), parameter.buffer, static_cast<uint32_t>(std::max(parameter.size, 0)));
|
||||
result = body < 0 ? body : prefix + body;
|
||||
}
|
||||
}
|
||||
@@ -271,36 +265,18 @@ namespace ps2x::iop::detail
|
||||
|
||||
void writeResult(GuestBuffer receive, int32_t result)
|
||||
{
|
||||
if (receive.address == 0u || receive.size < sizeof(result))
|
||||
{
|
||||
return;
|
||||
}
|
||||
(void)m_host.writeGuest(receive.address, &result, sizeof(result));
|
||||
if (receive.size > sizeof(result))
|
||||
{
|
||||
(void)m_host.zeroGuest(receive.address + sizeof(result), receive.size - sizeof(result));
|
||||
}
|
||||
const std::array<uint32_t, 1> values{static_cast<uint32_t>(result)};
|
||||
(void)writeRpcWords(m_host, receive, values);
|
||||
}
|
||||
|
||||
void writeInitResult(GuestBuffer receive)
|
||||
void writeInitResult(GuestBuffer receive, Flavor flavor, int32_t result)
|
||||
{
|
||||
if (receive.address == 0u || receive.size < sizeof(int32_t))
|
||||
{
|
||||
return;
|
||||
}
|
||||
const std::array<uint32_t, 3> values = {
|
||||
static_cast<uint32_t>(kSucceeded), kMcservVersion, kMcmanVersion};
|
||||
const uint32_t bytes = std::min<uint32_t>(receive.size, sizeof(values));
|
||||
(void)m_host.writeGuest(receive.address, values.data(), bytes);
|
||||
if (receive.size > bytes)
|
||||
{
|
||||
(void)m_host.zeroGuest(receive.address + bytes, receive.size - bytes);
|
||||
}
|
||||
const std::array<uint32_t, 3> values = {static_cast<uint32_t>(result), kMcservVersion, kMcmanVersion};
|
||||
const size_t count = flavor == Flavor::NewXmcserv ? values.size() : 1u;
|
||||
(void)writeRpcWords(m_host, receive, std::span<const uint32_t>(values.data(), count));
|
||||
}
|
||||
|
||||
int32_t handleNameOperation(Operation operation,
|
||||
uint32_t sendAddress,
|
||||
const NameParameter ¶meter)
|
||||
int32_t handleNameOperation(Operation operation, uint32_t sendAddress, const NameParameter ¶meter)
|
||||
{
|
||||
const uint32_t nameAddress = sendAddress + static_cast<uint32_t>(offsetof(NameParameter, name));
|
||||
const uint32_t port = static_cast<uint32_t>(parameter.port);
|
||||
@@ -367,8 +343,7 @@ namespace ps2x::iop::detail
|
||||
case Operation::Read:
|
||||
if (parameter.parameter != 0u)
|
||||
{
|
||||
(void)m_host.zeroGuest(parameter.parameter,
|
||||
flavor == Flavor::NewXmcserv ? 192u : 64u);
|
||||
(void)m_host.zeroGuest(parameter.parameter, flavor == Flavor::NewXmcserv ? 192u : 64u);
|
||||
}
|
||||
return call(MemoryCardOperation::Read,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
[[nodiscard]] inline bool writeRpcWords(IopHost &host, GuestBuffer receive, std::span<const uint32_t> words)
|
||||
{
|
||||
const size_t count = std::min<size_t>(receive.size / sizeof(uint32_t), words.size());
|
||||
const size_t bytes = count * sizeof(uint32_t);
|
||||
if (receive.address == 0u || bytes == 0u)
|
||||
return false;
|
||||
if (bytes - 1u > std::numeric_limits<uint32_t>::max() - receive.address)
|
||||
return false;
|
||||
return host.writeGuest(receive.address, words.data(), bytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace iop_test
|
||||
{
|
||||
using namespace ps2x::iop;
|
||||
|
||||
inline void require(bool condition, const char *message)
|
||||
{
|
||||
if (!condition)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
class Host final : public IopHost
|
||||
{
|
||||
public:
|
||||
explicit Host(size_t bytes = 0x20000u) : guest(bytes, 0xCCu) {}
|
||||
|
||||
bool readGuest(uint32_t address, void *destination, size_t size) const override
|
||||
{
|
||||
if ((size != 0u && !destination) || address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(destination, guest.data() + address, size);
|
||||
++guestReads;
|
||||
return true;
|
||||
}
|
||||
bool writeGuest(uint32_t address, const void *source, size_t size) override
|
||||
{
|
||||
if ((size != 0u && !source) || address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(guest.data() + address, source, size);
|
||||
++guestWrites;
|
||||
return true;
|
||||
}
|
||||
bool zeroGuest(uint32_t address, size_t size) override
|
||||
{
|
||||
if (address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
std::fill_n(guest.begin() + address, size, uint8_t{0});
|
||||
++guestWrites;
|
||||
return true;
|
||||
}
|
||||
bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override
|
||||
{
|
||||
normalized = address;
|
||||
return address < guest.size();
|
||||
}
|
||||
uint32_t allocateIopHandle(IopHandleKind) override { return nextHandle += 0x80u; }
|
||||
uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; }
|
||||
void freeGuest(uint32_t) override {}
|
||||
void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override { ++audioCalls; }
|
||||
std::string hostPath(HostPathKind) const override { return {}; }
|
||||
std::string translateGuestPath(std::string_view path) const override { return std::string(path); }
|
||||
uint64_t openHostFile(std::string_view) override { return file.empty() ? 0u : 1u; }
|
||||
bool hostFileSize(uint64_t handle, uint64_t &size) const override
|
||||
{
|
||||
size = file.size();
|
||||
return handle == 1u && !file.empty();
|
||||
}
|
||||
bool readHostFile(uint64_t handle, uint64_t offset, void *destination, size_t size,
|
||||
size_t &bytesRead) override
|
||||
{
|
||||
bytesRead = 0u;
|
||||
if (handle != 1u || offset > file.size())
|
||||
return false;
|
||||
bytesRead = std::min(size, file.size() - static_cast<size_t>(offset));
|
||||
if (bytesRead != 0u)
|
||||
std::memcpy(destination, file.data() + offset, bytesRead);
|
||||
return true;
|
||||
}
|
||||
void closeHostFile(uint64_t) override {}
|
||||
int32_t memoryCard(const MemoryCardRequest &request) override
|
||||
{
|
||||
cardCalls.push_back(request);
|
||||
return request.operation == MemoryCardOperation::Init ? initResult : 0;
|
||||
}
|
||||
bool hasGuestFunction(uint32_t) const override { return false; }
|
||||
bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void log(LogLevel, std::string_view message) override { logs.emplace_back(message); }
|
||||
|
||||
uint32_t word(uint32_t address) const
|
||||
{
|
||||
uint32_t value = 0u;
|
||||
require(readGuest(address, &value, sizeof(value)), "test read outside guest RAM");
|
||||
return value;
|
||||
}
|
||||
void fill(uint32_t address, size_t size, uint8_t value = 0xCCu)
|
||||
{
|
||||
require(address <= guest.size() && size <= guest.size() - address, "test fill outside RAM");
|
||||
std::fill_n(guest.begin() + address, size, value);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> guest;
|
||||
std::vector<uint8_t> file;
|
||||
std::vector<std::string> logs;
|
||||
std::vector<MemoryCardRequest> cardCalls;
|
||||
mutable size_t guestReads = 0u;
|
||||
size_t guestWrites = 0u;
|
||||
size_t audioCalls = 0u;
|
||||
int32_t initResult = 0;
|
||||
uint32_t nextHandle = 0x1000u;
|
||||
};
|
||||
|
||||
inline RpcRequest request(uint32_t sid, uint32_t function, uint32_t size = 16u)
|
||||
{
|
||||
RpcRequest result{};
|
||||
result.sid = sid;
|
||||
result.function = function;
|
||||
result.receive = {0x800u, size};
|
||||
return result;
|
||||
}
|
||||
|
||||
inline uint64_t metric(const IopSubsystem &iop, std::string_view service, std::string_view name)
|
||||
{
|
||||
for (const auto &row : iop.debugSnapshot().services)
|
||||
if (row.name == service)
|
||||
for (const auto &entry : row.metrics)
|
||||
if (entry.name == name)
|
||||
return entry.value;
|
||||
throw std::runtime_error("missing debug metric");
|
||||
}
|
||||
|
||||
class Irx
|
||||
{
|
||||
public:
|
||||
explicit Irx(uint32_t base = 0x10000u, uint32_t imageBytes = 0x500u)
|
||||
: bytes(0x100u + imageBytes, 0u)
|
||||
{
|
||||
put32(0u, 0x464C457Fu);
|
||||
bytes[4] = bytes[5] = bytes[6] = 1u;
|
||||
put16(16u, 2u);
|
||||
put16(18u, 8u);
|
||||
put32(20u, 1u);
|
||||
put32(24u, base);
|
||||
put32(28u, 52u);
|
||||
put16(40u, 52u);
|
||||
put16(42u, 32u);
|
||||
put16(44u, 1u);
|
||||
put32(52u, 1u);
|
||||
put32(56u, 0x100u);
|
||||
put32(60u, base);
|
||||
put32(64u, base);
|
||||
put32(68u, imageBytes);
|
||||
put32(72u, imageBytes);
|
||||
put32(76u, 7u);
|
||||
put32(80u, 4u);
|
||||
}
|
||||
void words(uint32_t offset, std::initializer_list<uint32_t> values)
|
||||
{
|
||||
for (uint32_t value : values)
|
||||
{
|
||||
put32(0x100u + offset, value);
|
||||
offset += 4u;
|
||||
}
|
||||
}
|
||||
void install(Host &host, uint32_t address = 0x1000u) const
|
||||
{
|
||||
require(host.writeGuest(address, bytes.data(), bytes.size()), "synthetic IRX does not fit");
|
||||
}
|
||||
std::vector<uint8_t> bytes;
|
||||
|
||||
private:
|
||||
void put16(uint32_t offset, uint16_t value)
|
||||
{
|
||||
require(offset + 2u <= bytes.size(), "IRX builder overflow");
|
||||
bytes[offset] = static_cast<uint8_t>(value);
|
||||
bytes[offset + 1u] = static_cast<uint8_t>(value >> 8u);
|
||||
}
|
||||
void put32(uint32_t offset, uint32_t value)
|
||||
{
|
||||
put16(offset, static_cast<uint16_t>(value));
|
||||
put16(offset + 2u, static_cast<uint16_t>(value >> 16u));
|
||||
}
|
||||
};
|
||||
|
||||
inline Irx rpcServer(uint32_t sid, uint32_t reply)
|
||||
{
|
||||
Irx image;
|
||||
image.words(0u, {
|
||||
0x27BDFFE0u, 0xAFBF001Cu, // save ra
|
||||
0x3C040001u, 0x34840200u,
|
||||
0x3C050000u | (sid >> 16u), 0x34A50000u | (sid & 0xFFFFu),
|
||||
0x3C060001u, 0x34C60300u,
|
||||
0x3C070001u, 0x34E70400u,
|
||||
0xAFA00010u, 0xAFA00014u, 0xAFA00018u,
|
||||
0x0C00401Du, 0u, // jal 0x10074: sceSifRegisterRpc
|
||||
0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0u,
|
||||
});
|
||||
image.words(0x60u, {0x41E00000u, 0u, 0x0101u, 0x63666973u, 0x0000646Du,
|
||||
0x03E00008u, 0x24000011u, 0u, 0u});
|
||||
image.words(0x300u, {0x3C020001u, 0x34420400u, 0x03E00008u, 0u});
|
||||
image.words(0x400u, {reply, reply, reply, reply});
|
||||
return image;
|
||||
}
|
||||
|
||||
struct Test
|
||||
{
|
||||
const char *name;
|
||||
void (*function)();
|
||||
};
|
||||
|
||||
inline int run(std::span<const Test> tests)
|
||||
{
|
||||
size_t failures = 0u;
|
||||
for (const Test &test : tests)
|
||||
{
|
||||
try
|
||||
{
|
||||
test.function();
|
||||
std::cout << "PASS " << test.name << '\n';
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
++failures;
|
||||
std::cerr << "FAIL " << test.name << ": " << error.what() << '\n';
|
||||
}
|
||||
}
|
||||
std::cout << tests.size() - failures << '/' << tests.size() << " cases passed\n";
|
||||
return failures == 0u ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
#include "iop_compat_test_support.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace iop_test;
|
||||
constexpr uint32_t dbcSid = 0x80001300u;
|
||||
constexpr uint32_t dbcVersion = 0x80001363u;
|
||||
constexpr uint32_t mcSid = 0x80000400u;
|
||||
|
||||
void dbcDefault()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(!iop.canBindRpc(dbcSid), "unloaded DBCMAN must stay dormant");
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "DBCMAN load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
require(iop.handleRpc(query).handled, "version RPC not handled");
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
require(host.word(0x800u + i * 4u) == 0x0310u, "DBCMAN target version changed");
|
||||
}
|
||||
|
||||
|
||||
|
||||
void dbcResetAndReconfigure()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "unexpected DBCMAN version");
|
||||
iop.reset();
|
||||
require(!iop.canBindRpc(dbcSid), "reset retained a module route");
|
||||
require(metric(iop, "dbcman", "version_queries") == 0u, "query counter not reset");
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "reload failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "IOP reboot changed target version");
|
||||
}
|
||||
|
||||
|
||||
void dbcReplyBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
for (uint32_t size = 0u; size <= 24u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 40u);
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion, size)).handled, "RPC failed");
|
||||
const uint32_t written = std::min(size / 4u, 4u) * 4u;
|
||||
for (uint32_t offset = written; offset < 32u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "reply wrote past whole-word payload");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "reply underflow");
|
||||
}
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
query.receive.address = 0u;
|
||||
const size_t writes = host.guestWrites;
|
||||
require(iop.handleRpc(query).handled && host.guestWrites == writes, "null reply was written");
|
||||
}
|
||||
|
||||
void dbcNoAddressWrap()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
query.receive.address = 0xFFFFFFF8u;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu && host.word(4u) == 0xCCCCCCCCu,
|
||||
"overflowed reply corrupted low guest addresses");
|
||||
require(metric(iop, "dbcman", "failed_version_replies") == 1u, "invalid reply not recorded");
|
||||
}
|
||||
|
||||
void dbcNoRequestGuessing()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
const std::array<uint32_t, 4> randomArguments{0x0310u, 0x00010000u, 0u, 0xFFFFu};
|
||||
require(host.writeGuest(0x600u, randomArguments.data(), sizeof(randomArguments)), "write failed");
|
||||
query.send = {0x600u, sizeof(randomArguments)};
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "send buffer was guessed to be a requested version");
|
||||
}
|
||||
|
||||
void dbcPhysicalServerWins()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "HLE load failed");
|
||||
auto image = rpcServer(dbcSid, 0xDEADBEEFu);
|
||||
image.install(host);
|
||||
auto physical = iop.loadModuleBuffer(0x1000u);
|
||||
require(physical.moduleId > 0 && physical.startResult == 0, "physical IRX failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "physical RPC not handled");
|
||||
require(host.word(0x800u) == 0xDEADBEEFu, "HLE overwrote physical server version");
|
||||
require(metric(iop, "dbcman", "version_queries") == 0u, "HLE ran after physical service");
|
||||
require(iop.stopModule(physical.moduleId), "physical stop failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "HLE fallback not restored");
|
||||
require(host.word(0x800u) == 0x0310u, "wrong HLE version after physical stop");
|
||||
}
|
||||
|
||||
void mcNewInit()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:XMCSERV").moduleId > 0, "XMCSERV load failed");
|
||||
require(iop.handleRpc(request(mcSid, 0xFEu, 16u)).handled, "init RPC failed");
|
||||
require(host.word(0x800u) == 0u && host.word(0x804u) == 0x0205u && host.word(0x808u) == 0x0206u,
|
||||
"new memory-card init layout changed");
|
||||
require(host.word(0x80Cu) == 0xCCCCCCCCu, "new init wrote beyond 12-byte response");
|
||||
}
|
||||
|
||||
void mcOldInit()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
require(iop.handleRpc(request(mcSid, 0x70u, 16u)).handled, "init RPC failed");
|
||||
require(host.word(0x800u) == 0u && host.word(0x804u) == 0xCCCCCCCCu,
|
||||
"old init leaked extended protocol versions");
|
||||
}
|
||||
|
||||
void mcInitFailure()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
host.initResult = -5;
|
||||
for (uint32_t operation : {0x70u, 0xFEu})
|
||||
{
|
||||
require(iop.handleRpc(request(mcSid, operation)).handled, "init RPC failed");
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "init failure reported as success");
|
||||
}
|
||||
}
|
||||
|
||||
void mcReplyBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
for (uint32_t operation : {0x70u, 0xFEu})
|
||||
for (uint32_t size = 0u; size <= 20u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 32u);
|
||||
require(iop.handleRpc(request(mcSid, operation, size)).handled, "init RPC failed");
|
||||
const uint32_t words = operation == 0xFEu ? 3u : 1u;
|
||||
const uint32_t written = std::min(size / 4u, words) * 4u;
|
||||
for (uint32_t offset = written; offset < 24u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "init clobbered response tail");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "init underflowed buffer");
|
||||
}
|
||||
auto query = request(mcSid, 0xFEu);
|
||||
query.receive.address = 0xFFFFFFF8u;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu, "init overflowed guest address");
|
||||
}
|
||||
|
||||
void mcShortNamePacket()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
auto query = request(mcSid, 0x02u, 4u);
|
||||
query.send = {0x1000u, 20u};
|
||||
host.fill(0x1000u, 1044u, 0u);
|
||||
const size_t calls = host.cardCalls.size();
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.cardCalls.size() == calls, "short packet read a filename beyond send.size");
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "short packet not rejected");
|
||||
}
|
||||
|
||||
void mcFullNamePacket()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
const std::array<uint32_t, 5> header{1u, 0u, 1u, 0u, 0u};
|
||||
host.fill(0x1000u, 1044u, 0u);
|
||||
require(host.writeGuest(0x1000u, header.data(), sizeof(header)), "packet header write failed");
|
||||
constexpr char name[] = "/save.dat";
|
||||
require(host.writeGuest(0x1014u, name, sizeof(name)), "packet filename write failed");
|
||||
for (uint32_t operation : {0x02u, 0x71u})
|
||||
{
|
||||
auto query = request(mcSid, operation, 4u);
|
||||
query.send = {0x1000u, 1044u};
|
||||
const size_t before = host.cardCalls.size();
|
||||
require(iop.handleRpc(query).handled, "open RPC failed");
|
||||
require(host.cardCalls.size() == before + 1u, "valid packet not dispatched");
|
||||
const auto &call = host.cardCalls.back();
|
||||
require(call.operation == MemoryCardOperation::Open &&
|
||||
call.arguments[0] == 1u && call.arguments[1] == 0u &&
|
||||
call.arguments[2] == 0x1014u && call.arguments[3] == 1u,
|
||||
"valid name packet decoded incorrectly");
|
||||
}
|
||||
}
|
||||
|
||||
void mcStatusBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
for (uint32_t size = 0u; size <= 20u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 32u);
|
||||
require(iop.handleRpc(request(mcSid, 0xFFFFFFFFu, size)).handled, "RPC failed");
|
||||
const uint32_t written = size >= 4u ? 4u : 0u;
|
||||
if (written != 0u)
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "missing error status");
|
||||
for (uint32_t offset = written; offset < 24u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "status clobbered receive tail");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "status underflowed receive buffer");
|
||||
}
|
||||
auto query = request(mcSid, 0xFFFFFFFFu, 16u);
|
||||
query.receive.address = 0xFFFFFFFCu;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu && host.word(4u) == 0xCCCCCCCCu,
|
||||
"status reply wrapped and zeroed low guest memory");
|
||||
}
|
||||
|
||||
void moduleAliases()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
for (const char *path : {"rom0:XSIO2MAN", "rom0:XPADMAN", "rom0:XMCMAN"})
|
||||
{
|
||||
auto result = iop.loadModule(path);
|
||||
require(result.moduleId > 0 && result.startResult == 0, "known extended module rejected");
|
||||
}
|
||||
require(!iop.canBindRpc(mcSid), "XMCMAN alone enabled a memory-card RPC server");
|
||||
const auto module = iop.loadModule("CDROM0:\\IOP\\xMcSeRv.IrX;1");
|
||||
require(module.moduleId > 0 && iop.canBindRpc(mcSid), "normalized XMCSERV alias not activated");
|
||||
require(iop.stopModule(module.moduleId) && !iop.canBindRpc(mcSid), "stopped alias remained active");
|
||||
}
|
||||
|
||||
void unknownModulesStayUnknown()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
for (const char *name : {"MC2_D.IRX", "DS2U_D.IRX", "CDVDSTM.IRX", "SDRDRV.IRX", "EZPCM.IRX", "ANYTHING_D.IRX"})
|
||||
{
|
||||
const auto result = iop.loadModule(std::string("host0:IOPModules/") + name);
|
||||
require(result.moduleId < 0 && result.startResult < 0, "unsupported module got a fake success");
|
||||
}
|
||||
require(!iop.canBindRpc(0x19740512u), "game-specific SDRDRV activated globally");
|
||||
}
|
||||
|
||||
void moduleLifetime()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
const auto a = iop.loadModule("rom0:DBCMAN");
|
||||
const auto b = iop.loadModule("rom0:dbcman.irx");
|
||||
const auto alias = iop.loadModule("rom0:DBCM");
|
||||
require(a.moduleId > 0 && a.moduleId == b.moduleId && alias.moduleId > 0, "module IDs unstable");
|
||||
require(iop.stopModule(a.moduleId) && iop.canBindRpc(dbcSid), "first release removed shared route");
|
||||
require(iop.stopModule(b.moduleId) && iop.canBindRpc(dbcSid), "remaining alias not honored");
|
||||
require(iop.stopModule(alias.moduleId) && !iop.canBindRpc(dbcSid), "last release retained route");
|
||||
}
|
||||
|
||||
void loaderDiagnostics()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("host0:LIBSD.IRX").moduleId > 0, "LIBSD fallback failed");
|
||||
require(iop.loadModule("host0:MISSING.IRX").moduleId < 0, "unknown load accepted");
|
||||
for (unsigned i = 0u; i < 100u; ++i)
|
||||
(void)iop.loadModule("host0:MISSING.IRX");
|
||||
auto snapshot = iop.debugSnapshot();
|
||||
require(snapshot.diagnostics.size() == 2u, "final loader outcomes not deduplicated");
|
||||
require(snapshot.diagnostics[0].find("[IOP:HLE]") != std::string::npos, "no fallback diagnostic");
|
||||
require(snapshot.diagnostics[1].find("no HLE provider") != std::string::npos, "no final failure diagnostic");
|
||||
for (unsigned i = 0u; i < 100u; ++i)
|
||||
(void)iop.loadModule("rom0:missing" + std::to_string(i));
|
||||
require(iop.debugSnapshot().diagnostics.size() <= 32u, "unbounded module diagnostics");
|
||||
iop.reset();
|
||||
require(iop.debugSnapshot().diagnostics.empty(), "stale load outcomes survived reset");
|
||||
}
|
||||
|
||||
void libsdUnchanged()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:LIBSD").moduleId > 0, "LIBSD load failed");
|
||||
require(iop.handleRpc(request(0x80000701u, 0x8010u)).handled, "LIBSD RPC not handled");
|
||||
require(host.audioCalls == 1u, "DBCMAN option intercepted LIBSD RPC");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
const Test tests[] = {
|
||||
{"DBCMAN default and dormant route", dbcDefault},
|
||||
{"DBCMAN reboot and reconfiguration", dbcResetAndReconfigure},
|
||||
{"DBCMAN bounded whole-word response", dbcReplyBounds},
|
||||
{"DBCMAN rejects wrapping reply addresses", dbcNoAddressWrap},
|
||||
{"DBCMAN does not infer version from arbitrary RPC payload", dbcNoRequestGuessing},
|
||||
{"Physical DBCMAN server wins over configured HLE", dbcPhysicalServerWins},
|
||||
{"XMCSERV init status and two version fields", mcNewInit},
|
||||
{"Old MCSERV init is status only", mcOldInit},
|
||||
{"MCSERV propagates initialization failure", mcInitFailure},
|
||||
{"MCSERV response bounds for both dialects", mcReplyBounds},
|
||||
{"MCSERV rejects truncated name packet", mcShortNamePacket},
|
||||
{"MCSERV accepts complete name packets in both dialects", mcFullNamePacket},
|
||||
{"MCSERV status replies preserve bounds and cannot wrap", mcStatusBounds},
|
||||
{"Extended module aliases and activation", moduleAliases},
|
||||
{"Unsupported debug and game IRX stay unsupported", unknownModulesStayUnknown},
|
||||
{"HLE repeated loads and alias lifetime", moduleLifetime},
|
||||
{"Loader outcomes are bounded and resettable", loaderDiagnostics},
|
||||
{"LIBSD audio dispatch is unchanged", libsdUnchanged},
|
||||
};
|
||||
return run(tests);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "iop_compat_test_support.h"
|
||||
|
||||
#include "emulator/core/iop_cpu.h"
|
||||
#include "emulator/core/iop_memory.h"
|
||||
#include "emulator/imports/iop_imports.h"
|
||||
#include "emulator/imports/iop_loadcore.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace iop_test;
|
||||
using namespace ps2x::iop::detail;
|
||||
|
||||
void addExport(IopMemory &memory, IopImportRegistry &imports, uint32_t address,
|
||||
uint16_t version, uint32_t target, uint32_t count = 4u)
|
||||
{
|
||||
require(memory.zeroRam(address, 128u), "export table does not fit");
|
||||
memory.write32(address, 0x41C00000u);
|
||||
memory.write16(address + 8u, version);
|
||||
constexpr char name[8] = "tstlib";
|
||||
require(memory.writeRam(address + 12u, name, sizeof(name)), "export name does not fit");
|
||||
for (uint32_t i = 0u; i < count; ++i)
|
||||
memory.write32(address + 20u + 4u * i, target);
|
||||
require(imports.registerExportTable(address), "export registration failed");
|
||||
}
|
||||
|
||||
void importTable(IopMemory &memory, uint32_t address, uint16_t version)
|
||||
{
|
||||
require(memory.zeroRam(address, 64u), "import table does not fit");
|
||||
memory.write32(address, 0x41E00000u);
|
||||
memory.write16(address + 8u, version);
|
||||
constexpr char name[8] = "tstlib";
|
||||
require(memory.writeRam(address + 12u, name, sizeof(name)), "import name does not fit");
|
||||
memory.write32(address + 20u, 0x03E00008u);
|
||||
memory.write32(address + 24u, 0x24000003u);
|
||||
}
|
||||
|
||||
void decodeVersion()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
importTable(memory, 0x1000u, 0x0310u);
|
||||
const auto call = imports.decode(0x1014u);
|
||||
require(call && call->library == "tstlib" && call->ordinal == 3u && call->version == 0x0310u,
|
||||
"decoder dropped the import library version");
|
||||
const auto alias = imports.decode(0x80001014u);
|
||||
require(alias && alias->version == 0x0310u, "cached alias lost import version");
|
||||
}
|
||||
|
||||
void majorIsolation()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0201u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0101u, 0x3100u);
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x3100u, "linked to wrong library major");
|
||||
require(imports.resolve("tstlib", 3u, 0x0201u) == 0x2100u, "second major unavailable");
|
||||
require(imports.resolve("tstlib", 3u, 0x0300u) == 0u, "incompatible major silently linked");
|
||||
require(imports.findTable("tstlib", 0x0300u) == 0u, "query ignored requested major");
|
||||
}
|
||||
|
||||
void newestMinor()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0101u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0104u, 0x3100u);
|
||||
addExport(memory, imports, 0x1400u, 0x0103u, 0x4100u);
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x3100u, "selected lowest address, not newest minor");
|
||||
require(imports.resolve("tstlib", 3u, 0x017Fu) == 0x3100u,
|
||||
"invented a minimum-minor rule absent from LOADCORE linking");
|
||||
require(imports.releaseExportTable(0x1800u), "unregister failed");
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x4100u, "unregistered library remained selected");
|
||||
}
|
||||
|
||||
void missingOrdinal()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0101u, 0x2100u, 8u);
|
||||
addExport(memory, imports, 0x1800u, 0x0102u, 0x3100u, 4u);
|
||||
require(imports.resolve("tstlib", 7u, 0x0101u) == 0u,
|
||||
"missing ordinal fell back to a different export table");
|
||||
require(imports.resolve("missing", 0u, 0x0101u) == 0u, "missing library resolved");
|
||||
imports.reset();
|
||||
require(imports.resolve("tstlib", 0u, 0x0101u) == 0u, "registry reset left exports");
|
||||
}
|
||||
|
||||
void queryFunctionArray()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
IopLoadcore loadcore(memory, imports);
|
||||
addExport(memory, imports, 0x1000u, 0x0201u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0101u, 0x3100u);
|
||||
importTable(memory, 0x800u, 0x0102u);
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = 0x800u;
|
||||
require(loadcore.dispatchImport(11u, cpu), "QueryLibraryEntryTable unhandled");
|
||||
require(cpu.gpr[2] == 0x1814u && memory.read32(cpu.gpr[2]) == 0x3100u,
|
||||
"query returned an export header instead of function array");
|
||||
memory.write16(0x808u, 0x0300u);
|
||||
require(loadcore.dispatchImport(11u, cpu) && cpu.gpr[2] == 0u, "query accepted wrong major");
|
||||
for (uint32_t address : {0u, 0xFFFFFFF8u, IopMemory::RamSize - 4u})
|
||||
{
|
||||
cpu.gpr[4] = address;
|
||||
require(loadcore.dispatchImport(11u, cpu) && cpu.gpr[2] == 0u, "invalid query pointer accepted");
|
||||
}
|
||||
}
|
||||
|
||||
Irx provider(uint32_t base, uint16_t version, uint32_t result)
|
||||
{
|
||||
Irx image(base);
|
||||
const uint32_t table = base + 0x80u;
|
||||
const uint32_t importStub = base + 0xC0u + 20u;
|
||||
image.words(0u, {0x27BDFFE0u, 0xAFBF001Cu,
|
||||
0x3C040000u | (table >> 16u), 0x34840000u | (table & 0xFFFFu),
|
||||
0x0C000000u | (importStub >> 2u), 0u,
|
||||
0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0u});
|
||||
image.words(0x60u, {0x03E00008u, 0x24020000u | result});
|
||||
image.words(0x80u, {0x41C00000u, 0u, version, 0x6C747374u, 0x00006269u,
|
||||
base, base, base, base + 0x60u, 0u});
|
||||
image.words(0xC0u, {0x41E00000u, 0u, 0x0101u, 0x64616F6Cu, 0x65726F63u,
|
||||
0x03E00008u, 0x24000006u, 0u, 0u});
|
||||
return image;
|
||||
}
|
||||
|
||||
Irx consumer(uint16_t version)
|
||||
{
|
||||
constexpr uint32_t base = 0x13000u;
|
||||
Irx image(base);
|
||||
image.words(0u, {0x27BDFFF0u, 0xAFBF000Cu,
|
||||
0x0C000000u | ((base + 0x54u) >> 2u), 0u,
|
||||
0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0u});
|
||||
image.words(0x40u, {0x41E00000u, 0u, version, 0x6C747374u, 0x00006269u,
|
||||
0x03E00008u, 0x24000003u, 0u, 0u});
|
||||
return image;
|
||||
}
|
||||
|
||||
void physicalImportsEndToEnd()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
auto wrongMajor = provider(0x10000u, 0x0201u, 0x22u);
|
||||
wrongMajor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 2 failed");
|
||||
auto oldMinor = provider(0x11000u, 0x0101u, 0x11u);
|
||||
oldMinor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 1 failed");
|
||||
auto newMinor = provider(0x12000u, 0x0103u, 0x13u);
|
||||
newMinor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 1.3 failed");
|
||||
auto client = consumer(0x0101u);
|
||||
client.install(host);
|
||||
const auto result = iop.loadModuleBuffer(0x1000u);
|
||||
require(result.moduleId > 0 && result.startResult == 0x13, "R3000A called wrong export version");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
const Test tests[] = {
|
||||
{"Import decoder preserves library ABI version", decodeVersion},
|
||||
{"Different major versions cannot cross-link", majorIsolation},
|
||||
{"Newest registered minor wins within the requested major", newestMinor},
|
||||
{"Ordinal lookup stays in the selected table", missingOrdinal},
|
||||
{"LOADCORE query returns function array and honors major", queryFunctionArray},
|
||||
{"Physical IRX consumer links correct version end to end", physicalImportsEndToEnd},
|
||||
};
|
||||
return run(tests);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime/gs/gs_backend.h"
|
||||
#include "runtime/gs/gs_texture_page_cache.h"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
@@ -61,8 +61,8 @@ private:
|
||||
uint32_t sourceOriginX,
|
||||
uint32_t sourceOriginY) const;
|
||||
|
||||
using WriteVramFunc = std::function<void(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t)>;
|
||||
using ReadVramFunc = std::function<uint32_t(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t)>;
|
||||
using WriteVramFunc = void (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
using ReadVramFunc = uint32_t (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t);
|
||||
|
||||
static constexpr size_t kPsmHandlerCount = 1u << 6u;
|
||||
mutable std::mutex m_mutex;
|
||||
@@ -72,8 +72,7 @@ private:
|
||||
std::array<WriteVramFunc, kPsmHandlerCount> m_writeVramFuncs{};
|
||||
std::array<uint16_t, 512> m_clut{};
|
||||
std::array<uint32_t, 2> m_clutCbp{};
|
||||
std::vector<uint8_t> m_texturePageBuffer;
|
||||
uint32_t m_texturePageIndex = UINT32_MAX;
|
||||
GSMem::TexturePageCache m_texturePageCache;
|
||||
|
||||
GSTransferCommand m_transfer{};
|
||||
GSTransferSnapshot m_transferState{};
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace GSMem
|
||||
{
|
||||
class TexturePageCache
|
||||
{
|
||||
public:
|
||||
static constexpr uint32_t kPageSize = 8192u;
|
||||
|
||||
void Invalidate() noexcept
|
||||
{
|
||||
m_pageBase = UINT32_MAX;
|
||||
}
|
||||
|
||||
// byteAddress is the wrapped, swizzled VRAM address. The returned
|
||||
// pointer is valid only until the next miss or invalidation.
|
||||
const uint8_t* Resolve(const uint8_t* vram, uint32_t byteAddress) noexcept
|
||||
{
|
||||
const uint32_t pageBase = byteAddress & ~(kPageSize - 1u);
|
||||
if (m_pageBase != pageBase)
|
||||
{
|
||||
std::memcpy(m_bytes.data(), vram + pageBase, kPageSize);
|
||||
m_pageBase = pageBase;
|
||||
}
|
||||
return m_bytes.data() + (byteAddress & (kPageSize - 1u));
|
||||
}
|
||||
|
||||
private:
|
||||
alignas(64) std::array<uint8_t, kPageSize> m_bytes{};
|
||||
uint32_t m_pageBase = UINT32_MAX;
|
||||
};
|
||||
}
|
||||
@@ -7,11 +7,12 @@
|
||||
#include <span>
|
||||
|
||||
#include "types.h"
|
||||
#include "runtime/gs/gs_texture_page_cache.h"
|
||||
|
||||
namespace GSMem
|
||||
{
|
||||
constexpr usz MEMORY_SIZE = 4_mb;
|
||||
constexpr usz GS_PAGE_SIZE = 8_kb;
|
||||
constexpr usz GS_PAGE_SIZE = TexturePageCache::kPageSize;
|
||||
|
||||
// these are all the same regardless of storage mode
|
||||
constexpr usz BLOCKS_PER_PAGE = 32;
|
||||
@@ -261,7 +262,7 @@ namespace GSMem
|
||||
static constexpr void Write(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y, PackedT value);
|
||||
|
||||
// reads the pixel
|
||||
static constexpr auto Read(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y) -> PackedT;
|
||||
static constexpr auto Read(const PageLookupTableT& table, const u8* data, u32 block, u32 bw, u32 x, u32 y, TexturePageCache* cache = nullptr) -> PackedT;
|
||||
|
||||
static_assert(BlocksPerPage() == BLOCKS_PER_PAGE);
|
||||
static_assert(IsValidPsm(psm));
|
||||
@@ -501,15 +502,16 @@ namespace GSMem
|
||||
}
|
||||
|
||||
template<PixelStorageMode psm>
|
||||
constexpr auto PixelStorageTraits<psm>::Read(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y) -> PackedT
|
||||
constexpr auto PixelStorageTraits<psm>::Read(const PageLookupTableT& table, const u8* data, u32 block, u32 bw, u32 x, u32 y, TexturePageCache* cache) -> PackedT
|
||||
{
|
||||
const u32 pixel_addr = Address(table, block, bw, x, y);
|
||||
const u32 bits = pixel_addr * UnpackedBitWidth(psm) + BitOffset();
|
||||
const u32 byte_addr = (bits / 8) & (MEMORY_SIZE - sizeof(PackedT));
|
||||
const u32 shift = bits % 8;
|
||||
|
||||
const u8* source = cache ? cache->Resolve(data, byte_addr) : data + byte_addr;
|
||||
PackedT v;
|
||||
std::memcpy(&v, &data[byte_addr], sizeof(PackedT));
|
||||
std::memcpy(&v, source, sizeof(PackedT));
|
||||
|
||||
switch (psm)
|
||||
{
|
||||
@@ -533,11 +535,14 @@ namespace GSMem
|
||||
break;
|
||||
}
|
||||
|
||||
return 0xFFFF00FFu;
|
||||
return static_cast<PackedT>(0xFFFF00FFu);
|
||||
}
|
||||
|
||||
void InitLookupTables();
|
||||
|
||||
// Shares swizzle, VRAM wrapping, and lane extraction with the direct reads.
|
||||
u32 ReadTexture(TexturePageCache& cache, const u8* data, u32 psm, u32 bp, u32 bw, u32 x, u32 y);
|
||||
|
||||
void WriteCT32(u8* data, u32 bp, u32 bw, u32 x, u32 y, u32 value);
|
||||
void WriteZ32(u8* data, u32 bp, u32 bw, u32 x, u32 y, u32 value);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace GSInternal;
|
||||
|
||||
@@ -291,32 +292,6 @@ namespace
|
||||
return psm == GS_PSM_T8 || psm == GS_PSM_T8H;
|
||||
}
|
||||
|
||||
uint32_t texturePageIndex(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y)
|
||||
{
|
||||
switch (psm & 0x3Fu)
|
||||
{
|
||||
case GS_PSM_CT32:
|
||||
case GS_PSM_CT24:
|
||||
case GS_PSM_Z32:
|
||||
case GS_PSM_Z24:
|
||||
case GS_PSM_T8H:
|
||||
case GS_PSM_T4HL:
|
||||
case GS_PSM_T4HH:
|
||||
return static_cast<uint32_t>(GSMem::PixelStorageTraits<GSMem::C32>::PageId(base, bw, x, y));
|
||||
case GS_PSM_CT16:
|
||||
case GS_PSM_CT16S:
|
||||
case GS_PSM_Z16:
|
||||
case GS_PSM_Z16S:
|
||||
return static_cast<uint32_t>(GSMem::PixelStorageTraits<GSMem::C16>::PageId(base, bw, x, y));
|
||||
case GS_PSM_T8:
|
||||
return static_cast<uint32_t>(GSMem::PixelStorageTraits<GSMem::P8>::PageId(base, bw, x, y));
|
||||
case GS_PSM_T4:
|
||||
return static_cast<uint32_t>(GSMem::PixelStorageTraits<GSMem::P4>::PageId(base, bw, x, y));
|
||||
default:
|
||||
return UINT32_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t lerpChannel(uint8_t c00, uint8_t c10, uint8_t c01, uint8_t c11, float fx, float fy)
|
||||
{
|
||||
const float top = static_cast<float>(c00) + (static_cast<float>(c10) - static_cast<float>(c00)) * fx;
|
||||
@@ -540,10 +515,12 @@ GSCpuBackend::GSCpuBackend()
|
||||
|
||||
void GSCpuBackend::Initialize(uint8_t *vram, uint32_t vramSize)
|
||||
{
|
||||
if (vram && vramSize < GSMem::MEMORY_SIZE)
|
||||
throw std::invalid_argument("GS CPU backend requires at least 4 MiB of VRAM");
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_vram = vram;
|
||||
m_vramSize = vramSize;
|
||||
m_texturePageBuffer.resize(vramSize);
|
||||
ResetUnlocked();
|
||||
}
|
||||
|
||||
@@ -557,7 +534,7 @@ void GSCpuBackend::ResetUnlocked()
|
||||
{
|
||||
m_clut.fill(0u);
|
||||
m_clutCbp.fill(0u);
|
||||
m_texturePageIndex = UINT32_MAX;
|
||||
m_texturePageCache.Invalidate();
|
||||
m_transfer = {};
|
||||
m_transfer.direction = 3u;
|
||||
m_transferState = {};
|
||||
@@ -645,9 +622,8 @@ void GSCpuBackend::LoadClutUnlocked(const GSTex0Reg &tex0, const GSTexClutReg &t
|
||||
sourceY = static_cast<uint32_t>(texclut.cov);
|
||||
}
|
||||
|
||||
const uint32_t raw = ReadTextureVramUnlocked(tex0.cpsm, tex0.cbp, sourceWidth, sourceX, sourceY);
|
||||
const uint32_t destination = (loadCsm1Suffix ? entry : destinationBase + entry) &
|
||||
(sixteenBit ? 0x1FFu : 0x0FFu);
|
||||
const uint32_t raw = ReadTextureVramUnlocked(tex0.cpsm, tex0.cbp, sourceWidth, sourceX, sourceY);
|
||||
const uint32_t destination = (loadCsm1Suffix ? entry : destinationBase + entry) & (sixteenBit ? 0x1FFu : 0x0FFu);
|
||||
if (sixteenBit)
|
||||
{
|
||||
m_clut[destination] = static_cast<uint16_t>(raw);
|
||||
@@ -668,7 +644,7 @@ void GSCpuBackend::Flush()
|
||||
void GSCpuBackend::TextureFlush()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_texturePageIndex = UINT32_MAX;
|
||||
m_texturePageCache.Invalidate();
|
||||
}
|
||||
|
||||
void GSCpuBackend::Sync(GSSyncReason)
|
||||
@@ -694,20 +670,7 @@ uint32_t GSCpuBackend::ReadTextureVramUnlocked(uint32_t psm, uint32_t base, uint
|
||||
if (!m_vram)
|
||||
return 0u;
|
||||
|
||||
const uint32_t pageCount = m_vramSize / static_cast<uint32_t>(GSMem::GS_PAGE_SIZE);
|
||||
uint32_t page = texturePageIndex(psm, base, bw, x, y);
|
||||
if (page == UINT32_MAX || pageCount == 0u || m_texturePageBuffer.size() < m_vramSize)
|
||||
return ReadVramUnlocked(psm, base, bw, x, y);
|
||||
|
||||
page %= pageCount;
|
||||
if (m_texturePageIndex != page)
|
||||
{
|
||||
const size_t pageOffset = static_cast<size_t>(page) * GSMem::GS_PAGE_SIZE;
|
||||
std::memcpy(m_texturePageBuffer.data() + pageOffset, m_vram + pageOffset, GSMem::GS_PAGE_SIZE);
|
||||
m_texturePageIndex = page;
|
||||
}
|
||||
|
||||
return m_readVramFuncs[psm & 0x3Fu](m_texturePageBuffer.data(), base, bw, x, y);
|
||||
return GSMem::ReadTexture(m_texturePageCache, m_vram, psm, base, bw, x, y);
|
||||
}
|
||||
|
||||
void GSCpuBackend::WriteVram(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y, uint32_t value)
|
||||
|
||||
@@ -220,6 +220,41 @@ namespace GSMem
|
||||
PixelStorageTraits<P4>::InitPageLookupTable(PageTableP4, BlockTableP4, ColumnTable4);
|
||||
}
|
||||
|
||||
u32 ReadTexture(TexturePageCache& cache, const u8* data, u32 psm, u32 bp, u32 bw, u32 x, u32 y)
|
||||
{
|
||||
switch (static_cast<PixelStorageMode>(psm & 0x3Fu))
|
||||
{
|
||||
case C32:
|
||||
return PixelStorageTraits<C32>::Read(PageTableC32, data, bp, bw, x, y, &cache);
|
||||
case C24:
|
||||
return PixelStorageTraits<C24>::Read(PageTableC32, data, bp, bw, x, y, &cache);
|
||||
case C16:
|
||||
return PixelStorageTraits<C16>::Read(PageTableC16, data, bp, bw, x, y, &cache);
|
||||
case C16S:
|
||||
return PixelStorageTraits<C16S>::Read(PageTableC16S, data, bp, bw, x, y, &cache);
|
||||
case P8:
|
||||
return PixelStorageTraits<P8>::Read(PageTableP8, data, bp, bw, x, y, &cache);
|
||||
case P4:
|
||||
return PixelStorageTraits<P4>::Read(PageTableP4, data, bp, bw, x, y, &cache);
|
||||
case P8H:
|
||||
return PixelStorageTraits<P8H>::Read(PageTableC32, data, bp, bw, x, y, &cache);
|
||||
case P4HL:
|
||||
return PixelStorageTraits<P4HL>::Read(PageTableC32, data, bp, bw, x, y, &cache);
|
||||
case P4HH:
|
||||
return PixelStorageTraits<P4HH>::Read(PageTableC32, data, bp, bw, x, y, &cache);
|
||||
case Z32:
|
||||
return PixelStorageTraits<Z32>::Read(PageTableZ32, data, bp, bw, x, y, &cache);
|
||||
case Z24:
|
||||
return PixelStorageTraits<Z24>::Read(PageTableZ32, data, bp, bw, x, y, &cache);
|
||||
case Z16:
|
||||
return PixelStorageTraits<Z16>::Read(PageTableZ16, data, bp, bw, x, y, &cache);
|
||||
case Z16S:
|
||||
return PixelStorageTraits<Z16S>::Read(PageTableZ16S, data, bp, bw, x, y, &cache);
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteCT32(u8* data, u32 bp, u32 bw, u32 x, u32 y, u32 value)
|
||||
{
|
||||
PixelStorageTraits<C32>::Write(PageTableC32, data, bp, bw, x, y, value);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
@@ -5,6 +5,11 @@ project(ps2xTest LANGUAGES C CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(gs_cache)
|
||||
endif()
|
||||
|
||||
if(PS2X_IOP_ENABLE_PLUGINS AND (WIN32 OR (UNIX AND NOT APPLE)))
|
||||
add_library(ps2_iop_fake_plugin MODULE
|
||||
src/fake_iop_plugin.cpp
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
project(PS2GSCacheTests LANGUAGES CXX)
|
||||
include(CTest)
|
||||
endif()
|
||||
|
||||
get_filename_component(PS2_GS_DEFAULT_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/../../ps2xRuntime" ABSOLUTE)
|
||||
set(PS2_GS_RUNTIME_DIR "${PS2_GS_DEFAULT_RUNTIME_DIR}" CACHE PATH "Runtime source to test")
|
||||
option(PS2_GS_CACHE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
|
||||
option(PS2_GS_CACHE_BUILD_MEMORY_TESTS "Build tests for the new shared cached-reader API" ON)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
add_library(ps2_gs_cache_backend_under_test STATIC
|
||||
"${PS2_GS_RUNTIME_DIR}/src/lib/gs/gs_cpu_backend.cpp"
|
||||
"${PS2_GS_RUNTIME_DIR}/src/lib/gs/gs_frontend.cpp"
|
||||
"${PS2_GS_RUNTIME_DIR}/src/lib/gs/ps2_gs_memory.cpp"
|
||||
)
|
||||
target_include_directories(ps2_gs_cache_backend_under_test PUBLIC "${PS2_GS_RUNTIME_DIR}/include")
|
||||
target_compile_features(ps2_gs_cache_backend_under_test PUBLIC cxx_std_20)
|
||||
target_compile_definitions(ps2_gs_cache_backend_under_test PRIVATE PS2_RUNTIME_LOGS=0 AGRESSIVE_LOGS=0)
|
||||
target_link_libraries(ps2_gs_cache_backend_under_test PUBLIC Threads::Threads)
|
||||
set_target_properties(ps2_gs_cache_backend_under_test PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
||||
if(PS2_GS_CACHE_SANITIZERS)
|
||||
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" OR MSVC)
|
||||
message(FATAL_ERROR "PS2_GS_CACHE_SANITIZERS requires a GCC/Clang sanitizer toolchain")
|
||||
endif()
|
||||
target_compile_options(ps2_gs_cache_backend_under_test PUBLIC
|
||||
-fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all)
|
||||
target_link_options(ps2_gs_cache_backend_under_test PUBLIC -fsanitize=address,undefined)
|
||||
endif()
|
||||
|
||||
function(add_gs_cache_suite target source prefix)
|
||||
add_executable(${target} "${source}")
|
||||
target_link_libraries(${target} PRIVATE ps2_gs_cache_backend_under_test)
|
||||
set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF)
|
||||
foreach(test_name IN LISTS ARGN)
|
||||
add_test(NAME "gs_cache.${prefix}.${test_name}" COMMAND ${target} "${test_name}")
|
||||
set_tests_properties("gs_cache.${prefix}.${test_name}" PROPERTIES LABELS "gs;cache" TIMEOUT 60)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
add_gs_cache_suite(ps2_gs_texture_cache_tests gs_texture_cache_tests.cpp texture
|
||||
unaligned_texture unaligned_wrap stale_mirror page_alternation
|
||||
flush_visibility upload_visibility local_copy_visibility raster_visibility
|
||||
reset_and_rebind invalid_vram_size reserved_psm)
|
||||
|
||||
add_gs_cache_suite(ps2_gs_clut_cache_tests gs_clut_cache_tests.cpp clut
|
||||
unaligned_csm1_ct32 unaligned_csm1_ct16 unaligned_csm1_ct16s wrapped_clut unaligned_csm2
|
||||
retained_palette clut_uses_page_cache cbp0_conditional cbp1_conditional
|
||||
reserved_cld nonindexed_cld tex2_reload shared_contexts
|
||||
csa_ct32 csa_ct16 csa_ct16s texa_without_reload palette_before_filtering high_planes)
|
||||
|
||||
if(PS2_GS_CACHE_BUILD_MEMORY_TESTS)
|
||||
add_gs_cache_suite(ps2_gs_memory_cache_tests gs_memory_cache_tests.cpp memory
|
||||
ct32 ct24 ct16 ct16s t8 t4 t8h t4hl t4hh z32 z24 z16 z16s
|
||||
alias_lanes physical_tag_aliases)
|
||||
endif()
|
||||
@@ -0,0 +1,283 @@
|
||||
#include "gs_test_support.h"
|
||||
|
||||
using namespace GSTest;
|
||||
|
||||
namespace
|
||||
{
|
||||
template<uint8_t Cpsm>
|
||||
void unalignedCsm1()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T8, 64);
|
||||
tex.cbp = 31;
|
||||
tex.cpsm = Cpsm;
|
||||
f.index(tex, 128);
|
||||
f.palette(tex, 128, Cpsm == GS_PSM_CT32 ? kRed : 0x801Fu);
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kRed, "CSM1 CLUT load crosses a physical page");
|
||||
}
|
||||
|
||||
void wrappedClut()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T8, 64);
|
||||
tex.cbp = 16383;
|
||||
f.index(tex, 128);
|
||||
f.palette(tex, 128, kGreen);
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kGreen, "CLUT load wraps at the end of VRAM");
|
||||
}
|
||||
|
||||
void unalignedCsm2()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T8, 64);
|
||||
tex.cbp = 31;
|
||||
tex.cpsm = GS_PSM_CT16;
|
||||
tex.csm = 1;
|
||||
constexpr uint32_t entry = 193;
|
||||
f.index(tex, entry);
|
||||
f.gs.writeRegister(GS_REG_TEXCLUT, 4ull | (3ull << 6) | (2ull << 12));
|
||||
f.gs.WriteVram(GS_PSM_CT16, tex.cbp, 4, 48 + entry, 2, 0x83E0);
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kGreen, "CSM2 CBW/COU/COV and swizzle carry");
|
||||
}
|
||||
|
||||
void retainedPalette()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
f.index(tex, 8);
|
||||
f.palette(tex, 8, kRed);
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kRed, "initial palette");
|
||||
f.palette(tex, 8, kGreen);
|
||||
f.flush();
|
||||
tex.cld = 0;
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kRed, "TEXFLUSH and CLD=0 preserve the CLUT temporary buffer");
|
||||
tex.cld = 1;
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kGreen, "CLD=1 reloads the palette");
|
||||
}
|
||||
|
||||
void clutUsesPageCache()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
f.index(tex, 8);
|
||||
f.palette(tex, 8, kRed);
|
||||
f.bind(tex);
|
||||
// No texture sampling between loads: the CLUT source page is still resident.
|
||||
f.palette(tex, 8, kGreen);
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kRed, "CLD=1 alone does not invalidate the texture page buffer");
|
||||
f.flush();
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kGreen, "identical TEX0 write still loads after TEXFLUSH");
|
||||
}
|
||||
|
||||
template<unsigned Bank>
|
||||
void conditionalLoad()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
tex.cld = 2 + Bank;
|
||||
f.index(tex, 0);
|
||||
f.palette(tex, 0, kRed);
|
||||
f.bind(tex);
|
||||
auto other = tex;
|
||||
other.cbp = 192;
|
||||
other.cld = 3 - Bank;
|
||||
f.palette(other, 0, kGreen);
|
||||
f.bind(other);
|
||||
tex.cld = 4 + Bank;
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kGreen, "matching CBP skips load, not switches palettes");
|
||||
tex.cbp = 160;
|
||||
f.palette(tex, 0, kBlue);
|
||||
f.flush();
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kBlue, "different CBP loads and updates comparison memory");
|
||||
f.palette(tex, 0, kRed);
|
||||
f.flush();
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kBlue, "repeated conditional CBP skips reload");
|
||||
}
|
||||
|
||||
void reservedCld()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
f.index(tex, 0);
|
||||
f.palette(tex, 0, kRed);
|
||||
f.bind(tex);
|
||||
tex.cbp = 192;
|
||||
f.palette(tex, 0, kGreen);
|
||||
for (uint8_t cld : {6, 7})
|
||||
{
|
||||
tex.cld = cld;
|
||||
f.flush();
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kRed, "reserved CLD leaves palette unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
void nonIndexedCld()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
tex.cld = 2;
|
||||
f.index(tex, 0);
|
||||
f.palette(tex, 0, kRed);
|
||||
f.bind(tex);
|
||||
auto direct = tex;
|
||||
direct.psm = GS_PSM_CT32;
|
||||
direct.cbp = 192;
|
||||
f.bind(direct);
|
||||
f.palette(tex, 0, kGreen);
|
||||
f.flush();
|
||||
tex.cld = 4;
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kRed, "direct texture TEX0 must not modify CBP0");
|
||||
}
|
||||
|
||||
void tex2Reload()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T8, 64);
|
||||
f.index(tex, 128);
|
||||
f.palette(tex, 128, kRed);
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kRed, "initial TEX0 palette");
|
||||
tex.cbp = 31;
|
||||
f.palette(tex, 128, kGreen);
|
||||
tex.tbp0 = 2048;
|
||||
tex.tbw = 8;
|
||||
tex.tw = tex.th = 9;
|
||||
f.flush();
|
||||
f.bind(tex, 0, true);
|
||||
expectEqual(f.sample(), kGreen, "TEX2 reloads from a crossing CLUT without changing texture layout");
|
||||
const auto state = f.gs.getDebugSnapshot();
|
||||
expectEqual(state.ctx[0].tex0.tbp0, 64, "TEX2 preserves TBP");
|
||||
expectEqual(state.ctx[0].tex0.tbw, 2, "TEX2 preserves TBW");
|
||||
expectEqual(state.ctx[0].tex0.tw, 8, "TEX2 preserves TW");
|
||||
}
|
||||
|
||||
void sharedContexts()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
f.index(tex, 0);
|
||||
f.palette(tex, 0, kRed);
|
||||
f.bind(tex, 0);
|
||||
tex.cbp = 192;
|
||||
f.palette(tex, 0, kGreen);
|
||||
f.bind(tex, 1);
|
||||
expectEqual(f.sample(0, 0, 0), kGreen, "both drawing contexts share one CLUT temporary buffer");
|
||||
tex.cbp = 256;
|
||||
f.palette(tex, 0, kBlue);
|
||||
f.bind(tex, 1, true);
|
||||
expectEqual(f.sample(0, 0, 0), kBlue, "context 1 TEX2 changes palette visible to context 0");
|
||||
}
|
||||
|
||||
template<uint8_t Cpsm>
|
||||
void csaBanks()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
tex.cpsm = Cpsm;
|
||||
f.index(tex, 15);
|
||||
f.palette(tex, 15, Cpsm == GS_PSM_CT32 ? kRed : 0x801Fu);
|
||||
f.bind(tex);
|
||||
auto other = tex;
|
||||
other.cbp = 192;
|
||||
other.csa = Cpsm == GS_PSM_CT32 ? 15 : 31;
|
||||
f.palette(other, 15, Cpsm == GS_PSM_CT32 ? kGreen : 0x83E0u);
|
||||
f.bind(other);
|
||||
expectEqual(f.sample(), kGreen, "highest CSA bank is readable");
|
||||
tex.cld = 0;
|
||||
tex.csa = Cpsm == GS_PSM_CT32 ? 16 : 0;
|
||||
f.bind(tex);
|
||||
expectEqual(f.sample(), kRed, "partial CLUT load retains unrelated banks and masks CSA per CPSM");
|
||||
}
|
||||
|
||||
void texaWithoutReload()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
tex.cpsm = GS_PSM_CT16;
|
||||
f.index(tex, 0);
|
||||
f.index(tex, 1, 1);
|
||||
f.index(tex, 2, 2);
|
||||
f.palette(tex, 0, 0x001F);
|
||||
f.palette(tex, 1, 0x8000);
|
||||
f.palette(tex, 2, 0x0000);
|
||||
f.bind(tex);
|
||||
f.gs.writeRegister(GS_REG_TEXA, 0x20ull | (0x40ull << 32));
|
||||
expectEqual(f.sample(), 0x200000F8, "TA0 applied at lookup");
|
||||
expectEqual(f.sample(1), 0x40000000, "TA1 applied to CLUT alpha bit");
|
||||
f.gs.writeRegister(GS_REG_TEXA, 0x70ull | (1ull << 15) | (0x60ull << 32));
|
||||
expectEqual(f.sample(), 0x700000F8, "TEXA changes without reloading raw palette");
|
||||
expectEqual(f.sample(1), 0x60000000, "AEM does not clear black with alpha bit set");
|
||||
expectEqual(f.sample(2), 0, "AEM clears zero color with alpha bit clear");
|
||||
}
|
||||
|
||||
void paletteBeforeFiltering()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto tex = texture(GS_PSM_T4, 64);
|
||||
f.index(tex, 0, 0, 0);
|
||||
f.index(tex, 2, 1, 0);
|
||||
f.index(tex, 4, 0, 1);
|
||||
f.index(tex, 6, 1, 1);
|
||||
f.palette(tex, 0, kRed);
|
||||
f.palette(tex, 2, kGreen);
|
||||
f.palette(tex, 4, kBlue);
|
||||
f.palette(tex, 6, 0x80F8F8F8);
|
||||
f.palette(tex, 3, 0x80FF00FF);
|
||||
f.bind(tex);
|
||||
f.gs.writeRegister(GS_REG_TEX1_1, (1ull << 5) | (1ull << 6));
|
||||
expectEqual(f.sample(1, 1), 0x807C7C7C, "bilinear filtering blends four colors, never four indices");
|
||||
}
|
||||
|
||||
void highPlanes()
|
||||
{
|
||||
FrontendFixture f;
|
||||
auto low = texture(GS_PSM_T4HL, 31);
|
||||
auto high = low;
|
||||
high.psm = GS_PSM_T4HH;
|
||||
high.cbp = 192;
|
||||
high.csa = 1;
|
||||
f.gs.WriteVram(GS_PSM_CT32, 31, 2, 8, 0, 0x00ABCDEF);
|
||||
f.index(low, 3, 8);
|
||||
f.index(high, 12, 8);
|
||||
f.palette(low, 3, kRed);
|
||||
f.palette(high, 12, kGreen);
|
||||
f.bind(low);
|
||||
f.bind(high);
|
||||
low.cld = high.cld = 0;
|
||||
f.bind(low);
|
||||
expectEqual(f.sample(8), kRed, "low nibble uses its own CSA bank");
|
||||
f.bind(high);
|
||||
expectEqual(f.sample(8), kGreen, "same cached physical bytes supply the high nibble");
|
||||
expectEqual(f.gs.ReadVram(GS_PSM_CT24, 31, 2, 8, 0), 0xABCDEF, "index writes preserve the RGB plane");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
return run(argc, argv, {
|
||||
{"unaligned_csm1_ct32", unalignedCsm1<GS_PSM_CT32>},
|
||||
{"unaligned_csm1_ct16", unalignedCsm1<GS_PSM_CT16>},
|
||||
{"unaligned_csm1_ct16s", unalignedCsm1<GS_PSM_CT16S>},
|
||||
{"wrapped_clut", wrappedClut}, {"unaligned_csm2", unalignedCsm2},
|
||||
{"retained_palette", retainedPalette}, {"clut_uses_page_cache", clutUsesPageCache},
|
||||
{"cbp0_conditional", conditionalLoad<0>}, {"cbp1_conditional", conditionalLoad<1>},
|
||||
{"reserved_cld", reservedCld}, {"nonindexed_cld", nonIndexedCld},
|
||||
{"tex2_reload", tex2Reload}, {"shared_contexts", sharedContexts},
|
||||
{"csa_ct32", csaBanks<GS_PSM_CT32>}, {"csa_ct16", csaBanks<GS_PSM_CT16>},
|
||||
{"csa_ct16s", csaBanks<GS_PSM_CT16S>}, {"texa_without_reload", texaWithoutReload},
|
||||
{"palette_before_filtering", paletteBeforeFiltering}, {"high_planes", highPlanes}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
#include "gs_test_support.h"
|
||||
#include "runtime/gs/ps2_gs_memory.h"
|
||||
|
||||
using namespace GSTest;
|
||||
|
||||
namespace
|
||||
{
|
||||
template<uint8_t Psm>
|
||||
void addressCoverage()
|
||||
{
|
||||
BackendFixture f;
|
||||
GSMem::TexturePageCache cache;
|
||||
uint32_t random = 0x51375A9Du;
|
||||
for (auto& byte : f.vram)
|
||||
{
|
||||
random ^= random << 13;
|
||||
random ^= random >> 17;
|
||||
random ^= random << 5;
|
||||
byte = static_cast<uint8_t>(random);
|
||||
}
|
||||
constexpr auto mode = static_cast<GSMem::PixelStorageMode>(Psm);
|
||||
constexpr auto extent = GSMem::PixelStorageTraits<mode>::PageExtent();
|
||||
uint32_t checked = 0;
|
||||
const auto check = [&](uint32_t base, uint32_t bw, uint32_t x, uint32_t y)
|
||||
{
|
||||
const auto direct = f.backend.ReadVram(Psm, base, bw, x, y);
|
||||
const auto cached = GSMem::ReadTexture(cache, f.vram.data(), Psm, base, bw, x, y);
|
||||
if (cached != direct)
|
||||
{
|
||||
std::ostringstream error;
|
||||
error << "PSM=" << unsigned(Psm) << " BP=" << base << " BW=" << bw << " XY=" << x << ',' << y;
|
||||
expectEqual(cached, direct, error.str());
|
||||
}
|
||||
++checked;
|
||||
};
|
||||
// Every local texel, for every 256-byte base offset inside an 8 KiB page.
|
||||
for (uint32_t offset = 0; offset < 32; ++offset)
|
||||
for (uint32_t y = 0; y < extent.y; ++y)
|
||||
for (uint32_t x = 0; x < extent.x; ++x)
|
||||
check(32 + offset, 2, x, y);
|
||||
|
||||
for (uint32_t base : {0u, 31u, 32u, 12160u, 16256u, 16383u})
|
||||
for (uint32_t bw : {0u, 1u, 2u, 3u, 7u, 8u, 10u, 63u})
|
||||
for (uint32_t y : {0u, 1u, 7u, 8u, 15u, 16u, 31u, 32u, 63u, 64u, 127u, 128u, 255u, 256u, 511u, 512u, 1023u, 2047u})
|
||||
for (uint32_t x : {0u, 1u, 7u, 8u, 15u, 16u, 31u, 32u, 63u, 64u, 127u, 128u, 255u, 256u, 511u, 512u, 1023u, 2047u})
|
||||
check(base, bw, x, y);
|
||||
std::cout << checked << " cached/direct comparisons\n";
|
||||
}
|
||||
|
||||
void aliasLanes()
|
||||
{
|
||||
BackendFixture f;
|
||||
GSMem::TexturePageCache cache;
|
||||
constexpr uint32_t base = 31;
|
||||
const auto read = [&](uint32_t psm)
|
||||
{
|
||||
return GSMem::ReadTexture(cache, f.vram.data(), psm, base, 2, 8, 0);
|
||||
};
|
||||
f.backend.WriteVram(GS_PSM_CT32, base, 2, 8, 0, 0xAB123456);
|
||||
expectEqual(read(GS_PSM_T8H), 0xAB, "8H lane on a crossing page");
|
||||
f.backend.WriteVram(GS_PSM_CT24, base, 2, 8, 0, 0x654321);
|
||||
expectEqual(read(GS_PSM_CT32), 0xAB123456, "changing PSM does not refresh the physical page");
|
||||
cache.Invalidate();
|
||||
expectEqual(read(GS_PSM_CT32), 0xAB654321, "CT24 upload preserves alpha");
|
||||
f.backend.WriteVram(GS_PSM_T4HL, base, 2, 8, 0, 5);
|
||||
expectEqual(read(GS_PSM_T4HL), 11, "low nibble remains cached before flush");
|
||||
cache.Invalidate();
|
||||
expectEqual(read(GS_PSM_T4HL), 5, "low nibble after flush");
|
||||
expectEqual(read(GS_PSM_T4HH), 10, "high nibble preserved");
|
||||
expectEqual(read(GS_PSM_CT24), 0x654321, "RGB plane preserved");
|
||||
}
|
||||
|
||||
void physicalTagAliases()
|
||||
{
|
||||
BackendFixture f;
|
||||
GSMem::TexturePageCache cache;
|
||||
f.backend.WriteVram(GS_PSM_CT32, 32, 2, 0, 0, kRed);
|
||||
expectEqual(GSMem::ReadTexture(cache, f.vram.data(), GS_PSM_CT32, 32, 2, 0, 0), kRed, "prime aligned view");
|
||||
f.backend.WriteVram(GS_PSM_CT32, 32, 2, 0, 0, kGreen);
|
||||
// Both descriptors resolve to the very same physical byte, so this is a hit.
|
||||
expectEqual(GSMem::ReadTexture(cache, f.vram.data(), GS_PSM_CT32, 31, 2, 8, 0), kRed, "alias descriptor keeps the same cached bytes");
|
||||
cache.Invalidate();
|
||||
expectEqual(GSMem::ReadTexture(cache, f.vram.data(), GS_PSM_CT32, 31, 2, 8, 0), kGreen, "alias after flush");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
return run(argc, argv, {
|
||||
{"ct32", addressCoverage<GS_PSM_CT32>}, {"ct24", addressCoverage<GS_PSM_CT24>},
|
||||
{"ct16", addressCoverage<GS_PSM_CT16>}, {"ct16s", addressCoverage<GS_PSM_CT16S>},
|
||||
{"t8", addressCoverage<GS_PSM_T8>}, {"t4", addressCoverage<GS_PSM_T4>},
|
||||
{"t8h", addressCoverage<GS_PSM_T8H>}, {"t4hl", addressCoverage<GS_PSM_T4HL>},
|
||||
{"t4hh", addressCoverage<GS_PSM_T4HH>}, {"z32", addressCoverage<GS_PSM_Z32>},
|
||||
{"z24", addressCoverage<GS_PSM_Z24>}, {"z16", addressCoverage<GS_PSM_Z16>},
|
||||
{"z16s", addressCoverage<GS_PSM_Z16S>}, {"alias_lanes", aliasLanes},
|
||||
{"physical_tag_aliases", physicalTagAliases}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
#pragma once
|
||||
|
||||
#include "runtime/gs/gs_cpu_backend.h"
|
||||
#include "runtime/gs/gs_frontend.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace GSTest
|
||||
{
|
||||
constexpr uint32_t kVramSize = 4u * 1024u * 1024u;
|
||||
constexpr uint32_t kOutputPage = 200u;
|
||||
constexpr uint32_t kRed = 0x800000F8u;
|
||||
constexpr uint32_t kGreen = 0x8000F800u;
|
||||
constexpr uint32_t kBlue = 0x80F80000u;
|
||||
|
||||
inline void require(bool condition, std::string_view message)
|
||||
{
|
||||
if (!condition)
|
||||
throw std::runtime_error(std::string(message));
|
||||
}
|
||||
|
||||
inline void expectEqual(uint32_t actual, uint32_t expected, std::string_view message)
|
||||
{
|
||||
if (actual != expected)
|
||||
{
|
||||
std::ostringstream error;
|
||||
error << message << ": expected 0x" << std::hex << expected << ", got 0x" << actual;
|
||||
throw std::runtime_error(error.str());
|
||||
}
|
||||
}
|
||||
|
||||
struct Test
|
||||
{
|
||||
std::string_view name;
|
||||
void (*run)();
|
||||
};
|
||||
|
||||
inline int run(int argc, char** argv, std::initializer_list<Test> tests)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (argc != 2)
|
||||
throw std::invalid_argument("Pass one test name; run the complete suite with CTest.");
|
||||
for (const Test& test : tests)
|
||||
{
|
||||
if (test.name == argv[1])
|
||||
{
|
||||
test.run();
|
||||
std::cout << "PASS " << test.name << '\n';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
throw std::invalid_argument("Unknown test name: " + std::string(argv[1]));
|
||||
}
|
||||
catch (const std::exception& error)
|
||||
{
|
||||
std::cerr << "FAIL: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
inline GSTex0Reg texture(uint8_t psm = GS_PSM_CT32, uint32_t base = 32u)
|
||||
{
|
||||
GSTex0Reg tex{};
|
||||
tex.tbp0 = base;
|
||||
tex.tbw = 2;
|
||||
tex.psm = psm;
|
||||
tex.tw = tex.th = 8;
|
||||
tex.tcc = tex.tfx = 1;
|
||||
tex.cbp = 128;
|
||||
tex.cpsm = GS_PSM_CT32;
|
||||
tex.cld = 1;
|
||||
return tex;
|
||||
}
|
||||
|
||||
inline uint64_t encodeTex0(const GSTex0Reg& tex)
|
||||
{
|
||||
return uint64_t(tex.tbp0) | (uint64_t(tex.tbw) << 14) | (uint64_t(tex.psm) << 20) |
|
||||
(uint64_t(tex.tw) << 26) | (uint64_t(tex.th) << 30) | (uint64_t(tex.tcc) << 34) |
|
||||
(uint64_t(tex.tfx) << 35) | (uint64_t(tex.cbp) << 37) | (uint64_t(tex.cpsm) << 51) |
|
||||
(uint64_t(tex.csm) << 55) | (uint64_t(tex.csa) << 56) | (uint64_t(tex.cld) << 61);
|
||||
}
|
||||
|
||||
inline GSPrimitiveBatch sprite(const GSTex0Reg& tex, uint32_t x, uint32_t y, bool linear = false)
|
||||
{
|
||||
GSPrimitiveBatch batch{};
|
||||
batch.vertexCount = 2;
|
||||
auto& state = batch.state;
|
||||
state.prim.type = GS_PRIM_SPRITE;
|
||||
state.prim.tme = state.prim.fst = true;
|
||||
state.context.frame.fbp = kOutputPage;
|
||||
state.context.frame.fbw = 1;
|
||||
state.context.zbuf.zmask = true;
|
||||
state.context.test = 1ull << 17;
|
||||
state.context.tex0 = tex;
|
||||
state.context.clamp = 5; // Clamp both axes.
|
||||
state.textureWidth = state.textureHeight = 256;
|
||||
state.texa.ta0 = state.texa.ta1 = 128;
|
||||
state.linearFilter = linear;
|
||||
for (auto& vertex : batch.vertices)
|
||||
{
|
||||
vertex.r = vertex.g = vertex.b = vertex.a = 128;
|
||||
vertex.u = static_cast<uint16_t>(x * 16u);
|
||||
vertex.v = static_cast<uint16_t>(y * 16u);
|
||||
}
|
||||
batch.vertices[1].x = batch.vertices[1].y = 1;
|
||||
return batch;
|
||||
}
|
||||
|
||||
struct BackendFixture
|
||||
{
|
||||
std::vector<uint8_t> vram = std::vector<uint8_t>(kVramSize);
|
||||
GSCpuBackend backend;
|
||||
|
||||
BackendFixture()
|
||||
{
|
||||
backend.Initialize(vram.data(), static_cast<uint32_t>(vram.size()));
|
||||
}
|
||||
|
||||
uint32_t sample(const GSTex0Reg& tex, uint32_t x = 0, uint32_t y = 0, bool linear = false)
|
||||
{
|
||||
backend.Submit(sprite(tex, x, y, linear));
|
||||
return backend.ReadVram(GS_PSM_CT32, kOutputPage * 32u, 1u, 0u, 0u);
|
||||
}
|
||||
};
|
||||
|
||||
struct FrontendFixture
|
||||
{
|
||||
std::vector<uint8_t> vram = std::vector<uint8_t>(kVramSize);
|
||||
GS gs;
|
||||
|
||||
FrontendFixture()
|
||||
{
|
||||
gs.init(vram.data(), static_cast<uint32_t>(vram.size()), nullptr);
|
||||
for (uint8_t context = 0; context < 2; ++context)
|
||||
{
|
||||
gs.writeRegister(context ? GS_REG_FRAME_2 : GS_REG_FRAME_1, kOutputPage | (1ull << 16));
|
||||
gs.writeRegister(context ? GS_REG_ZBUF_2 : GS_REG_ZBUF_1, 1ull << 32);
|
||||
gs.writeRegister(context ? GS_REG_SCISSOR_2 : GS_REG_SCISSOR_1, 0);
|
||||
gs.writeRegister(context ? GS_REG_TEST_2 : GS_REG_TEST_1, 0x30000);
|
||||
gs.writeRegister(context ? GS_REG_CLAMP_2 : GS_REG_CLAMP_1, 5);
|
||||
}
|
||||
gs.writeRegister(GS_REG_TEXA, 128ull | (128ull << 32));
|
||||
}
|
||||
|
||||
void bind(const GSTex0Reg& tex, uint8_t context = 0, bool tex2 = false)
|
||||
{
|
||||
const uint8_t reg = tex2 ? (context ? GS_REG_TEX2_2 : GS_REG_TEX2_1)
|
||||
: (context ? GS_REG_TEX0_2 : GS_REG_TEX0_1);
|
||||
gs.writeRegister(reg, encodeTex0(tex));
|
||||
}
|
||||
|
||||
void flush()
|
||||
{
|
||||
gs.writeRegister(GS_REG_TEXFLUSH, 0);
|
||||
}
|
||||
|
||||
void index(const GSTex0Reg& tex, uint32_t value, uint32_t x = 0, uint32_t y = 0)
|
||||
{
|
||||
gs.WriteVram(tex.psm, tex.tbp0, tex.tbw, x, y, value);
|
||||
}
|
||||
|
||||
void palette(const GSTex0Reg& tex, uint32_t entry, uint32_t value)
|
||||
{
|
||||
// CSM1 source layout; CSA selects the destination, not this source.
|
||||
const uint32_t position = (entry & ~0x18u) | ((entry & 8u) << 1u) | ((entry & 16u) >> 1u);
|
||||
gs.WriteVram(tex.cpsm, tex.cbp, 1, position & 15u, position >> 4u, value);
|
||||
}
|
||||
|
||||
uint32_t sample(uint32_t x = 0, uint32_t y = 0, uint8_t context = 0)
|
||||
{
|
||||
gs.writeRegister(GS_REG_PRIM, GS_PRIM_SPRITE | (1ull << 4) | (1ull << 8) | (uint64_t(context) << 9));
|
||||
gs.writeRegister(GS_REG_RGBAQ, 0x80808080);
|
||||
const uint64_t uv = (uint64_t(y * 16u) << 16) | uint64_t(x * 16u);
|
||||
gs.writeRegister(GS_REG_UV, uv);
|
||||
gs.writeRegister(GS_REG_XYZ2, 0);
|
||||
gs.writeRegister(GS_REG_UV, uv);
|
||||
gs.writeRegister(GS_REG_XYZ2, 16ull | (16ull << 16));
|
||||
return gs.ReadVram(GS_PSM_CT32, kOutputPage * 32u, 1, 0, 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "gs_test_support.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
using namespace GSTest;
|
||||
|
||||
namespace
|
||||
{
|
||||
void unalignedTexture()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture(GS_PSM_CT32, 31);
|
||||
// TBP=31, CT32(8,0): block 31 + swizzled block 1 = physical page 1.
|
||||
std::memcpy(f.vram.data() + 8192u, &kRed, sizeof(kRed));
|
||||
expectEqual(f.sample(tex, 8), kRed, "non-page-aligned texture base");
|
||||
}
|
||||
|
||||
void unalignedWrap()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture(GS_PSM_CT32, 16383);
|
||||
std::memcpy(f.vram.data(), &kGreen, sizeof(kGreen));
|
||||
expectEqual(f.sample(tex, 8), kGreen, "swizzle carry wraps through the 4 MiB boundary");
|
||||
}
|
||||
|
||||
void staleMirror()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture();
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed);
|
||||
expectEqual(f.sample(tex), kRed, "prime the following physical page");
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kGreen);
|
||||
f.backend.TextureFlush();
|
||||
tex.tbp0 = 31;
|
||||
expectEqual(f.sample(tex, 8), kGreen, "TEXFLUSH must not expose stale bytes in the old mirror");
|
||||
}
|
||||
|
||||
void pageAlternation()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture(GS_PSM_CT32, 31);
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed);
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 8, 0, kGreen);
|
||||
for (unsigned i = 0; i < 8; ++i)
|
||||
{
|
||||
expectEqual(f.sample(tex), kRed, "first physical page");
|
||||
expectEqual(f.sample(tex, 8), kGreen, "second physical page in the same logical page");
|
||||
}
|
||||
}
|
||||
|
||||
void flushVisibility()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture();
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed);
|
||||
expectEqual(f.sample(tex), kRed, "initial cache fill");
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kGreen);
|
||||
expectEqual(f.backend.ReadVram(tex.psm, tex.tbp0, tex.tbw, 0, 0), kGreen, "canonical VRAM changes immediately");
|
||||
f.backend.Flush();
|
||||
f.backend.Sync(GSSyncReason::Finish);
|
||||
expectEqual(f.sample(tex), kRed, "ordinary flush and FINISH do not invalidate texels");
|
||||
f.backend.TextureFlush();
|
||||
expectEqual(f.sample(tex), kGreen, "TEXFLUSH exposes the updated texels");
|
||||
}
|
||||
|
||||
void uploadVisibility()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture();
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed);
|
||||
expectEqual(f.sample(tex), kRed, "prime destination");
|
||||
GSTransferCommand transfer{};
|
||||
transfer.direction = 0;
|
||||
transfer.bitbltbuf.dbp = tex.tbp0;
|
||||
transfer.bitbltbuf.dbw = tex.tbw;
|
||||
transfer.bitbltbuf.dpsm = tex.psm;
|
||||
transfer.trxreg.rrw = transfer.trxreg.rrh = 1;
|
||||
f.backend.BeginTransfer(transfer);
|
||||
f.backend.UploadImage(reinterpret_cast<const uint8_t*>(&kGreen), sizeof(kGreen));
|
||||
expectEqual(f.sample(tex), kRed, "host upload does not implicitly flush texels");
|
||||
f.backend.TextureFlush();
|
||||
expectEqual(f.sample(tex), kGreen, "host upload visible after TEXFLUSH");
|
||||
}
|
||||
|
||||
void localCopyVisibility()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture();
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed);
|
||||
f.backend.WriteVram(tex.psm, 96, tex.tbw, 0, 0, kGreen);
|
||||
expectEqual(f.sample(tex), kRed, "prime destination");
|
||||
GSTransferCommand transfer{};
|
||||
transfer.direction = 2;
|
||||
transfer.bitbltbuf.sbp = 96;
|
||||
transfer.bitbltbuf.sbw = transfer.bitbltbuf.dbw = tex.tbw;
|
||||
transfer.bitbltbuf.spsm = transfer.bitbltbuf.dpsm = tex.psm;
|
||||
transfer.bitbltbuf.dbp = tex.tbp0;
|
||||
transfer.trxreg.rrw = transfer.trxreg.rrh = 1;
|
||||
f.backend.BeginTransfer(transfer);
|
||||
expectEqual(f.sample(tex), kRed, "local copy does not implicitly flush texels");
|
||||
f.backend.TextureFlush();
|
||||
expectEqual(f.sample(tex), kGreen, "local copy visible after TEXFLUSH");
|
||||
}
|
||||
|
||||
void rasterVisibility()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture();
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed);
|
||||
expectEqual(f.sample(tex), kRed, "prime render target as texture");
|
||||
auto batch = sprite(tex, 0, 0);
|
||||
batch.state.prim.tme = false;
|
||||
batch.state.context.frame.fbp = tex.tbp0 / 32;
|
||||
for (auto& vertex : batch.vertices)
|
||||
{
|
||||
vertex.r = 0;
|
||||
vertex.g = 248;
|
||||
vertex.b = 0;
|
||||
}
|
||||
f.backend.Submit(batch);
|
||||
expectEqual(f.sample(tex), kRed, "raster writes do not implicitly flush texels");
|
||||
f.backend.TextureFlush();
|
||||
expectEqual(f.sample(tex), kGreen, "render-to-texture visible after TEXFLUSH");
|
||||
}
|
||||
|
||||
void resetAndRebind()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture();
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed);
|
||||
expectEqual(f.sample(tex), kRed, "prime cache");
|
||||
f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kGreen);
|
||||
f.backend.Reset();
|
||||
expectEqual(f.sample(tex), kGreen, "reset invalidates without clearing VRAM");
|
||||
std::vector<uint8_t> other(kVramSize);
|
||||
std::memcpy(other.data() + 8192u, &kBlue, sizeof(kBlue));
|
||||
f.backend.Initialize(other.data(), static_cast<uint32_t>(other.size()));
|
||||
expectEqual(f.sample(tex), kBlue, "initialize invalidates the previous VRAM allocation");
|
||||
}
|
||||
|
||||
void invalidVramSize()
|
||||
{
|
||||
BackendFixture f;
|
||||
std::vector<uint8_t> shortVram(8192);
|
||||
bool rejected = false;
|
||||
try { f.backend.Initialize(shortVram.data(), static_cast<uint32_t>(shortVram.size())); }
|
||||
catch (const std::invalid_argument&) { rejected = true; }
|
||||
require(rejected, "undersized VRAM must be rejected before masked accesses can escape it");
|
||||
f.backend.WriteVram(GS_PSM_CT32, 32, 2, 0, 0, kGreen);
|
||||
expectEqual(f.sample(texture()), kGreen, "failed initialize preserves the existing backend binding");
|
||||
f.backend.Initialize(nullptr, 0);
|
||||
expectEqual(f.backend.ReadVram(GS_PSM_CT32, 32, 2, 0, 0), 0, "null binding is safe");
|
||||
}
|
||||
|
||||
void reservedPsm()
|
||||
{
|
||||
BackendFixture f;
|
||||
auto tex = texture(0x3F);
|
||||
f.backend.WriteVram(GS_PSM_CT32, 32, 2, 0, 0, kGreen);
|
||||
f.backend.WriteVram(0x3F, 32, 2, 0, 0, kRed);
|
||||
expectEqual(f.backend.ReadVram(GS_PSM_CT32, 32, 2, 0, 0), kGreen, "reserved writes are no-op");
|
||||
expectEqual(f.backend.ReadVram(0x3F, 32, 2, 0, 0), 0, "reserved raw reads use null semantics");
|
||||
expectEqual(f.sample(tex), 0xFFFF00FFu, "reserved sampling preserves the existing magenta diagnostic");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
return run(argc, argv, {
|
||||
{"unaligned_texture", unalignedTexture}, {"unaligned_wrap", unalignedWrap},
|
||||
{"stale_mirror", staleMirror}, {"page_alternation", pageAlternation},
|
||||
{"flush_visibility", flushVisibility}, {"upload_visibility", uploadVisibility},
|
||||
{"local_copy_visibility", localCopyVisibility}, {"raster_visibility", rasterVisibility},
|
||||
{"reset_and_rebind", resetAndRebind}, {"invalid_vram_size", invalidVramSize},
|
||||
{"reserved_psm", reservedPsm}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user