Feature/runtime ecosystem refactor (#107)

* feat: remove memory and pad from stub section

* feat: add support for resume entry targets in CodeGenerator (this allow jumps in address outside function)
feat: refactor entry point discovery one more try to reduce big generated file

* feat: optmizations for release build

* feat: remove unused  file

* feat: added some test cases for code gen

* feat: added log macro and remove win specific code

* feat: refactor runtime folder structure
feat: added reset sound driver RPC state and compatibility layout
feat: rename and added new test
feat: update RPC calls to use defined constants
feat: added more PSMC(16, 32)
feat: change cd read to try find the asset ignoring case sensitive
fix: fix some render problems
feat: add logs on pad
feat: added more RPC handles

* feat: added game override for code veronica

* feat: apply vita patch

* feat: flags to disable build

* feat: fix merges
feat: break a lot of tests

* feat: better throw error on empty cd path
feat: remove recompiler unusde function
feat: apply missing patch

* feat: gamedp is now part of lib
feat: missing file

* feat: small cleanup

* feat: missing vita changes

* feat: fix more merge

* feat: fix tests

* feat: last missing feature

* feat: added missing import

* feat: rename test local functions

* feat: init syscall on ps2 list

* feat: added DMA helpers

* feat: faster builds
feat: more implement for darkcloud

* feat: back missing file

* feat: added missing includes

* feat: remove test

* feat: missing include

* feat: read register funtion

* feat: build  fix

* feat: force  exit on detach thread
This commit is contained in:
Ranieri
2026-04-04 23:09:56 -03:00
committed by GitHub
parent 553a9027d8
commit 93e221feaa
137 changed files with 26594 additions and 15314 deletions
+347 -28
View File
@@ -4,11 +4,16 @@
#include "ps2recomp/r5900_decoder.h"
#include "ps2recomp/types.h"
#include "ps2_runtime.h"
#include "ps2_memory.h"
#include "runtime/ps2_memory.h"
#include "ps2_syscalls.h"
#include "ps2_stubs.h"
#include "ps2_gs_gpu.h"
#include "runtime/ps2_gs_gpu.h"
#include "runtime/ps2_gs_psmct32.h"
#include "ps2_runtime_macros.h"
#include "Stubs/MPEG.h"
#include "Stubs/Audio.h"
#include "Stubs/GS.h"
#include "Stubs/VU.h"
#include <atomic>
#include <chrono>
@@ -78,8 +83,7 @@ namespace
uint32_t frameOffsetBytes(uint32_t x, uint32_t y, uint32_t fbw)
{
const uint32_t stride = fbw * 64u * 4u; // CT32
return y * stride + x * 4u;
return GSPSMCT32::addrPSMCT32(0u, (fbw != 0u) ? fbw : 1u, x, y);
}
void testRuntimeWorkerLoop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
@@ -104,9 +108,9 @@ namespace
std::atomic<int32_t> gSerializedGuestActive{0};
std::atomic<int32_t> gSerializedGuestMaxActive{0};
std::atomic<int32_t> gCooperativeYieldEntryCount{0};
std::atomic<bool> gCooperativeYieldAllowFirstYield{false};
std::atomic<bool> gCooperativeYieldPeerRan{false};
std::atomic<int32_t> gPreemptionPolicyEntryCount{0};
std::atomic<bool> gPreemptionPolicyAllowFirstProbe{false};
std::atomic<bool> gPreemptionPolicyPeerRan{false};
void testSerializedGuestStep(uint8_t *, R5900Context *ctx, PS2Runtime *)
{
@@ -129,32 +133,33 @@ namespace
}
}
void testCooperativeGuestYieldStep(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
void testPreemptionPolicyStep(uint8_t *, R5900Context *ctx, PS2Runtime *runtime)
{
if (!ctx || !runtime)
{
return;
}
const int32_t entryIndex = gCooperativeYieldEntryCount.fetch_add(1, std::memory_order_acq_rel) + 1;
const int32_t entryIndex = gPreemptionPolicyEntryCount.fetch_add(1, std::memory_order_acq_rel) + 1;
if (entryIndex == 1)
{
while (!gCooperativeYieldAllowFirstYield.load(std::memory_order_acquire))
while (!gPreemptionPolicyAllowFirstProbe.load(std::memory_order_acquire))
{
std::this_thread::yield();
}
for (int attempt = 0; attempt < 32 &&
!gCooperativeYieldPeerRan.load(std::memory_order_acquire);
bool shouldPreempt = false;
for (int attempt = 0; attempt < 256 &&
!shouldPreempt;
++attempt)
{
runtime->cooperativeGuestYield();
shouldPreempt = runtime->shouldPreemptGuestExecution();
}
setRegU32(*ctx, 2, gCooperativeYieldPeerRan.load(std::memory_order_acquire) ? 1u : 0u);
setRegU32(*ctx, 2, shouldPreempt ? 1u : 0u);
}
else
{
gCooperativeYieldPeerRan.store(true, std::memory_order_release);
gPreemptionPolicyPeerRan.store(true, std::memory_order_release);
setRegU32(*ctx, 2, 2u);
}
@@ -211,6 +216,7 @@ namespace
gAsyncCallbackObservedGp.store(::getRegU32(ctx, 28), std::memory_order_release);
ctx->pc = 0u;
}
}
void register_ps2_runtime_expansion_tests()
@@ -293,19 +299,19 @@ void register_ps2_runtime_expansion_tests()
"dispatchLoop should not execute guest code concurrently on one runtime");
});
tc.Run("cooperative guest yield lets another guest thread run without leaving the current function", [](TestCase &t)
tc.Run("guest preemption policy requests a dispatcher handoff when another guest thread contends", [](TestCase &t)
{
PS2Runtime runtime;
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
constexpr uint32_t kFirstEntry = 0x190000u;
constexpr uint32_t kSecondEntry = 0x1A0000u;
gCooperativeYieldEntryCount.store(0, std::memory_order_release);
gCooperativeYieldAllowFirstYield.store(false, std::memory_order_release);
gCooperativeYieldPeerRan.store(false, std::memory_order_release);
gPreemptionPolicyEntryCount.store(0, std::memory_order_release);
gPreemptionPolicyAllowFirstProbe.store(false, std::memory_order_release);
gPreemptionPolicyPeerRan.store(false, std::memory_order_release);
runtime.registerFunction(kFirstEntry, &testCooperativeGuestYieldStep);
runtime.registerFunction(kSecondEntry, &testCooperativeGuestYieldStep);
runtime.registerFunction(kFirstEntry, &testPreemptionPolicyStep);
runtime.registerFunction(kSecondEntry, &testPreemptionPolicyStep);
R5900Context firstCtx{};
R5900Context secondCtx{};
@@ -319,7 +325,7 @@ void register_ps2_runtime_expansion_tests()
const bool firstEntered = waitUntil([&]()
{
return gCooperativeYieldEntryCount.load(std::memory_order_acquire) >= 1;
return gPreemptionPolicyEntryCount.load(std::memory_order_acquire) >= 1;
}, std::chrono::milliseconds(100));
std::thread secondWorker([&]()
@@ -332,7 +338,7 @@ void register_ps2_runtime_expansion_tests()
return runtime.guestExecutionWaiterCountForTesting() > 0u;
}, std::chrono::milliseconds(100));
gCooperativeYieldAllowFirstYield.store(true, std::memory_order_release);
gPreemptionPolicyAllowFirstProbe.store(true, std::memory_order_release);
if (firstWorker.joinable())
{
@@ -343,12 +349,12 @@ void register_ps2_runtime_expansion_tests()
secondWorker.join();
}
t.IsTrue(firstEntered, "first guest worker should enter before yielding");
t.IsTrue(secondContending, "second guest worker should contend for guest execution before the first yields");
t.IsTrue(gCooperativeYieldPeerRan.load(std::memory_order_acquire),
"second guest worker should run while the first cooperatively yields");
t.IsTrue(firstEntered, "first guest worker should enter before probing for preemption");
t.IsTrue(secondContending, "second guest worker should contend for guest execution before the first returns");
t.IsTrue(gPreemptionPolicyPeerRan.load(std::memory_order_acquire),
"second guest worker should run after the first returns to the dispatcher");
t.Equals(getRegU32(&firstCtx, 2), 1u,
"first guest worker should observe that a peer ran before it resumed");
"first guest worker should observe that the runtime requested preemption under contention");
});
tc.Run("vblank intc handlers can preempt serialized guest execution", [](TestCase &t)
@@ -459,6 +465,171 @@ void register_ps2_runtime_expansion_tests()
notifyRuntimeStop();
});
tc.Run("MPEG init and callback stubs return success instead of TODO errors", [](TestCase &t)
{
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
ps2_stubs::resetMpegStubState();
R5900Context initCtx{};
ps2_stubs::sceMpegInit(rdram.data(), &initCtx, nullptr);
t.Equals(getRegS32(initCtx, 2), 0,
"sceMpegInit should succeed so games can continue through movie setup");
R5900Context addCtx0{};
setRegU32(addCtx0, 4, 0x00123000u);
setRegU32(addCtx0, 5, 1u);
setRegU32(addCtx0, 6, 0x00124000u);
setRegU32(addCtx0, 7, 0u);
ps2_stubs::sceMpegAddCallback(rdram.data(), &addCtx0, nullptr);
t.Equals(getRegS32(addCtx0, 2), 1,
"first sceMpegAddCallback should hand back a non-error callback handle");
R5900Context addCtx1{};
setRegU32(addCtx1, 4, 0x00123000u);
setRegU32(addCtx1, 5, 2u);
setRegU32(addCtx1, 6, 0x00124010u);
setRegU32(addCtx1, 7, 0u);
ps2_stubs::sceMpegAddCallback(rdram.data(), &addCtx1, nullptr);
t.Equals(getRegS32(addCtx1, 2), 2,
"subsequent sceMpegAddCallback calls should keep succeeding");
R5900Context reinitCtx{};
ps2_stubs::sceMpegInit(rdram.data(), &reinitCtx, nullptr);
R5900Context addAfterReinit{};
setRegU32(addAfterReinit, 4, 0x00123000u);
setRegU32(addAfterReinit, 5, 3u);
setRegU32(addAfterReinit, 6, 0x00124020u);
setRegU32(addAfterReinit, 7, 0u);
ps2_stubs::sceMpegAddCallback(rdram.data(), &addAfterReinit, nullptr);
t.Equals(getRegS32(addAfterReinit, 2), 1,
"sceMpegInit should reset MPEG callback bookkeeping between runs");
});
tc.Run("movie startup MPEG and audio stubs return safe progress values", [](TestCase &t)
{
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
ps2_stubs::clearMpegCompatLayout();
ps2_stubs::resetMpegStubState();
ps2_stubs::resetAudioStubState();
R5900Context firstIsEndCtx{};
setRegU32(firstIsEndCtx, 4, 0x00123000u);
ps2_stubs::sceMpegIsEnd(rdram.data(), &firstIsEndCtx, nullptr);
t.Equals(getRegS32(firstIsEndCtx, 2), 0,
"sceMpegIsEnd should allow one synthetic frame before reporting end");
R5900Context demuxCtx{};
setRegU32(demuxCtx, 4, 0x00123000u);
setRegU32(demuxCtx, 5, 0x00400000u);
setRegU32(demuxCtx, 6, 0x00004000u);
setRegU32(demuxCtx, 7, 0x00410000u);
ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &demuxCtx, nullptr);
t.Equals(getRegS32(demuxCtx, 2), 0x4000,
"sceMpegDemuxPssRing should consume the provided input instead of trapping");
R5900Context getPictureCtx{};
setRegU32(getPictureCtx, 4, 0x00123000u);
setRegU32(getPictureCtx, 5, 0x00124000u);
setRegU32(getPictureCtx, 6, 440u);
ps2_stubs::sceMpegGetPicture(rdram.data(), &getPictureCtx, nullptr);
t.Equals(Ps2FastRead32(rdram.data(), 0x00123000u + 0x00u), 320u,
"sceMpegGetPicture should seed a safe movie width");
t.Equals(Ps2FastRead32(rdram.data(), 0x00123000u + 0x04u), 240u,
"sceMpegGetPicture should seed a safe movie height");
t.Equals(Ps2FastRead32(rdram.data(), 0x00123000u + 0x08u), 0u,
"first synthetic picture should preserve frameCount==0 for guest setup");
R5900Context secondIsEndCtx{};
setRegU32(secondIsEndCtx, 4, 0x00123000u);
ps2_stubs::sceMpegIsEnd(rdram.data(), &secondIsEndCtx, nullptr);
t.Equals(getRegS32(secondIsEndCtx, 2), 0,
"sceMpegIsEnd should keep the decode thread alive and let the guest stop playback");
R5900Context remoteInitCtx{};
ps2_stubs::sceSdRemoteInit(rdram.data(), &remoteInitCtx, nullptr);
t.Equals(getRegS32(remoteInitCtx, 2), 0,
"sceSdRemoteInit should succeed so Veronica can set up movie audio");
R5900Context blockTransCtx{};
const uint32_t blockTransSp = 0x00100000u;
setRegU32(blockTransCtx, 29, blockTransSp);
setRegU32(blockTransCtx, 4, 1u);
setRegU32(blockTransCtx, 5, 0x80E0u);
setRegU32(blockTransCtx, 6, 1u);
setRegU32(blockTransCtx, 7, 2u);
std::memcpy(rdram.data() + blockTransSp + 0x10u, "\x40\x23\x01\x00", 4u);
std::memcpy(rdram.data() + blockTransSp + 0x14u, "\x00\x30\x00\x00", 4u);
std::memcpy(rdram.data() + blockTransSp + 0x18u, "\x40\x27\x01\x00", 4u);
ps2_stubs::sceSdRemote(rdram.data(), &blockTransCtx, nullptr);
t.Equals(getRegU32(&blockTransCtx, 2), 0x00012340u,
"sceSdRemote block transfer should publish the current IOP ring position");
R5900Context statusCtx{};
setRegU32(statusCtx, 29, blockTransSp);
setRegU32(statusCtx, 4, 1u);
setRegU32(statusCtx, 5, 0x80F0u);
setRegU32(statusCtx, 6, 1u);
setRegU32(statusCtx, 7, 0u);
std::memset(rdram.data() + blockTransSp + 0x10u, 0, 12u);
ps2_stubs::sceSdRemote(rdram.data(), &statusCtx, nullptr);
t.Equals(getRegU32(&statusCtx, 2), 0x00012340u,
"sceSdRemote status polling should reuse the last configured transfer base");
R5900Context setParamCtx{};
setRegU32(setParamCtx, 29, blockTransSp);
setRegU32(setParamCtx, 4, 1u);
setRegU32(setParamCtx, 5, 0x8010u);
setRegU32(setParamCtx, 6, 0x0F81u);
setRegU32(setParamCtx, 7, 0u);
ps2_stubs::sceSdRemote(rdram.data(), &setParamCtx, nullptr);
t.Equals(getRegU32(&setParamCtx, 2), 0x00012340u,
"sceSdRemote set-param calls should not trap or disturb the movie audio state");
});
tc.Run("IPU init skips missing optional helper instead of dispatching the default trap", [](TestCase &t)
{
PS2Runtime runtime;
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
ctx.pc = 0x0010B470u;
ps2_stubs::sceIpuInit(rdram.data(), &ctx, &runtime);
t.IsFalse(runtime.isStopRequested(),
"sceIpuInit should tolerate the missing optional SetD4 helper");
t.Equals(runtime.memory().read32(0x10002010u), 0x40000000u,
"sceIpuInit should still program IPU_CTRL");
t.Equals(runtime.memory().read32(0x10002000u), 0u,
"sceIpuInit should leave IPU_CMD reset after initialization");
});
tc.Run("sprintf consumes EE varargs from a2 a3 t0 and preserves width formatting", [](TestCase &t)
{
PS2Runtime runtime;
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
constexpr uint32_t kDestAddr = 0x00002000u;
constexpr uint32_t kFormatAddr = 0x00002100u;
constexpr char kFormat[] = "rm_%1d%02d%1d.rdx";
std::memcpy(rdram.data() + kFormatAddr, kFormat, sizeof(kFormat));
setRegU32(ctx, 4, kDestAddr);
setRegU32(ctx, 5, kFormatAddr);
setRegU32(ctx, 6, 0u); // a2
setRegU32(ctx, 7, 3u); // a3
setRegU32(ctx, 8, 1u); // t0
ps2_stubs::sprintf(rdram.data(), &ctx, &runtime);
const std::string rendered(reinterpret_cast<const char *>(rdram.data() + kDestAddr));
t.Equals(rendered, std::string("rm_0031.rdx"),
"sprintf should read the third variadic integer from t0 and honor %02d");
t.Equals(getRegS32(ctx, 2), static_cast<int32_t>(rendered.size()),
"sprintf should return the rendered length");
});
tc.Run("multiply-add matrix writes rd only when R5900 requires it", [](TestCase &t)
{
R5900Decoder decoder;
@@ -950,5 +1121,153 @@ void register_ps2_runtime_expansion_tests()
runtime.requestStop();
notifyRuntimeStop();
});
tc.Run("sceVu0ApplyMatrix uses libvux matrix math with the imported EE ABI", [](TestCase &t)
{
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
constexpr uint32_t kOutAddr = 0x00100000u;
constexpr uint32_t kMatrixAddr = 0x00100040u;
constexpr uint32_t kSrcAddr = 0x00100080u;
const float matrix[16] = {
1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 6.0f, 7.0f, 8.0f,
9.0f, 10.0f, 11.0f, 12.0f,
13.0f, 14.0f, 15.0f, 16.0f,
};
const float src[4] = {1.0f, 2.0f, 3.0f, 1.0f};
std::memcpy(rdram.data() + kMatrixAddr, matrix, sizeof(matrix));
std::memcpy(rdram.data() + kSrcAddr, src, sizeof(src));
setRegU32(ctx, 4, kOutAddr);
setRegU32(ctx, 5, kMatrixAddr);
setRegU32(ctx, 6, kSrcAddr);
ps2_stubs::sceVu0ApplyMatrix(rdram.data(), &ctx, nullptr);
float out[4]{};
std::memcpy(out, rdram.data() + kOutAddr, sizeof(out));
t.Equals(out[0], 51.0f, "sceVu0ApplyMatrix should compute X with libvux layout");
t.Equals(out[1], 58.0f, "sceVu0ApplyMatrix should compute Y with libvux layout");
t.Equals(out[2], 65.0f, "sceVu0ApplyMatrix should compute Z with libvux layout");
t.Equals(out[3], 72.0f, "sceVu0ApplyMatrix should compute W with libvux layout");
t.Equals(getRegS32(ctx, 2), 0, "sceVu0ApplyMatrix should report success");
});
tc.Run("sceVu0TransposeMatrix transposes a 4x4 matrix with dst/src ABI", [](TestCase &t)
{
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
constexpr uint32_t kDstAddr = 0x00100100u;
constexpr uint32_t kSrcAddr = 0x00100140u;
const float src[16] = {
1.0f, 2.0f, 3.0f, 4.0f,
5.0f, 6.0f, 7.0f, 8.0f,
9.0f, 10.0f, 11.0f, 12.0f,
13.0f, 14.0f, 15.0f, 16.0f,
};
std::memcpy(rdram.data() + kSrcAddr, src, sizeof(src));
setRegU32(ctx, 4, kDstAddr);
setRegU32(ctx, 5, kSrcAddr);
ps2_stubs::sceVu0TransposeMatrix(rdram.data(), &ctx, nullptr);
float out[16]{};
std::memcpy(out, rdram.data() + kDstAddr, sizeof(out));
t.Equals(out[0], 1.0f, "transpose should preserve [0][0]");
t.Equals(out[1], 5.0f, "transpose should swap row 0 col 1");
t.Equals(out[2], 9.0f, "transpose should swap row 0 col 2");
t.Equals(out[3], 13.0f, "transpose should swap row 0 col 3");
t.Equals(out[4], 2.0f, "transpose should swap row 1 col 0");
t.Equals(out[5], 6.0f, "transpose should preserve [1][1]");
t.Equals(out[10], 11.0f, "transpose should preserve [2][2]");
t.Equals(out[12], 4.0f, "transpose should swap row 3 col 0");
t.Equals(out[15], 16.0f, "transpose should preserve [3][3]");
t.Equals(getRegS32(ctx, 2), 0, "sceVu0TransposeMatrix should report success");
});
tc.Run("sceVif1PkReset preserves the packet base pointer and clears open tag state", [](TestCase &t)
{
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
constexpr uint32_t kStateAddr = 0x00100200u;
constexpr uint32_t kBaseAddr = 0x00101000u;
setRegU32(ctx, 4, kStateAddr);
setRegU32(ctx, 5, kBaseAddr);
ps2_stubs::sceVif1PkInit(rdram.data(), &ctx, nullptr);
const uint32_t dirtyCurrent = kBaseAddr + 0x40u;
const uint32_t dirtyPending = 0x12345678u;
const uint32_t dirtyDirectOpen = 0x00ABCDEFu;
const uint32_t dirtyGifOpen = 0x00112233u;
std::memcpy(rdram.data() + kStateAddr + 0u, &dirtyCurrent, sizeof(dirtyCurrent));
std::memcpy(rdram.data() + kStateAddr + 8u, &dirtyPending, sizeof(dirtyPending));
std::memcpy(rdram.data() + kStateAddr + 12u, &dirtyDirectOpen, sizeof(dirtyDirectOpen));
std::memcpy(rdram.data() + kStateAddr + 20u, &dirtyGifOpen, sizeof(dirtyGifOpen));
std::memset(&ctx, 0, sizeof(ctx));
setRegU32(ctx, 4, kStateAddr);
ps2_stubs::sceVif1PkReset(rdram.data(), &ctx, nullptr);
uint32_t current = 0u;
uint32_t base = 0u;
uint32_t pending = 0u;
uint32_t directOpen = 0u;
uint32_t gifOpen = 0u;
std::memcpy(&current, rdram.data() + kStateAddr + 0u, sizeof(current));
std::memcpy(&base, rdram.data() + kStateAddr + 4u, sizeof(base));
std::memcpy(&pending, rdram.data() + kStateAddr + 8u, sizeof(pending));
std::memcpy(&directOpen, rdram.data() + kStateAddr + 12u, sizeof(directOpen));
std::memcpy(&gifOpen, rdram.data() + kStateAddr + 20u, sizeof(gifOpen));
t.Equals(current, kBaseAddr, "sceVif1PkReset should restore current pointer to the packet base");
t.Equals(base, kBaseAddr, "sceVif1PkReset should preserve the packet base pointer");
t.Equals(pending, 0u, "sceVif1PkReset should clear pending count tracking");
t.Equals(directOpen, 0u, "sceVif1PkReset should clear direct-code open state");
t.Equals(gifOpen, 0u, "sceVif1PkReset should clear GIF-tag open state");
t.Equals(::getRegU32(&ctx, 2), kBaseAddr, "sceVif1PkReset should return the packet base pointer");
});
tc.Run("sceVif1PkCloseDirectCode encodes DIRECT length in qwords", [](TestCase &t)
{
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
constexpr uint32_t kStateAddr = 0x00100400u;
constexpr uint32_t kBaseAddr = 0x00102000u;
setRegU32(ctx, 4, kStateAddr);
setRegU32(ctx, 5, kBaseAddr);
ps2_stubs::sceVif1PkInit(rdram.data(), &ctx, nullptr);
std::memset(&ctx, 0, sizeof(ctx));
setRegU32(ctx, 4, kStateAddr);
setRegU32(ctx, 5, 0u);
ps2_stubs::sceVif1PkCnt(rdram.data(), &ctx, nullptr);
std::memset(&ctx, 0, sizeof(ctx));
setRegU32(ctx, 4, kStateAddr);
setRegU32(ctx, 5, 0u);
ps2_stubs::sceVif1PkOpenDirectCode(rdram.data(), &ctx, nullptr);
std::memset(&ctx, 0, sizeof(ctx));
setRegU32(ctx, 4, kStateAddr);
setRegU32(ctx, 5, 4u); // reserve one qword worth of GIF payload
ps2_stubs::sceVif1PkReserve(rdram.data(), &ctx, nullptr);
std::memset(&ctx, 0, sizeof(ctx));
setRegU32(ctx, 4, kStateAddr);
ps2_stubs::sceVif1PkCloseDirectCode(rdram.data(), &ctx, nullptr);
uint32_t directCmd = 0u;
std::memcpy(&directCmd, rdram.data() + kBaseAddr + 12u, sizeof(directCmd));
t.Equals(directCmd, 0x50000001u, "sceVif1PkCloseDirectCode should store a 1-QW DIRECT length");
});
});
}