Skip to content

Commit

Permalink
Refactoring / fixing / documenting ps1-packer.
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasnoble committed Nov 16, 2023
1 parent a072e38 commit 0a4c06f
Showing 1 changed file with 156 additions and 34 deletions.
190 changes: 156 additions & 34 deletions src/supportpsx/ps1-packer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,44 @@ SOFTWARE.

#include "supportpsx/ps1-packer.h"

#include <assert.h>
#include <stdint.h>

#include <exception>
#include <vector>

#include "mips/common/util/encoder.hh"
#include "n2e-d.h"
#include "support/polyfills.h"
#include "ucl/ucl.h"

using namespace Mips::Encoder;
constexpr uint64_t PSEXE = 0x45584520582d5350;

namespace {

template <typename T>
template <PCSX::PolyFill::IntegralConcept T>
void pushBytes(std::vector<uint8_t>& data, T value) {
for (unsigned i = 0; i < sizeof(T); i++) {
data.push_back(value & 0xff);
value >>= 8;
}
}

template <PCSX::PolyFill::IntegralConcept T, size_t S>
void pushBytes(std::vector<uint8_t>& data, const T (&a)[S]) {
for (unsigned i = 0; i < S; i++) {
pushBytes(data, a[i]);

Check warning on line 56 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L54-L56

Added lines #L54 - L56 were not covered by tests
}
}

Check warning on line 58 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L58

Added line #L58 was not covered by tests

template <PCSX::PolyFill::IntegralConcept T>
void pushBytes(std::vector<uint8_t>& data, const std::vector<T>& in) {
for (auto b : in) {
pushBytes(data, b);

Check warning on line 63 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L61-L63

Added lines #L61 - L63 were not covered by tests
}
}

Check warning on line 65 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L65

Added line #L65 was not covered by tests

int16_t getHI(uint32_t v) {
int16_t lo = v & 0xffff;
int16_t hi = v >> 16;
Expand All @@ -63,6 +79,7 @@ int16_t getLO(uint32_t v) {

void PCSX::PS1Packer::pack(IO<File> src, IO<File> dest, uint32_t addr, uint32_t pc, uint32_t gp, uint32_t sp,
const Options& options) {
constexpr size_t stubSize = 16;

Check warning on line 82 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L82

Added line #L82 was not covered by tests
std::vector<uint8_t> dataIn;
dataIn.resize(src->size());
src->read(dataIn.data(), dataIn.size());
Expand All @@ -73,80 +90,141 @@ void PCSX::PS1Packer::pack(IO<File> src, IO<File> dest, uint32_t addr, uint32_t
ucl_uint outSize;
int r;

r = ucl_nrv2e_99_compress(dataIn.data(), dataIn.size(), dataOut.data() + (options.raw ? 16 : 0), &outSize, nullptr,
10, nullptr, nullptr);
// Compress the binary using ucl-nrv2e, and store the compressed
// binary in dataOut, potentially offset by the size of our stub,
// if we're outputting a raw file.
r = ucl_nrv2e_99_compress(dataIn.data(), dataIn.size(), dataOut.data() + (options.raw ? stubSize : 0), &outSize,

Check warning on line 96 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L96

Added line #L96 was not covered by tests
nullptr, 10, nullptr, nullptr);
if (r != UCL_E_OK) {
throw std::runtime_error("Fatal error during data compression.\n");
}
dataOut.resize(outSize + (options.raw ? 16 : 0));
uint32_t newPC;
uint32_t compLoad;
bool inplace;

while ((dataOut.size() & 3) != 0) {
dataOut.push_back(0);
}
// If we're outputting a rom file, our tload will be fixed
// at 0x1f000110, after the license string.
uint32_t tload = options.rom ? 0x1f000110 : options.tload;

if (tload != 0) {
newPC = tload + dataOut.size();

Check warning on line 113 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L113

Added line #L113 was not covered by tests
compLoad = tload;
newPC = compLoad + dataOut.size();
inplace = false;
} else {
// If we don't have a tload, it means we're doing
// in-place decompression. We need to make sure
// we have enough space to decompress our binary
// in-place, and ucl-nrv2e requires 16 bytes to
// ensure this property.
newPC = addr + dataIn.size() + 16;
compLoad = newPC - dataOut.size();
inplace = true;
}
newPC += sizeof(n2e_d::code);

if (options.raw) {
// If outputting a raw file, our start address is going
// to be the same as our tload address, so we need to inject
// a jump to the start of our code.
std::vector<uint8_t> stub;
pushBytes(stub, lui(Reg::V0, getHI(newPC)));
pushBytes(stub, addiu(Reg::V0, Reg::V0, getLO(newPC)));
pushBytes(stub, jr(Reg::V0));
pushBytes(stub, nop());

assert(stub.size() == stubSize);

Check warning on line 136 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L136

Added line #L136 was not covered by tests

std::copy(stub.begin(), stub.end(), dataOut.begin());
compLoad += stub.size();
}

for (auto b : n2e_d::code) pushBytes<uint32_t>(dataOut, b);

pushBytes(dataOut, addiu(Reg::T8, Reg::RA, 0));
// Now we start writing our decompressor stub.
// First, dump the decompressor code.
pushBytes(dataOut, n2e_d::code);

Check warning on line 144 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L144

Added line #L144 was not covered by tests

// At this point in the dataOut buffer, we have the following:
// 1. Maybe a small stub to jump over the compressed data
// and decompressor code, if we're a raw file.
// 2. The compressed data.
// 3. The decompressor code.
//
// What comes next is the bootstrapper code, which will
// set things up for the decompressor code to run, and
// call it, then run the decompressed code.
//
// If not a raw file, the current location in dataOut
// will be the PC we're setting up in the output
// binary file, and so the next instructions will
// be the very first our binary will run.

if (!options.shell) {

Check warning on line 161 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L161

Added line #L161 was not covered by tests
// We save $ra to $t8, so we can restore it later. This breaks ABI,
// but the ucl-nrv2e decompressor won't use it. This isn't useful
// for the shell trick, since we're just going to reboot the machine.
pushBytes(dataOut, addiu(Reg::T8, Reg::RA, 0));

Check warning on line 165 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L165

Added line #L165 was not covered by tests
}
// Kill interrupts by setting IMASK to 0.
pushBytes(dataOut, lui(Reg::V1, 0x1f80));
pushBytes(dataOut, sw(Reg::R0, 0x1074, Reg::V1));
// Calls the ucl-nrv2e decompressor.
pushBytes(dataOut, lui(Reg::A0, getHI(compLoad)));
pushBytes(dataOut, addiu(Reg::A0, Reg::A0, getLO(compLoad)));
pushBytes(dataOut, lui(Reg::A1, getHI(addr)));
pushBytes(dataOut, bgezal(Reg::R0, -((int16_t)(sizeof(n2e_d::code) + 7 * 4))));
pushBytes(dataOut, addiu(Reg::A1, Reg::A1, getLO(addr)));

// Then, bootstrap our newly-decompressed binary.
if (options.shell) {
pushBytes(dataOut, bgezal(Reg::R0, 36));
std::vector<uint8_t> breakHandler;
std::vector<uint8_t> shellCode;

Check warning on line 180 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L179-L180

Added lines #L179 - L180 were not covered by tests

// This goes to 0x40. This is our break handler,
// which will prevent the shell from being memcpy()ed
// to 0x80030000, and run our code instead.
pushBytes(breakHandler, mtc0(Reg::R0, 7));
pushBytes(breakHandler, jr(Reg::RA));
pushBytes(breakHandler, rfe());

Check warning on line 187 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L185-L187

Added lines #L185 - L187 were not covered by tests

// This goes to 0x80030000. We just bootstrap our decompressed
// binary in memory.
pushBytes(shellCode, nop());
pushBytes(shellCode, lui(Reg::T0, getHI(pc)));
pushBytes(shellCode, addiu(Reg::T0, Reg::T0, getLO(pc)));
pushBytes(shellCode, lui(Reg::GP, getHI(gp)));
pushBytes(shellCode, jr(Reg::T0));
pushBytes(shellCode, addiu(Reg::GP, Reg::GP, getLO(gp)));

Check warning on line 196 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L191-L196

Added lines #L191 - L196 were not covered by tests

// Jumps over the two blocks of code above, grabbing their address
// in $ra using bal.
pushBytes(dataOut, bgezal(Reg::R0, breakHandler.size() + shellCode.size()));

Check warning on line 200 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L200

Added line #L200 was not covered by tests
// $s0 = 0xa0
pushBytes(dataOut, addiu(Reg::S0, Reg::R0, 0xa0));
// this goes to 0x40
pushBytes(dataOut, mtc0(Reg::R0, 7));
pushBytes(dataOut, jr(Reg::RA));
pushBytes(dataOut, rfe());
// this goes to 0x80030000
pushBytes(dataOut, lui(Reg::T0, getHI(pc)));
pushBytes(dataOut, addiu(Reg::T0, Reg::T0, getLO(pc)));
pushBytes(dataOut, lui(Reg::GP, getHI(gp)));
pushBytes(dataOut, jr(Reg::T0));
pushBytes(dataOut, addiu(Reg::GP, Reg::GP, getLO(gp)));
// copying stuff around

// Insert the two pieces of code we need to copy.
pushBytes(dataOut, breakHandler);
pushBytes(dataOut, shellCode);

Check warning on line 206 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L205-L206

Added lines #L205 - L206 were not covered by tests

// Copying stuff around, calling into the kernel.
// $s1 = address of the break handler.
pushBytes(dataOut, addiu(Reg::S1, Reg::RA, 0));

pushBytes(dataOut, addiu(Reg::A0, Reg::R0, 0x40));
pushBytes(dataOut, addiu(Reg::A1, Reg::S1, 0));
pushBytes(dataOut, addiu(Reg::A2, Reg::R0, 12));
pushBytes(dataOut, addiu(Reg::A2, Reg::R0, breakHandler.size()));

Check warning on line 214 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L214

Added line #L214 was not covered by tests
// Call A0:2A - memcpy.
pushBytes(dataOut, jalr(Reg::S0));
pushBytes(dataOut, addiu(Reg::T1, Reg::R0, 0x2a));

pushBytes(dataOut, lui(Reg::A0, 0x8003));
pushBytes(dataOut, addiu(Reg::A1, Reg::S1, 12));
pushBytes(dataOut, addiu(Reg::A2, Reg::R0, 20));
pushBytes(dataOut, addiu(Reg::A1, Reg::S1, breakHandler.size()));
pushBytes(dataOut, addiu(Reg::A2, Reg::R0, shellCode.size()));

Check warning on line 221 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L220-L221

Added lines #L220 - L221 were not covered by tests
// Call A0:2A - memcpy.
pushBytes(dataOut, jalr(Reg::S0));
pushBytes(dataOut, addiu(Reg::T1, Reg::R0, 0x2a));

// And reboot, leaving cop0's registers set to break
// on writes to 0x80030000.
constexpr uint32_t partialReboot = 0xbfc00390;

pushBytes(dataOut, lui(Reg::RA, getHI(partialReboot)));
Expand All @@ -160,33 +238,60 @@ void PCSX::PS1Packer::pack(IO<File> src, IO<File> dest, uint32_t addr, uint32_t
pushBytes(dataOut, mtc0(Reg::T2, 9));
pushBytes(dataOut, mtc0(Reg::T0, 7));

// Call A0:44 - FlushCache, after mangling $ra to
// almost be the address of the start of the bios,
// skipping over SBUS settings, and the resetting
// of all the cop0 registers.
pushBytes(dataOut, jr(Reg::S0));
pushBytes(dataOut, addiu(Reg::T1, Reg::R0, 0x44));
} else {
// Calls A0:44 - FlushCache
pushBytes(dataOut, addiu(Reg::T0, Reg::R0, 0xa0));
pushBytes(dataOut, jalr(Reg::T0));
pushBytes(dataOut, addiu(Reg::T1, Reg::R0, 0x44));
// Then jumps into the decompressed binary, restoring
// $ra if needed, so the decompressed binary can return
// to the caller gracefully.
pushBytes(dataOut, lui(Reg::T0, getHI(pc)));
pushBytes(dataOut, addiu(Reg::T0, Reg::T0, getLO(pc)));
pushBytes(dataOut, jr(Reg::T0));
pushBytes(dataOut, addiu(Reg::RA, Reg::T8, 0));
}

// Pad our PS-X EXE to a multiple of 2048 bytes, because
// that's what the PS1's BIOS expects when reading from CD-Rom.
while (!options.cpe && !options.booty && !options.rom && !options.raw && !options.nopad &&
((dataOut.size() & 0x7ff) != 0)) {
dataOut.push_back(0);
}

// Finally, create the header of our binary.

// For a rom file, this is going to be the most complex
// header, as we still need to push some more code.
std::vector<uint8_t> header;
if (options.booty || options.rom) {
// We'll use the same bootstrap code for both the normal rom code
// and the booty code, cause we're lazy. The stage2 code is
// going to be executed by the breakpoint we're setting on top
// of 0x80030000 during the rom boot process. The booty
// code is too large to fit properly, so it'll fit at 0x24.
// The normal code fits properly at 0x40.
std::vector<uint32_t> stage2;
if (options.booty) {
// What the booty code will do here is continue pumping
// bytes out of the parallel port, which will be the
// compressed binary, to place it in its proper location
// in RAM. Once it's done, it'll jump to the start of
// the compressed binary through FlushCache.
/* 0x24 */ stage2.push_back(lw(Reg::A3, 0, Reg::A1));
/* 0x28 */ stage2.push_back(addiu(Reg::A2, Reg::A2, -1));
/* 0x2c */ stage2.push_back(sw(Reg::A3, 0, Reg::A0));
/* 0x30 */ stage2.push_back(bne(Reg::A2, Reg::R0, -16));
/* 0x34 */ stage2.push_back(addiu(Reg::A0, Reg::A0, 4));
/* 0x38 */ stage2.push_back(j(0xa0));
/* 0x3c */ stage2.push_back(addiu(Reg::T1, Reg::R0, 0x44));
// This is actually the entry point.
/* 0x40 */ stage2.push_back(mtc0(Reg::R0, 7));
/* 0x44 */ stage2.push_back(lui(Reg::A0, compLoad >> 16));
if ((compLoad & 0xffff) != 0) {
Expand All @@ -200,20 +305,26 @@ void PCSX::PS1Packer::pack(IO<File> src, IO<File> dest, uint32_t addr, uint32_t
/* 0x58 */ stage2.push_back(j(0x24));
/* 0x5c */ stage2.push_back(ori(Reg::A2, Reg::R0, dataOut.size() / 4));
} else {
// Stage 2 for a normal rom file is much more simple. We just
// need to jump to the start of our compressed binary. We only
// need roughly 3 or 4 instructions.
stage2.push_back(lui(Reg::V0, newPC >> 16));
if ((newPC & 0xffff) != 0) {
stage2.push_back(ori(Reg::V0, Reg::V0, newPC & 0xffff));
}
stage2.push_back(jr(Reg::V0));
stage2.push_back(mtc0(Reg::R0, 7));

// We still need to build the rom header file.
static constexpr char disclaimer[] =
"This is self-decompressing binary,"
" suitable for a flash cart rom, "
"This is a self-decompressing binary "
"suitable for a flash cart rom "
"created by ps1-packer (https://bit.ly/pcsx-redux). "
"It is NOT";
"It is NOT ";
for (auto b : disclaimer) {
header.push_back(b);
}
assert(header.size() <= 0x80);

Check warning on line 327 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L327

Added line #L327 was not covered by tests
while (header.size() < 0x80) {
header.push_back(0);
}
Expand All @@ -226,22 +337,25 @@ void PCSX::PS1Packer::pack(IO<File> src, IO<File> dest, uint32_t addr, uint32_t
0x61, 0x69, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x49, 0x6e, 0x63, 0x2e, 0x00, 0x00, 0x00, 0x1f,
};

for (auto b : license) {
pushBytes(header, b);
}
pushBytes(header, license);

Check warning on line 340 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L340

Added line #L340 was not covered by tests

// break on writes and/or exec
// Break on writes and/or exec, to call our break handler which we
// will place in memory next.
pushBytes(header, mtc0(Reg::R0, 7));
pushBytes(header, addiu(Reg::T2, Reg::R0, 0xffff));
pushBytes(header, addiu(Reg::T2, Reg::R0, -1));

Check warning on line 345 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L345

Added line #L345 was not covered by tests
pushBytes(header, lui(Reg::T1, 0x8003));
pushBytes(header, lui(Reg::T0, 0xeb80));
pushBytes(header, lui(Reg::T0, 0b1100101010000000));

Check warning on line 347 in src/supportpsx/ps1-packer.cc

View check run for this annotation

Codecov / codecov/patch

src/supportpsx/ps1-packer.cc#L347

Added line #L347 was not covered by tests
pushBytes(header, mtc0(Reg::T2, 11));
pushBytes(header, mtc0(Reg::T2, 9));
pushBytes(header, mtc0(Reg::T1, 5));
pushBytes(header, mtc0(Reg::T1, 3));
pushBytes(header, mtc0(Reg::T0, 7));

// As said before, the break handler will be placed at 0x40 or 0x24.
int16_t base = options.booty ? 0x24 : 0x40;
// We just put the bytes of the stage2 in ram, short by short, with
// no loop. Works great for booty as it's becoming very deterministic,
// and only requires a few extra instructions as a rom file.
uint32_t last = 0;
for (auto b : stage2) {
if (b == 0) {
Expand All @@ -257,18 +371,24 @@ void PCSX::PS1Packer::pack(IO<File> src, IO<File> dest, uint32_t addr, uint32_t
pushBytes(header, last);
base += 4;
}
// Flip the jr $ra with the last instruction, to place
// it into the delay slot.
header.pop_back();
header.pop_back();
header.pop_back();
header.pop_back();
pushBytes(header, jr(Reg::RA));
pushBytes(header, last);
if (options.rom) {
// Our tload for a rom file is fixed at 0x1f000110, so we
// have some leeway for stage2 above, and so we need to
// pad until we arrive at 0x110.
while (header.size() < (tload & 0xffffff)) {
header.push_back(0);
}
}
} else if (options.cpe) {
// Basic CPE file format.
pushBytes<uint32_t>(header, 0x1455043);
pushBytes<uint16_t>(header, 0x0008);
pushBytes<uint8_t>(header, 3);
Expand All @@ -279,6 +399,7 @@ void PCSX::PS1Packer::pack(IO<File> src, IO<File> dest, uint32_t addr, uint32_t
pushBytes<uint32_t>(header, dataOut.size());
dataOut.push_back(0);
} else if (!options.raw) {
// Basic PS-X EXE file format.
pushBytes(header, PSEXE);
pushBytes<uint32_t>(header, 0);
pushBytes<uint32_t>(header, 0);
Expand All @@ -296,6 +417,7 @@ void PCSX::PS1Packer::pack(IO<File> src, IO<File> dest, uint32_t addr, uint32_t
}
}

// All done. Write the header, and then the code blob.

Check notice on line 420 in src/supportpsx/ps1-packer.cc

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

✅ Getting better: Complex Method

PCSX::PS1Packer::pack decreases in cyclomatic complexity from 36 to 35, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
dest->write(header.data(), header.size());
dest->write(dataOut.data(), dataOut.size());
}

0 comments on commit 0a4c06f

Please sign in to comment.