mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Share validated Neversoft archive and renderer extensions
This commit is contained in:
@@ -897,6 +897,11 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME mod_runtime_test COMMAND mod_runtime_test)
|
||||
|
||||
add_executable(neversoft_helpers_test tests/test_neversoft_helpers.cpp)
|
||||
target_include_directories(neversoft_helpers_test PRIVATE include)
|
||||
target_compile_features(neversoft_helpers_test PRIVATE cxx_std_17)
|
||||
add_test(NAME neversoft_helpers_test COMMAND neversoft_helpers_test)
|
||||
|
||||
add_executable(mod_resident_test
|
||||
tests/test_mod_resident.cpp
|
||||
src/mod_resident.cpp
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
// Neversoft's HED format: NUL-terminated name, word padding, offset/size,
|
||||
// terminated by 0xff. Only the archive format is shared; title lookup and
|
||||
// request/completion contracts remain with the game.
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
class PSXNamedWadIndex {
|
||||
std::unordered_map<uint32_t, uint32_t> extents;
|
||||
static uint32_t word(const uint8_t* p) {
|
||||
return uint32_t(p[0]) | uint32_t(p[1]) << 8 |
|
||||
uint32_t(p[2]) << 16 | uint32_t(p[3]) << 24;
|
||||
}
|
||||
public:
|
||||
bool assign(const uint8_t* hed, uint32_t size, uint32_t wad_size) {
|
||||
extents.clear();
|
||||
if (!hed) return false;
|
||||
uint32_t cursor = 0;
|
||||
while (cursor < size && hed[cursor] != 0xff) {
|
||||
uint32_t end = cursor;
|
||||
while (end < size && hed[end]) ++end;
|
||||
if (end == size || end == cursor) { extents.clear(); return false; }
|
||||
const uint64_t fields = (uint64_t(end) + 4) & ~uint64_t(3);
|
||||
if (fields > size || size - fields < 8) { extents.clear(); return false; }
|
||||
const uint32_t offset = word(hed + fields), bytes = word(hed + fields + 4);
|
||||
const uint64_t padded = (uint64_t(bytes) + 2047) & ~uint64_t(2047);
|
||||
if ((offset & 2047) || offset > wad_size || padded > wad_size - offset) {
|
||||
extents.clear(); return false;
|
||||
}
|
||||
const auto found = extents.find(offset);
|
||||
if (found != extents.end() && found->second != bytes) {
|
||||
extents.clear(); return false;
|
||||
}
|
||||
extents[offset] = bytes;
|
||||
cursor = uint32_t(fields + 8);
|
||||
}
|
||||
if (cursor >= size || hed[cursor] != 0xff || extents.empty()) {
|
||||
extents.clear(); return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool matches(uint32_t offset, uint32_t padded_bytes, uint32_t sectors) const {
|
||||
const auto found = extents.find(offset);
|
||||
return found != extents.end() && padded_bytes != 0 &&
|
||||
uint64_t(padded_bytes) == ((uint64_t(found->second) + 2047) & ~uint64_t(2047)) &&
|
||||
sectors == padded_bytes / 2048;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
// Shared Neversoft renderer extensions. Layout supplies retail code guards,
|
||||
// globals, mod identifiers and counter names; this never changes simulation.
|
||||
#include "mod_plugins.h"
|
||||
#include "cpu_state.h"
|
||||
#include "pgxp.h"
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
struct PSXNeversoftPacketLengthSite { uint32_t address, word; };
|
||||
|
||||
template<class Layout> struct PSXNeversoftRenderer {
|
||||
static constexpr uint32_t ArenaSize = 0x60000u, StockSize = 0x17000u;
|
||||
static constexpr uint32_t MaxFar = 16380u;
|
||||
inline static uint32_t arena = 0, peak = 0, packet_corrections = 0;
|
||||
inline static bool extended = true;
|
||||
|
||||
static bool boot_contract() {
|
||||
return psx_mod_read_word(Layout::AllocA) == Layout::AllocWord &&
|
||||
psx_mod_read_word(Layout::AllocB) == Layout::AllocWord &&
|
||||
psx_mod_read_word(Layout::LimitPc + 4) == 0x34846F00u;
|
||||
}
|
||||
static void subdivision_precision(CPUState* cpu, uint32_t) {
|
||||
// Runs after the original scratchpad SXY store, before RGB2 transfer.
|
||||
const uint32_t address = cpu->gpr[5];
|
||||
if (address < 0x1F800000u || address > 0x1F8003FCu || (address & 3u) ||
|
||||
psx_mod_read_word(Layout::PrecisionPc - 4) != 0xACB00000u) return;
|
||||
const uint32_t stored = psx_mod_read_word(address);
|
||||
if (stored != cpu->gpr[16]) return;
|
||||
if (pgxp_store_flagged_gte_sxy(address, 14, cpu->gte_data[14], stored))
|
||||
psx_mod_counter_add(Layout::PrecisionCounter, 1);
|
||||
}
|
||||
static void packet_length(CPUState* cpu, uint32_t reg) {
|
||||
if (!arena) return;
|
||||
const uint32_t address = cpu->gpr[reg] & 0x00FFFFFFu;
|
||||
const uint32_t base = arena & 0x00FFFFFFu;
|
||||
if (address < base || address >= base + 2 * ArenaSize) return;
|
||||
// The stored DMA tag retains its link; only the subsequent byte-count
|
||||
// calculation excludes the expanded aperture's address bits 22/23.
|
||||
cpu->gpr[reg] &= 0xFF000000u;
|
||||
++packet_corrections;
|
||||
}
|
||||
static void packet_length_t2(CPUState* cpu, uint32_t) { packet_length(cpu, 10); }
|
||||
static void packet_length_s2(CPUState* cpu, uint32_t) { packet_length(cpu, 18); }
|
||||
static void fog(CPUState* cpu, uint32_t) {
|
||||
const int32_t start = int32_t(cpu->gpr[5]), range = int32_t(cpu->gpr[6]);
|
||||
if (!extended || start < 0 || range <= 0 || uint64_t(start) + range >= MaxFar ||
|
||||
psx_mod_read_word(Layout::FogPc) != 0x00803821u) return;
|
||||
uint32_t span = 1;
|
||||
while (span < uint32_t(range) && span < 16384u) span <<= 1;
|
||||
if (span >= 16384u) return;
|
||||
if (span < 8192u) span <<= 1;
|
||||
uint32_t begin = uint32_t(start) * 2u;
|
||||
if (begin > MaxFar - span) begin = MaxFar - span;
|
||||
if (begin < uint32_t(start) || begin + span < uint32_t(start) + uint32_t(range)) return;
|
||||
cpu->gpr[5] = begin;
|
||||
cpu->gpr[6] = span;
|
||||
psx_mod_counter_add(Layout::FogCounter, 1);
|
||||
}
|
||||
static void swap(CPUState*, uint32_t) {
|
||||
if (!arena || !boot_contract()) return;
|
||||
const uint32_t a = psx_mod_read_word(Layout::BufferA + 0x74);
|
||||
const uint32_t b = psx_mod_read_word(Layout::BufferB + 0x74);
|
||||
if (a == arena && b == arena + ArenaSize) return;
|
||||
if (a < 0x80010000u || a > 0x80200000u - StockSize ||
|
||||
b < 0x80010000u || b > 0x80200000u - StockSize) return;
|
||||
for (const auto& site : Layout::PacketLengthSites)
|
||||
if (psx_mod_read_word(site.address) != site.word) return;
|
||||
psx_mod_write_word(Layout::BufferA + 0x74, arena);
|
||||
psx_mod_write_word(Layout::BufferB + 0x74, arena + ArenaSize);
|
||||
psx_mod_counter_add(Layout::ArenaCounter, 1);
|
||||
}
|
||||
static int limit(CPUState* cpu, uint32_t) {
|
||||
if (!arena || !boot_contract()) return 0;
|
||||
const uint32_t cur = psx_mod_read_word(Layout::Current);
|
||||
if (cur != Layout::BufferA && cur != Layout::BufferB) return 0;
|
||||
const uint32_t base = psx_mod_read_word(cur + 0x74);
|
||||
if (base != arena && base != arena + ArenaSize) return 0;
|
||||
const uint32_t end = (base + ArenaSize - 0x100u) & 0x7FFFFFFFu;
|
||||
psx_mod_write_word(Layout::Limit, end);
|
||||
cpu->gpr[4] = ArenaSize - 0x100u;
|
||||
cpu->gpr[5] = cur;
|
||||
cpu->gpr[3] = 0x7FFFFFFFu;
|
||||
cpu->gpr[2] = end;
|
||||
return 1;
|
||||
}
|
||||
static void submit(CPUState*, uint32_t) {
|
||||
if (packet_corrections) {
|
||||
psx_mod_counter_add(Layout::PacketCounter, packet_corrections);
|
||||
packet_corrections = 0;
|
||||
}
|
||||
const uint32_t cur = psx_mod_read_word(Layout::Current);
|
||||
if (cur != Layout::BufferA && cur != Layout::BufferB) return;
|
||||
const uint32_t base = psx_mod_read_word(cur + 0x74);
|
||||
const uint32_t used = (psx_mod_read_word(Layout::Cursor) & 0x7FFFFFFFu) -
|
||||
(base & 0x7FFFFFFFu);
|
||||
const uint32_t size = arena && (base == arena || base == arena + ArenaSize) ? ArenaSize : StockSize;
|
||||
if (used > size) return;
|
||||
if (used + 0x200 >= size) psx_mod_counter_add(Layout::FullCounter, 1);
|
||||
if (used > peak) {
|
||||
psx_mod_counter_add(Layout::PeakCounter, used - peak);
|
||||
peak = used;
|
||||
}
|
||||
}
|
||||
static void activate() {
|
||||
char value[16] = "";
|
||||
extended = !psx_mod_option_value(Layout::Package, "draw-distance", "distance", value, sizeof value) ||
|
||||
std::strcmp(value, "original") != 0;
|
||||
arena = extended ? psx_mod_alloc_gpu_dma_memory(2 * ArenaSize, 8) : 0;
|
||||
peak = 0;
|
||||
packet_corrections = 0;
|
||||
}
|
||||
static void register_plugins() {
|
||||
psx_mod_register_activation_plugin(Layout::Plugin, activate);
|
||||
psx_mod_register_function_entry_plugin(Layout::Plugin, Layout::FogPc, fog);
|
||||
psx_mod_register_function_entry_plugin(Layout::Plugin, Layout::SwapPc, swap);
|
||||
psx_mod_register_function_filter_plugin(Layout::Plugin, Layout::LimitPc, limit);
|
||||
psx_mod_register_function_entry_plugin(Layout::Plugin, Layout::SubmitPc, submit);
|
||||
for (const auto& site : Layout::PacketLengthSites)
|
||||
psx_mod_register_instruction_plugin(Layout::Plugin, site.address, site.word,
|
||||
site.word == 0x000A5582u ? packet_length_t2 : packet_length_s2);
|
||||
psx_mod_register_instruction_plugin(Layout::PrecisionPlugin, Layout::PrecisionPc,
|
||||
0x4819B000u, subdivision_precision);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
// Focused host-only checks: no game launch, disc scan or renderer required.
|
||||
#include "mod_named_wad.hpp"
|
||||
#include "mod_neversoft_renderer.hpp"
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
struct TestLayout {
|
||||
static constexpr const char* Plugin = "test.draw", *PrecisionPlugin = "test.pgxp", *Package = "test.package";
|
||||
static constexpr const char* PrecisionCounter = "test.precision", *FogCounter = "test.fog", *ArenaCounter = "test.arena";
|
||||
static constexpr const char* PacketCounter = "test.packet", *FullCounter = "test.full", *PeakCounter = "test.peak";
|
||||
static constexpr uint32_t BufferA = 0x800A707C, BufferB = BufferA + 0x78;
|
||||
static constexpr uint32_t Current = 0x800C247C, Cursor = 0x800C2484, Limit = 0x800C2034;
|
||||
static constexpr uint32_t AllocA = 0x8006BDD8, AllocB = 0x8006BDF0, AllocWord = 0x0C01C0CB;
|
||||
static constexpr uint32_t FogPc = 0x80081178, SwapPc = 0x8006BE18, LimitPc = 0x800314E4;
|
||||
static constexpr uint32_t SubmitPc = 0x8006BE68, PrecisionPc = 0x80089A98;
|
||||
static constexpr PSXNeversoftPacketLengthSite PacketLengthSites[] = {
|
||||
{0x800891C0, 0x000A5582}, {0x8008A128, 0x00129582},
|
||||
};
|
||||
};
|
||||
using R = PSXNeversoftRenderer<TestLayout>;
|
||||
static bool loaded = false, reject_packet_code = false;
|
||||
static uint32_t a = 0x800D0000, b = 0x800E7000, cursor = 0, limit = 0;
|
||||
static unsigned registered = 0;
|
||||
extern "C" {
|
||||
int psx_mod_register_activation_plugin(const char*, PSXModActivationCallback) { return 1; }
|
||||
int psx_mod_register_function_entry_plugin(const char*, uint32_t, PSXModFunctionEntryCallback) { return 1; }
|
||||
int psx_mod_register_function_filter_plugin(const char*, uint32_t, PSXModFunctionFilterCallback) { return 1; }
|
||||
int psx_mod_register_instruction_plugin(const char*, uint32_t, uint32_t, PSXModFunctionEntryCallback) { ++registered; return 1; }
|
||||
int pgxp_store_flagged_gte_sxy(uint32_t, uint8_t, uint32_t, uint32_t) { return 0; }
|
||||
int psx_mod_option_value(const char*, const char*, const char*, char*, uint32_t) { return 0; }
|
||||
uint32_t psx_mod_alloc_gpu_dma_memory(uint32_t bytes, uint32_t alignment) {
|
||||
assert(bytes == 2 * R::ArenaSize && alignment == 8); return 0x80800000;
|
||||
}
|
||||
uint32_t psx_mod_read_word(uint32_t address) {
|
||||
if (address == TestLayout::BufferA + 0x74) return a;
|
||||
if (address == TestLayout::BufferB + 0x74) return b;
|
||||
if (address == TestLayout::Current) return TestLayout::BufferA;
|
||||
if (address == TestLayout::Cursor) return cursor;
|
||||
if (!loaded) return 0;
|
||||
if (address == TestLayout::AllocA || address == TestLayout::AllocB) return TestLayout::AllocWord;
|
||||
if (address == TestLayout::LimitPc + 4) return 0x34846F00;
|
||||
if (address == TestLayout::FogPc) return 0x00803821;
|
||||
for (const auto& site : TestLayout::PacketLengthSites)
|
||||
if (address == site.address) return reject_packet_code ? 0 : site.word;
|
||||
return 0;
|
||||
}
|
||||
void psx_mod_write_word(uint32_t address, uint32_t value) {
|
||||
if (address == TestLayout::BufferA + 0x74) a = value;
|
||||
else if (address == TestLayout::BufferB + 0x74) b = value;
|
||||
else if (address == TestLayout::Limit) limit = value;
|
||||
else assert(false);
|
||||
}
|
||||
void psx_mod_counter_add(const char*, uint32_t) {}
|
||||
}
|
||||
int main() {
|
||||
PSXNamedWadIndex index;
|
||||
std::vector<uint8_t> hed = {'a',0,0,0, 0,0,0,0, 1,0,0,0, 0xff};
|
||||
assert(index.assign(hed.data(), uint32_t(hed.size()), 2048));
|
||||
assert(index.matches(0,2048,1));
|
||||
assert(!index.matches(0,1,1) && !index.matches(0,2048,2) && !index.matches(2048,2048,1));
|
||||
assert(!index.assign(hed.data(), uint32_t(hed.size()), 1));
|
||||
assert(!index.matches(0,2048,1)); // failed assignment clears prior catalog
|
||||
assert(!index.assign(hed.data(),12,2048)); // absent sentinel
|
||||
hed[4] = 1;
|
||||
assert(!index.assign(hed.data(),13,4096)); // unaligned offset
|
||||
hed[4] = 0; hed[8] = hed[9] = hed[10] = hed[11] = 0xff;
|
||||
assert(!index.assign(hed.data(),13,UINT32_MAX)); // padded-size overflow
|
||||
hed[8] = 1; hed[9] = hed[10] = hed[11] = 0;
|
||||
hed.pop_back();
|
||||
hed.insert(hed.end(), {'b',0,0,0, 0,0,0,0, 2,0,0,0, 0xff});
|
||||
assert(!index.assign(hed.data(),25,2048)); // conflicting duplicate
|
||||
hed[20] = 1;
|
||||
assert(index.assign(hed.data(),25,2048)); // aliases with identical extent
|
||||
R::register_plugins(); assert(registered == 3);
|
||||
R::activate(); R::swap(nullptr,0);
|
||||
assert(a == 0x800D0000 && b == 0x800E7000);
|
||||
loaded = true; reject_packet_code = true; R::swap(nullptr,0);
|
||||
assert(a == 0x800D0000 && b == 0x800E7000);
|
||||
reject_packet_code = false; R::swap(nullptr,0);
|
||||
assert(a == R::arena && b == R::arena + R::ArenaSize);
|
||||
for (unsigned reg : {10u,18u}) for (unsigned length = 0; length < 256; ++length)
|
||||
for (uint32_t address : {0x00800000u,0x00860000u,0x008BFFFCu}) {
|
||||
CPUState cpu{};
|
||||
for (unsigned i=0; i<32; ++i) cpu.gpr[i] = 0x13570000 + i;
|
||||
cpu.pc = 0x800891C0;
|
||||
cpu.gpr[reg] = length << 24 | address;
|
||||
R::packet_length(&cpu,reg);
|
||||
assert((cpu.gpr[reg] >> 22) + 4 == 4 * (length + 1));
|
||||
assert(cpu.pc == 0x800891C0);
|
||||
for (unsigned i=0; i<32; ++i) if (i != reg) assert(cpu.gpr[i] == 0x13570000 + i);
|
||||
}
|
||||
CPUState cpu{}; cpu.gpr[10] = 0x090D0000;
|
||||
R::packet_length_t2(&cpu,0); assert(cpu.gpr[10] == 0x090D0000);
|
||||
assert(R::limit(&cpu,0) == 1 && limit == 0x0085FF00 && cpu.gpr[2] == limit);
|
||||
cursor = R::arena + 1232; R::submit(nullptr,0); assert(R::peak == 1232);
|
||||
for (int32_t range : {1,128,2048,4096,8192,8193,16379}) {
|
||||
cpu.gpr[5] = 0; cpu.gpr[6] = range; R::fog(&cpu,0);
|
||||
assert(cpu.gpr[5] + cpu.gpr[6] <= R::MaxFar);
|
||||
assert(cpu.gpr[5] + cpu.gpr[6] >= uint32_t(range));
|
||||
}
|
||||
cpu.gpr[5] = 6000; cpu.gpr[6] = 2048; R::fog(&cpu,0);
|
||||
assert(cpu.gpr[5] == 12000 && cpu.gpr[6] == 4096);
|
||||
R::arena = 0; cpu.gpr[10] = 0x09805978;
|
||||
R::packet_length_t2(&cpu,0); assert(cpu.gpr[10] == 0x09805978);
|
||||
std::puts("PASS: shared named archive bounds, packet ABI, code guards, primitive capacity and fog ceiling");
|
||||
}
|
||||
Reference in New Issue
Block a user