fix: fix texture caching

feat: wip multi version on dbcman
This commit is contained in:
Ran-j
2026-09-14 02:11:06 -03:00
parent 590b884ba4
commit 78ecbae377
25 changed files with 1778 additions and 117 deletions
+10
View File
@@ -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
+24 -9
View File
@@ -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)
+5 -2
View File
@@ -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;
};
+11 -2
View File
@@ -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;
+3 -2
View File
@@ -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;
+3
View File
@@ -30,9 +30,12 @@ namespace ps2x::iop::detail
"vblank",
"secrman",
"sio2man",
"xsio2man",
"sio2d",
"padman",
"xpadman",
"mcman",
"xmcman",
"mcserv",
"libsd",
"cdvdman",
+29
View File
@@ -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);
+25 -6
View File
@@ -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;
};
}
+15 -40
View File
@@ -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, &parameter, 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 &parameter)
int32_t handleNameOperation(Operation operation, uint32_t sendAddress, const NameParameter &parameter)
{
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),
+22
View File
@@ -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);
}
}
+237
View File
@@ -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;
}
}
+315
View File
@@ -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);
}
+170
View File
@@ -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;
};
}
+10 -5
View File
@@ -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);
+9 -46
View File
@@ -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)
+35
View File
@@ -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);
+1
View File
@@ -15,6 +15,7 @@
#include "ps2x/iop/iop_subsystem.h"
#include <iostream>
#include <stdexcept>
#include <fstream>
#include <algorithm>
#include <array>
+5
View File
@@ -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
+59
View File
@@ -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()
+283
View File
@@ -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}
});
}
+190
View File
@@ -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}
});
}