1 Commits

Author SHA1 Message Date
Ran-j e27a658b32 feat: wip performance patch
feat: added parallel gs
feat: optmized IOP emulator
feat: added guest and game fps count
feat: small vu1 optmization and guards
2026-09-25 14:29:05 -03:00
46 changed files with 3140 additions and 676 deletions
+5
View File
@@ -62,6 +62,11 @@ if(PS2X_IOP_BUILD_TESTS)
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)
add_executable(ps2_iop_execution_tests tests/iop_execution_tests.cpp)
target_link_libraries(ps2_iop_execution_tests PRIVATE ps2_iop)
target_include_directories(ps2_iop_execution_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
add_test(NAME ps2_iop_execution_tests COMMAND ps2_iop_execution_tests)
endif()
install(TARGETS ps2_iop
+5 -1
View File
@@ -53,9 +53,13 @@ namespace ps2x::iop::detail
}
bool IopCpuCore::executeInstruction(IopCpuState &cpu)
{
return executeInstruction(cpu, m_memory.read32(cpu.pc));
}
bool IopCpuCore::executeInstruction(IopCpuState &cpu, uint32_t instruction)
{
const uint32_t pc = cpu.pc;
const uint32_t instruction = m_memory.read32(pc);
const bool wasDelaySlot = cpu.branchPending;
const uint32_t priorBranchTarget = cpu.branchTarget;
+1
View File
@@ -31,6 +31,7 @@ namespace ps2x::iop::detail
explicit IopCpuCore(IopMemory &memory) noexcept;
[[nodiscard]] bool executeInstruction(IopCpuState &cpu);
[[nodiscard]] bool executeInstruction(IopCpuState &cpu, uint32_t instruction);
void raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional<uint32_t> badAddress = std::nullopt) const;
private:
+73 -30
View File
@@ -32,6 +32,20 @@ namespace ps2x::iop::detail
m_nextSemaphoreId = 1;
m_nextEventFlagId = 1;
m_currentThread = nullptr;
m_nextReady = nullptr;
m_nextWakeCycle = UINT64_MAX;
m_scheduleDirty = true;
m_hasDeadThreads = false;
}
void IopKernel::setThreadState(IopThread &thread, IopThreadState state)
{
if (thread.state == state)
return;
thread.state = state;
m_scheduleDirty = true;
if (state == IopThreadState::Dead)
m_hasDeadThreads = true;
}
bool IopKernel::dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
@@ -62,6 +76,7 @@ namespace ps2x::iop::detail
}
const int id = thread.id;
m_threads.emplace(id, std::move(thread));
m_scheduleDirty = true;
setV0(id);
return true;
}
@@ -74,6 +89,14 @@ namespace ps2x::iop::detail
setV0(-1);
return true;
}
if (&it->second == m_currentThread)
{
setThreadState(it->second, IopThreadState::Dead);
cpu.stopped = cpu.yielded = true;
setV0(0);
return true;
}
m_scheduleDirty = true;
if (it->second.stackBase != 0u)
(void)m_memory.freeAllocation(it->second.stackBase);
m_threads.erase(it);
@@ -98,7 +121,7 @@ namespace ps2x::iop::detail
thread.cpu.gpr[28] = cpu.gpr[28];
thread.cpu.gpr[29] = alignUp(thread.stackBase + thread.stackSize, 16u) - 16u;
thread.cpu.gpr[31] = kThreadReturnSentinel;
thread.state = IopThreadState::Ready;
setThreadState(thread, IopThreadState::Ready);
setV0(0);
return true;
}
@@ -106,7 +129,7 @@ namespace ps2x::iop::detail
case 9: // ExitDeleteThread
if (m_currentThread != nullptr)
{
m_currentThread->state = ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant;
setThreadState(*m_currentThread, ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant);
cpu.stopped = true;
cpu.yielded = true;
}
@@ -122,7 +145,7 @@ namespace ps2x::iop::detail
setV0(-1);
return true;
}
it->second.state = IopThreadState::Dormant;
setThreadState(it->second, IopThreadState::Dormant);
setV0(0);
return true;
}
@@ -143,6 +166,7 @@ namespace ps2x::iop::detail
return true;
}
it->second.priority = std::clamp<uint32_t>(cpu.gpr[5], 1u, 126u);
m_scheduleDirty = true;
setV0(0);
return true;
}
@@ -166,7 +190,7 @@ namespace ps2x::iop::detail
it->second.state == IopThreadState::Semaphore ||
it->second.state == IopThreadState::EventFlag)
{
it->second.state = IopThreadState::Ready;
setThreadState(it->second, IopThreadState::Ready);
}
setV0(0);
return true;
@@ -202,7 +226,7 @@ namespace ps2x::iop::detail
return true;
}
if (it->second.state == IopThreadState::Sleep)
it->second.state = IopThreadState::Ready;
setThreadState(it->second, IopThreadState::Ready);
else
++it->second.wakeupCount;
setV0(0);
@@ -233,7 +257,7 @@ namespace ps2x::iop::detail
setV0(-1);
return true;
}
it->second.state = IopThreadState::Suspended;
setThreadState(it->second, IopThreadState::Suspended);
if (m_currentThread == &it->second)
cpu.yielded = true;
setV0(0);
@@ -250,7 +274,7 @@ namespace ps2x::iop::detail
return true;
}
if (it->second.state == IopThreadState::Suspended)
it->second.state = IopThreadState::Ready;
setThreadState(it->second, IopThreadState::Ready);
setV0(0);
return true;
}
@@ -410,7 +434,7 @@ namespace ps2x::iop::detail
setV0(-419);
else if (m_currentThread != nullptr)
{
m_currentThread->state = IopThreadState::Semaphore;
setThreadState(*m_currentThread, IopThreadState::Semaphore);
m_currentThread->waitId = id;
cpu.yielded = true;
setV0(0);
@@ -466,7 +490,7 @@ namespace ps2x::iop::detail
if (best != nullptr && semaphore != m_semaphores.end() && semaphore->second.current > 0)
{
--semaphore->second.current;
best->state = IopThreadState::Ready;
setThreadState(*best, IopThreadState::Ready);
best->waitId = 0;
}
}
@@ -492,7 +516,7 @@ namespace ps2x::iop::detail
thread.cpu.gpr[2] = 0u;
if ((thread.waitMode & 0x10u) != 0u)
event.bits = 0u;
thread.state = IopThreadState::Ready;
setThreadState(thread, IopThreadState::Ready);
thread.waitId = 0;
thread.waitBits = 0;
thread.waitMode = 0;
@@ -553,7 +577,7 @@ namespace ps2x::iop::detail
{
if (thread.state == IopThreadState::EventFlag && thread.waitId == id)
{
thread.state = IopThreadState::Ready;
setThreadState(thread, IopThreadState::Ready);
thread.waitId = 0;
thread.cpu.gpr[2] = static_cast<uint32_t>(-1);
}
@@ -609,7 +633,7 @@ namespace ps2x::iop::detail
setV0(-418);
else if (m_currentThread != nullptr)
{
m_currentThread->state = IopThreadState::EventFlag;
setThreadState(*m_currentThread, IopThreadState::EventFlag);
m_currentThread->waitId = event->second.id;
m_currentThread->waitBits = bits;
m_currentThread->waitMode = mode;
@@ -656,7 +680,7 @@ namespace ps2x::iop::detail
{
if (m_currentThread == nullptr)
return;
m_currentThread->state = IopThreadState::Sleep;
setThreadState(*m_currentThread, IopThreadState::Sleep);
cpu.yielded = true;
}
@@ -665,27 +689,33 @@ namespace ps2x::iop::detail
if (m_currentThread == nullptr)
return;
m_currentThread->wakeCycle = wakeCycle;
m_currentThread->state = IopThreadState::Delay;
setThreadState(*m_currentThread, IopThreadState::Delay);
m_scheduleDirty = true;
cpu.yielded = true;
}
IopThread *IopKernel::beginNextReady(uint64_t currentCycle)
{
for (auto &[id, thread] : m_threads)
if (m_scheduleDirty || currentCycle >= m_nextWakeCycle)
{
if (thread.state == IopThreadState::Delay && thread.wakeCycle <= currentCycle)
thread.state = IopThreadState::Ready;
}
IopThread *next = nullptr;
for (auto &[id, thread] : m_threads)
{
if (thread.state != IopThreadState::Ready)
continue;
if (next == nullptr || thread.priority < next->priority ||
(thread.priority == next->priority && thread.id < next->id))
next = &thread;
m_nextReady = nullptr;
m_nextWakeCycle = UINT64_MAX;
for (auto &[id, thread] : m_threads)
{
if (thread.state == IopThreadState::Delay)
{
if (thread.wakeCycle <= currentCycle)
thread.state = IopThreadState::Ready;
else
m_nextWakeCycle = std::min(m_nextWakeCycle, thread.wakeCycle);
}
if (thread.state == IopThreadState::Ready && (m_nextReady == nullptr || thread.priority < m_nextReady->priority ||
(thread.priority == m_nextReady->priority && thread.id < m_nextReady->id)))
m_nextReady = &thread;
}
m_scheduleDirty = false;
}
IopThread *next = m_nextReady;
if (next == nullptr)
return nullptr;
@@ -698,6 +728,8 @@ namespace ps2x::iop::detail
uint64_t IopKernel::nextWakeCycle(uint64_t fallback) const
{
if (!m_scheduleDirty)
return std::min(fallback, m_nextWakeCycle);
uint64_t nextWake = fallback;
for (const auto &[id, thread] : m_threads)
{
@@ -709,8 +741,8 @@ namespace ps2x::iop::detail
void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel)
{
if (thread.cpu.pc == returnSentinel || thread.cpu.stopped)
thread.state = IopThreadState::Dormant;
if (thread.state != IopThreadState::Dead && (thread.cpu.pc == returnSentinel || thread.cpu.stopped))
setThreadState(thread, IopThreadState::Dormant);
else if (thread.state == IopThreadState::Running)
thread.state = IopThreadState::Ready;
m_currentThread = nullptr;
@@ -719,6 +751,9 @@ namespace ps2x::iop::detail
void IopKernel::cleanupDeadThreads()
{
if (!m_hasDeadThreads)
return;
m_hasDeadThreads = false;
for (auto thread = m_threads.begin(); thread != m_threads.end();)
{
if (thread->second.state != IopThreadState::Dead)
@@ -726,6 +761,14 @@ namespace ps2x::iop::detail
++thread;
continue;
}
if (&thread->second == m_currentThread)
{
thread->second.cpu.stopped = thread->second.cpu.yielded = true;
m_hasDeadThreads = true;
++thread;
continue;
}
m_scheduleDirty = true;
if (thread->second.stackBase != 0u)
(void)m_memory.freeAllocation(thread->second.stackBase);
thread = m_threads.erase(thread);
@@ -738,7 +781,7 @@ namespace ps2x::iop::detail
{
const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc);
if (pc >= base && pc < base + size)
thread.state = IopThreadState::Dead;
setThreadState(thread, IopThreadState::Dead);
}
}
}
+8 -2
View File
@@ -5,6 +5,7 @@
#include <cstddef>
#include <cstdint>
#include <map>
#include <unordered_map>
namespace ps2x::iop::detail
{
@@ -90,14 +91,19 @@ namespace ps2x::iop::detail
void wakeOneSemaphore(int id);
[[nodiscard]] static bool eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode);
void wakeEventWaiters(EventFlag &event);
void setThreadState(IopThread &thread, IopThreadState state);
IopMemory &m_memory;
std::map<int, IopThread> m_threads;
std::map<int, Semaphore> m_semaphores;
std::map<int, EventFlag> m_eventFlags;
std::unordered_map<int, Semaphore> m_semaphores;
std::unordered_map<int, EventFlag> m_eventFlags;
uint32_t m_nextThreadId = 1;
uint32_t m_nextSemaphoreId = 1;
uint32_t m_nextEventFlagId = 1;
IopThread *m_currentThread = nullptr;
IopThread *m_nextReady = nullptr;
uint64_t m_nextWakeCycle = UINT64_MAX;
bool m_scheduleDirty = true;
bool m_hasDeadThreads = false;
};
}
+1
View File
@@ -67,6 +67,7 @@ namespace ps2x::iop::detail
void setInterruptMask(uint32_t value) noexcept { m_interruptMask = value; }
void setInterruptControl(uint32_t value) noexcept { m_interruptControl = value & 1u; }
[[nodiscard]] bool hasDmaStart() const noexcept { return m_dmaStart.has_value(); }
[[nodiscard]] std::optional<DmaStart> takeDmaStart() noexcept;
[[nodiscard]] std::span<const uint8_t> ram() const noexcept { return m_ram; }
+5 -2
View File
@@ -47,8 +47,11 @@ namespace ps2x::iop::detail
std::optional<IopImportCall> IopImportRegistry::decode(uint32_t pc) const
{
if (m_memory.read32(pc) != 0x03E00008u)
return std::nullopt;
return decode(pc, m_memory.read32(pc));
}
std::optional<IopImportCall> IopImportRegistry::decodeStub(uint32_t pc) const
{
const uint32_t delay = m_memory.read32(pc + 4u);
if ((delay & 0xFFFF0000u) != 0x24000000u)
return std::nullopt;
@@ -26,6 +26,12 @@ namespace ps2x::iop::detail
void reset();
[[nodiscard]] std::optional<IopImportCall> decode(uint32_t pc) const;
[[nodiscard]] std::optional<IopImportCall> decode(uint32_t pc, uint32_t instruction) const
{
if (instruction != 0x03E00008u) // Import stubs begin with jr ra.
return std::nullopt;
return decodeStub(pc);
}
[[nodiscard]] bool registerExportTable(uint32_t address);
[[nodiscard]] bool releaseExportTable(uint32_t address);
[[nodiscard]] uint32_t findTable(std::string_view library, std::optional<uint16_t> version = std::nullopt) const;
@@ -34,6 +40,7 @@ namespace ps2x::iop::detail
void eraseRange(uint32_t base, uint32_t size);
private:
[[nodiscard]] std::optional<IopImportCall> decodeStub(uint32_t pc) const;
struct ExportLibrary
{
uint32_t tableAddress = 0;
+17 -7
View File
@@ -125,6 +125,7 @@ namespace ps2x::iop::detail
timrman.reset();
ioman.reset();
pendingDmaInterrupts.clear();
nextDmaInterruptCycle = UINT64_MAX;
pendingGuestCallbacks.clear();
nextModuleId = 1;
moduleCursor = kModuleLoadBase;
@@ -177,7 +178,12 @@ namespace ps2x::iop::detail
void schedulePendingDma()
{
if (const auto dma = memory.takeDmaStart())
{
pendingDmaInterrupts[dma->irq] = totalCycles + dma->delayCycles;
nextDmaInterruptCycle = UINT64_MAX;
for (const auto &[irq, cycle] : pendingDmaInterrupts)
nextDmaInterruptCycle = std::min(nextDmaInterruptCycle, cycle);
}
}
bool readRam(uint32_t address, void *destination, size_t size) const
@@ -378,7 +384,8 @@ namespace ps2x::iop::detail
if (checkInterrupt(cpu))
return true;
if (const auto import = imports.decode(cpu.pc))
const uint32_t instruction = memory.read32(cpu.pc);
if (const auto import = imports.decode(cpu.pc, instruction))
{
const ImportDisposition disposition = dispatchImport(*import, cpu);
++totalInstructions;
@@ -390,8 +397,9 @@ namespace ps2x::iop::detail
return !cpu.stopped;
}
const bool running = cpuCore.executeInstruction(cpu);
schedulePendingDma();
const bool running = cpuCore.executeInstruction(cpu, instruction);
if (memory.hasDmaStart())
schedulePendingDma();
++totalInstructions;
++totalCycles;
return running;
@@ -406,7 +414,7 @@ namespace ps2x::iop::detail
{
if (!step(cpu))
break;
if (!servicingDmaInterrupts && !pendingDmaInterrupts.empty())
if (!servicingDmaInterrupts && totalCycles >= nextDmaInterruptCycle)
servicePendingDmaInterrupts();
if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty())
servicePendingGuestCallbacks();
@@ -483,16 +491,18 @@ namespace ps2x::iop::detail
// Not that good to use exception handling for control flow but will do for now
void servicePendingDmaInterrupts()
{
if (servicingDmaInterrupts || pendingDmaInterrupts.empty())
if (servicingDmaInterrupts || totalCycles < nextDmaInterruptCycle)
return;
servicingDmaInterrupts = true;
std::vector<int> completed;
nextDmaInterruptCycle = UINT64_MAX;
for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();)
{
if (it->second > totalCycles)
{
nextDmaInterruptCycle = std::min(nextDmaInterruptCycle, it->second);
++it;
continue;
}
@@ -567,8 +577,7 @@ namespace ps2x::iop::detail
if (!next)
{
uint64_t nextWake = kernel.nextWakeCycle(target);
for (const auto &[irq, completionCycle] : pendingDmaInterrupts)
nextWake = std::min(nextWake, completionCycle);
nextWake = std::min(nextWake, nextDmaInterruptCycle);
if (!pendingGuestCallbacks.empty())
nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first);
nextWake = timrman.nextEventCycle(nextWake);
@@ -696,6 +705,7 @@ namespace ps2x::iop::detail
IopLoadcore loadcore;
std::map<int, Module> modules;
std::map<int, uint64_t> pendingDmaInterrupts;
uint64_t nextDmaInterruptCycle = UINT64_MAX;
std::multimap<uint64_t, ScheduledGuestCallback> pendingGuestCallbacks;
uint32_t nextModuleId = 1;
uint32_t moduleCursor = kModuleLoadBase;
+230
View File
@@ -0,0 +1,230 @@
#include "iop_compat_test_support.h"
#include "emulator/iop_emulator.h"
#include "emulator/core/iop_cpu.h"
#include "emulator/core/iop_memory.h"
#include "emulator/imports/iop_imports.h"
namespace
{
using namespace iop_test;
using namespace ps2x::iop::detail;
// Real guest stores start SPU DMA; intrman invokes the guest handler below.
struct DmaFixture
{
Host host;
IopEmulator iop{host};
Irx image{0x10000, 0x900};
uint32_t offset = 0;
std::array<uint32_t, 2> starts{};
std::array<uint32_t, 2> delays{};
std::array<uint64_t, 2> deadlines{};
DmaFixture()
{
emit({0x27bdfff0, 0xafbf000c}); // save ra
for (unsigned irq : {0x24u, 0x28u})
{
emit({0x24040000 | irq, 0x24050000, 0x3c060001, 0x34c60300,
0x24070000 | irq, 0x0c004185, 0}); // RegisterIntrHandler
emit({0x24040000 | irq, 0x0c004187, 0}); // EnableIntr
}
image.words(0x600, {0x41e00000, 0, 0x0101, 0x72746e69, 0x006e616d,
0x03e00008, 0x24000004, 0x03e00008, 0x24000006, 0, 0});
image.words(0x300, {
0x3c080001, 0x8d090800, 0, // t1 = completed count (load delay)
0x00095080, 0x01485021, 0xad440810, // trace[count] = irq
0x25290001, 0xad090800,
0x8d090804, 0, 0x1120000c, 0, // optionally rearm DMA from the handler
0xad000804, 0x3c091f80, 0x352910c0,
0x240a0020, 0xad2a0004, 0x3c0a0100, 0xad2a0008,
0x24090050, 0x2529ffff, 0x1520fffe, 0, // handler runs past the new deadline
0x03e00008, 0,
});
}
void emit(std::initializer_list<uint32_t> words)
{
image.words(offset, words);
offset += uint32_t(words.size()) * 4;
}
void start(unsigned channel, uint32_t delay)
{
emit({0x3c081f80, channel ? 0x35081500u : 0x350810c0u,
0x24090000 | (delay / 2), 0xad090004, 0x3c090100});
starts[channel] = offset;
delays[channel] = delay;
emit({0xad090008});
}
void load()
{
const std::initializer_list<uint32_t> epilogue{0x8fbf000c, 0x27bd0010, 0x00001021, 0x03e00008, 0};
image.words(offset, epilogue);
const uint32_t entryEnd = offset + uint32_t(epilogue.size()) * 4;
image.install(host);
const auto result = iop.loadModuleBuffer(0x1000, nullptr, 0);
require(result.moduleId > 0 && result.startResult == 0, "DMA IRX initialization failed");
for (unsigned channel = 0; channel < 2; ++channel)
if (delays[channel])
deadlines[channel] = iop.cycles() - (entryEnd - starts[channel]) / 4 + delays[channel];
}
uint32_t word(uint32_t address)
{
uint32_t value = 0;
require(iop.readMemory(address, &value, sizeof(value)), "IOP trace read failed");
return value;
}
void advanceTo(uint64_t cycle)
{
require(cycle >= iop.cycles(), "test attempted to reverse time");
iop.runEeCycles((cycle - iop.cycles()) * 8);
}
};
void dmaDeadlines()
{
DmaFixture f;
f.start(0, 1000);
f.start(1, 200);
f.load();
f.advanceTo(f.deadlines[1]);
require(f.word(0x10800) == 0, "idle DMA dispatched before its deadline was serviced");
f.iop.runEeCycles(8);
require(f.word(0x10800) == 1 && f.word(0x10810) == 0x28, "earliest DMA did not wake the idle IOP");
f.advanceTo(f.deadlines[0]);
require(f.word(0x10800) == 1, "later DMA dispatched early");
f.iop.runEeCycles(8);
require(f.word(0x10800) == 2 && f.word(0x10814) == 0x24, "later DMA was lost");
f.iop.runEeCycles(8000);
require(f.word(0x10800) == 2, "DMA completion dispatched twice");
}
void dmaReplacementAndReset()
{
DmaFixture f;
f.start(0, 200);
f.start(1, 400);
f.start(0, 1000); // replace the earliest event with a later one
f.load();
f.advanceTo(f.deadlines[1]);
require(f.word(0x10800) == 0, "replaced DMA deadline survived");
f.iop.runEeCycles(8);
require(f.word(0x10800) == 1 && f.word(0x10810) == 0x28, "replacement hid the other channel");
f.iop.reset();
f.iop.runEeCycles(16000);
require(f.iop.instructions() == 0, "reset retained a DMA callback");
f.load();
f.advanceTo(f.deadlines[1]);
f.iop.runEeCycles(8);
require(f.word(0x10800) == 1, "DMA scheduling did not recover after reset");
}
void dmaReentrantHandler()
{
DmaFixture f;
f.start(0, 200);
f.image.words(0x804, {1});
f.load();
f.advanceTo(f.deadlines[0]);
f.iop.runEeCycles(8);
require(f.word(0x10800) == 1, "DMA handler recursively dispatched a new completion");
f.iop.runEeCycles(8);
require(f.word(0x10800) == 2 && f.word(0x10814) == 0x24,
"DMA started inside its handler was lost");
}
void dmaEqualDeadlines()
{
DmaFixture f;
f.start(1, 206);
f.start(0, 200); // Six instructions later: same completion cycle.
f.load();
require(f.deadlines[0] == f.deadlines[1], "fixture deadlines differ");
f.advanceTo(f.deadlines[0]);
f.iop.runEeCycles(8);
require(f.word(0x10800) == 2 && f.word(0x10810) == 0x24 && f.word(0x10814) == 0x28,
"simultaneous DMA callbacks changed IRQ order");
}
void dmaWhileExecuting()
{
DmaFixture f;
f.start(0, 200);
// Keep guest execution active beyond completion; no idle path is involved.
f.emit({0x24100050, 0x2610ffff, 0x1600fffe, 0});
f.load();
require(f.word(0x10800) == 1 && f.word(0x10810) == 0x24,
"DMA did not dispatch during guest execution");
}
void instructionChanges()
{
IopMemory memory;
IopCpuCore core(memory);
IopImportRegistry imports(memory);
IopCpuState cpu{};
const auto step = [&]()
{
const uint32_t instruction = memory.read32(cpu.pc);
require(!imports.decode(cpu.pc, instruction), "ordinary instruction mistaken for an import");
return core.executeInstruction(cpu, instruction);
};
cpu.pc = 0x80001000;
memory.write32(0x1000, 0x24020011); // addiu v0, zero, 0x11
require(step() && cpu.gpr[2] == 0x11, "first instruction failed");
cpu.pc = 0x80001000;
memory.write32(0x1000, 0x24020022);
require(step() && cpu.gpr[2] == 0x22, "modified instruction was not refetched");
cpu.pc = 0x1000;
memory.write32(0x1000, 0x8c020800); // lw v0, 0x800(zero)
memory.write32(0x1004, 0x24430001); // addiu v1, v0, 1 (sees old v0)
memory.write32(0x800, 0x44);
require(step() && cpu.gpr[2] == 0x22, "load delay changed");
require(step() && cpu.gpr[2] == 0x44 && cpu.gpr[3] == 0x23,
"instruction sharing changed load delay semantics");
cpu.pc = 0x1000;
memory.write32(0x1000, 0x10000001); // beq zero, zero, +1
memory.write32(0x1004, 0x24020033);
require(step() && cpu.pc == 0x1004 && cpu.branchPending, "branch delay slot was skipped");
require(step() && cpu.pc == 0x1008 && cpu.gpr[2] == 0x33, "branch delay slot did not execute");
}
void modifiedImportStub()
{
IopMemory memory;
IopImportRegistry imports(memory);
memory.write32(0x1000, 0x41e00000);
memory.write16(0x1008, 0x0101);
constexpr char name[8] = "tstlib";
require(memory.writeRam(0x100c, name, sizeof(name)), "import name write failed");
memory.write32(0x1014, 0x03e00008);
memory.write32(0x1018, 0x24000003);
const auto decode = [&]() { return imports.decode(0x80001014, memory.read32(0x80001014)); };
auto call = decode();
require(call && call->ordinal == 3 && call->version == 0x0101, "shared fetch lost import metadata");
memory.write16(0x1008, 0x0102);
memory.write32(0x1018, 0x24000007);
call = decode();
require(call && call->ordinal == 7 && call->version == 0x0102, "import metadata became stale");
memory.write32(0x1014, 0x24020011);
require(!decode(), "patched import stub still dispatched as an import");
}
}
int main()
{
const Test tests[] = {
{"DMA deadlines and idle wakeup", dmaDeadlines},
{"DMA replacement and reset", dmaReplacementAndReset},
{"DMA scheduled from a running IRQ handler", dmaReentrantHandler},
{"Simultaneous DMA IRQ order", dmaEqualDeadlines},
{"DMA dispatch while guest instructions execute", dmaWhileExecuting},
{"Modified instructions and load delay", instructionChanges},
{"Modified import stub and metadata", modifiedImportStub},
};
return run(tests);
}
+123 -1
View File
@@ -274,6 +274,128 @@ namespace
return passed;
}
bool testKernelSchedulingTransitions()
{
IopMemory memory;
IopKernel kernel(memory);
IopCpuState caller{};
const auto createThread = [&](uint32_t entry, uint32_t priority)
{
memory.write32(0x2008, entry);
memory.write32(0x200c, 0x100);
memory.write32(0x2010, priority);
caller.gpr[4] = 0x2000;
(void)kernel.dispatchThreadImport(4, caller, 0);
const int id = static_cast<int>(caller.gpr[2]);
caller.gpr[4] = static_cast<uint32_t>(id);
(void)kernel.dispatchThreadImport(6, caller, 0);
return id;
};
const auto select = [&](int id, uint64_t cycle)
{
IopThread *thread = kernel.beginNextReady(cycle);
if (!expect(thread && thread->id == id, "scheduler selected the wrong thread"))
return static_cast<IopThread *>(nullptr);
return thread;
};
const auto threadImport = [&](uint16_t ordinal, int id, uint32_t value = 0)
{
caller.gpr[4] = static_cast<uint32_t>(id);
caller.gpr[5] = value;
return kernel.dispatchThreadImport(ordinal, caller, 0) && caller.gpr[2] == 0;
};
constexpr uint32_t sentinel = 0xfffffffcu;
const int first = createThread(0x10000, 32);
const int second = createThread(0x20000, 64);
for (int i = 0; i < 128; ++i)
{
auto *thread = select(first, 0);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
}
if (!threadImport(14, second, 16)) return false;
auto *thread = select(second, 0);
if (!thread) return false;
kernel.delayCurrentUntil(100, thread->cpu);
kernel.endTimeslice(*thread, sentinel);
if (!expect(kernel.nextWakeCycle(1000) == 100, "delay deadline lost after blocking")) return false;
thread = select(first, 99);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
thread = select(second, 100);
if (!thread) return false;
kernel.sleepCurrent(thread->cpu);
kernel.endTimeslice(*thread, sentinel);
thread = select(first, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!threadImport(25, second)) return false;
thread = select(second, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!threadImport(29, second)) return false;
thread = select(first, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!threadImport(31, second)) return false;
memory.write32(0x202c, 1); // semaphore maximum, initially empty
caller.gpr[4] = 0x2020;
(void)kernel.dispatchSemaphoreImport(4, caller);
const uint32_t semaphore = caller.gpr[2];
thread = select(second, 101);
if (!thread) return false;
thread->cpu.gpr[4] = semaphore;
(void)kernel.dispatchSemaphoreImport(8, thread->cpu);
kernel.endTimeslice(*thread, sentinel);
thread = select(first, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
caller.gpr[4] = semaphore;
(void)kernel.dispatchSemaphoreImport(6, caller);
thread = select(second, 101);
if (!thread) return false;
const int event = kernel.createInternalEventFlag(0, 0, 0);
thread->cpu.gpr[4] = static_cast<uint32_t>(event);
thread->cpu.gpr[5] = 1;
thread->cpu.gpr[6] = 0;
thread->cpu.gpr[7] = 0;
(void)kernel.dispatchEventImport(10, thread->cpu);
kernel.endTimeslice(*thread, sentinel);
thread = select(first, 101);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!kernel.setInternalEventFlag(event, 1)) return false;
const int third = createThread(0x30000, 16);
thread = select(second, 101); // Equal priorities retain ascending ID order.
if (!thread) return false;
(void)kernel.dispatchThreadImport(9, thread->cpu, 101);
kernel.endTimeslice(*thread, sentinel);
if (!expect(kernel.threadCount() == 2, "ExitDeleteThread must not turn Dead into Dormant")) return false;
thread = select(third, 101);
if (!thread) return false;
kernel.terminateThreadsInRange(0x30000, 0x100);
kernel.cleanupDeadThreads();
if (!expect(kernel.threadCount() == 2 && thread->cpu.stopped, "active CPU reference must survive module unload")) return false;
kernel.endTimeslice(*thread, sentinel);
if (!expect(kernel.threadCount() == 1, "unloaded thread must retire after its timeslice")) return false;
thread = select(first, 101);
if (!thread) return false;
kernel.delayCurrentUntil(1000, thread->cpu);
kernel.endTimeslice(*thread, sentinel);
if (!expect(kernel.beginNextReady(999) == nullptr && kernel.nextWakeCycle(2000) == 1000,
"idle scheduling must preserve the next wake deadline")) return false;
thread = select(first, 1000);
if (!thread) return false;
kernel.endTimeslice(*thread, sentinel);
if (!threadImport(5, first)) return false;
if (!expect(kernel.beginNextReady(1000) == nullptr, "deletion must invalidate the cached ready thread")) return false;
kernel.reset();
return expect(kernel.beginNextReady(0) == nullptr && kernel.threadCount() == 0,
"reset must discard scheduling state");
}
bool testTimrmanPeriodicCallback()
{
IopTimrman timrman;
@@ -331,7 +453,7 @@ namespace
int main()
{
if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() ||
!testTimrmanPeriodicCallback())
!testTimrmanPeriodicCallback() || !testKernelSchedulingTransitions())
return 1;
std::cout << "ps2xIOP import tests passed\n";
return 0;
+17 -2
View File
@@ -5,13 +5,23 @@ project(PS2Runtime VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(PS2X_GS_BACKEND "PARALLEL_GS" CACHE STRING "GS backend: CPU or PARALLEL_GS")
set_property(CACHE PS2X_GS_BACKEND PROPERTY STRINGS CPU PARALLEL_GS)
if(NOT PS2X_GS_BACKEND MATCHES "^(CPU|PARALLEL_GS)$")
message(FATAL_ERROR "PS2X_GS_BACKEND must be CPU or PARALLEL_GS")
endif()
if(PS2X_GS_BACKEND STREQUAL "PARALLEL_GS" AND
(ANDROID OR NOT (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux")))
message(FATAL_ERROR "PARALLEL_GS is supported only on Windows and Linux desktop")
endif()
option(PS2X_ENABLE_RUNNER_UNITY_BUILD "Build ps2EntryRunner with CMake unity build" ON)
set(PS2X_RUNNER_UNITY_BUILD_BATCH_SIZE 32 CACHE STRING "Unity build batch size for ps2EntryRunner")
option(PS2X_ENABLE_RUNNER_PCH "Precompile the heavy runtime headers for ps2EntryRunner" ON)
option(PS2X_ENABLE_SCCACHE "Use sccache as compiler launcher when available" ON)
option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" ON)
option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" ON)
option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" OFF)
option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" OFF)
option(PS2X_ENABLE_IOP_RPC_TRACE "Log unhandled IOP/SIF RPC trace suggestions" ON)
option(PS2X_STRICT_RETURN_DIAGNOSTICS "Route generated JR $ra returns through runtime branch diagnostics" OFF)
option(PS2X_SHOW_WINDOWS_CONSOLE "Show a console window for ps2EntryRunner on Windows release builds" ON)
@@ -396,6 +406,11 @@ add_library(ps2_runtime STATIC
src/lib/games_database.cpp
)
if(PS2X_GS_BACKEND STREQUAL "PARALLEL_GS")
message(STATUS "Using ParallelGS backend for PS2 graphics")
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ParallelGS.cmake")
endif()
if(PS2X_ENABLE_RUNTIME_LOGS)
target_compile_definitions(ps2_runtime PUBLIC
PS2_RUNTIME_LOGS=1
+44
View File
@@ -0,0 +1,44 @@
include(FetchContent)
set(PARALLEL_GS_STANDALONE ON CACHE BOOL "Build parallel-gs as a library" FORCE)
set(GRANITE_TOOLS OFF CACHE BOOL "" FORCE)
set(GRANITE_INSTALL_TARGETS OFF CACHE BOOL "" FORCE)
if(MSVC AND NOT CMAKE_MSVC_RUNTIME_LIBRARY)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
FetchContent_Declare(parallel_gs
GIT_REPOSITORY https://github.com/Arntzen-Software/parallel-gs.git
GIT_TAG 3a66c1976170cbc2cb53a3593fabbc7c4b2ccfbd
GIT_SUBMODULES_RECURSE TRUE
EXCLUDE_FROM_ALL
)
# Workaround for my pc large path remove this later hahaha
if(CMAKE_HOST_WIN32)
set(_ps2x_git_config_count "$ENV{GIT_CONFIG_COUNT}")
set(_ps2x_git_config_index 0)
if(NOT _ps2x_git_config_count STREQUAL "")
set(_ps2x_git_config_index "${_ps2x_git_config_count}")
endif()
set(ENV{GIT_CONFIG_KEY_${_ps2x_git_config_index}} "core.longpaths")
set(ENV{GIT_CONFIG_VALUE_${_ps2x_git_config_index}} "true")
math(EXPR _ps2x_git_config_next "${_ps2x_git_config_index} + 1")
set(ENV{GIT_CONFIG_COUNT} "${_ps2x_git_config_next}")
endif()
FetchContent_MakeAvailable(parallel_gs)
if(CMAKE_HOST_WIN32)
set(ENV{GIT_CONFIG_COUNT} "${_ps2x_git_config_count}")
unset(ENV{GIT_CONFIG_KEY_${_ps2x_git_config_index}})
unset(ENV{GIT_CONFIG_VALUE_${_ps2x_git_config_index}})
endif()
include("${CMAKE_CURRENT_LIST_DIR}/ParallelGSTransferTails.cmake")
target_sources(ps2_runtime PRIVATE
"${CMAKE_CURRENT_LIST_DIR}/../src/lib/gs/gs_parallel_backend.cpp"
"${CMAKE_CURRENT_LIST_DIR}/../src/lib/gs/gs_gl_interop.cpp"
)
target_link_libraries(ps2_runtime PRIVATE parallel-gs)
target_compile_definitions(ps2_runtime PUBLIC PS2X_GS_PARALLEL=1)
install(FILES "${parallel_gs_SOURCE_DIR}/COPYING.LGPLv3" DESTINATION share/licenses/parallel-gs)
@@ -0,0 +1,24 @@
file(READ "${parallel_gs_SOURCE_DIR}/gs/gs_interface.cpp" gs_interface_source)
function(patch_gs_transfer before after)
string(FIND "${gs_interface_source}" "${before}" position)
if(position EQUAL -1)
message(FATAL_ERROR "parallel-gs transfer patch does not match the pinned source")
endif()
string(REPLACE "${before}" "${after}" gs_interface_source "${gs_interface_source}")
set(gs_interface_source "${gs_interface_source}" PARENT_SCOPE)
endfunction()
patch_gs_transfer("get_bits_per_pixel(transfer_state.copy.bitbltbuf.desc.DPSM)) / 64;" "get_bits_per_pixel(transfer_state.copy.bitbltbuf.desc.DPSM) + 63) / 64;")
patch_gs_transfer("get_bits_per_pixel(transfer_state.copy.bitbltbuf.desc.SPSM)) / 8;" "get_bits_per_pixel(transfer_state.copy.bitbltbuf.desc.SPSM) + 7) / 8;")
patch_gs_transfer("transfer_state.fifo_readback.reserve(required_bytes);" "transfer_state.fifo_readback.reserve((required_bytes + 15) & ~15u);\n\t\tif (required_bytes) memset(transfer_state.fifo_readback.data(), 0, (required_bytes + 15) & ~15u);")
patch_gs_transfer("transfer_state.fifo_readback_128b_size = required_bytes / 16;" "transfer_state.fifo_readback_128b_size = (required_bytes + 15) / 16;")
set(patched_interface "${parallel_gs_BINARY_DIR}/gs_interface_transfer_tails.cpp")
file(CONFIGURE OUTPUT "${patched_interface}" CONTENT "${gs_interface_source}" @ONLY)
get_target_property(gs_sources parallel-gs SOURCES)
list(REMOVE_ITEM gs_sources gs_interface.cpp)
set_property(TARGET parallel-gs PROPERTY SOURCES "${gs_sources}")
target_sources(parallel-gs PRIVATE "${patched_interface}")
+10
View File
@@ -1,6 +1,10 @@
#ifndef PS2_DEBUG_PANEL_H
#define PS2_DEBUG_PANEL_H
#include <array>
#include <chrono>
#include <cstdint>
class PS2Runtime;
class PS2DebugPanel
@@ -20,6 +24,12 @@ private:
bool m_showRegisters = true;
unsigned int m_memoryAddress = 0x00100000u;
unsigned int m_memoryBytes = 0x100u;
std::chrono::steady_clock::time_point m_fpsSampleStart{};
std::array<uint64_t, 2> m_lastDisplayFlips{};
uint64_t m_lastSdkPresents = 0;
uint64_t m_hostFramesInSample = 0;
double m_gameFps = 0.0;
double m_hostFps = 0.0;
};
#endif // PS2_DEBUG_PANEL_H
+2
View File
@@ -31,6 +31,8 @@
ps2_log::append_runtime_log_text(_ps2_runtime_error_text); \
} while (0)
#define RUNTIME_WARNING(x) std::cout << x
namespace ps2_log
{
struct RuntimeLogEntry
@@ -5,6 +5,8 @@
#include <cstdint>
#include <vector>
struct GSDebugSnapshot;
class GSRasterBackend
{
public:
@@ -12,6 +14,12 @@ public:
virtual void Initialize(uint8_t *vram, uint32_t vramSize) = 0;
virtual void Reset() = 0;
virtual bool UsesRawCommands() const { return false; }
virtual void ProcessGIF(uint32_t, const uint8_t *, uint32_t) {}
virtual void WriteRegisterRaw(uint8_t, uint64_t) {}
virtual void ReadRegisterState(GSDebugSnapshot &) const {}
virtual uint64_t GetReadbackCount() const { return 0; }
virtual void Submit(const GSPrimitiveBatch &batch) = 0;
virtual void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) = 0;
+8 -3
View File
@@ -105,8 +105,10 @@ public:
void init(uint8_t *vram, uint32_t vramSize, struct GSRegisters *privRegs = nullptr);
void reset();
void setRasterBackend(std::unique_ptr<GSRasterBackend> backend);
void shutdownBackend();
uint64_t getReadbackCount() const;
void processGIFPacket(const uint8_t *data, uint32_t sizeBytes);
void processGIFPacket(const uint8_t *data, uint32_t sizeBytes, uint32_t path = 3);
bool processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes);
void uploadImageNative(uint64_t bitbltbuf,
uint64_t trxpos,
@@ -119,9 +121,9 @@ public:
const uint8_t *lockDisplaySnapshot(uint32_t &outSize);
void unlockDisplaySnapshot();
uint32_t getLastDisplayBaseBytes() const;
const GSFrameReg &getContextFrame(int index) const
GSFrameReg getContextFrame(int index) const
{
return m_ctx[(index != 0) ? 1 : 0].frame;
return getDebugSnapshot().ctx[(index != 0) ? 1 : 0].frame;
}
GSDebugSnapshot getDebugSnapshot() const;
std::vector<GSDebugHistoryEntry> getDebugHistory() const;
@@ -130,6 +132,7 @@ public:
void setDebugHistoryPaused(bool paused);
bool getPreferredDisplaySource(GSFrameReg &outSource, uint32_t &outDestFbp) const;
void latchHostPresentationFrame();
std::shared_ptr<GSGpuFrame> getLatchedGpuFrame(uint32_t &width, uint32_t &height, float &aspectRatio) const;
bool copyLatchedHostPresentationFrame(std::vector<uint8_t> &outPixels,
uint32_t &outWidth,
uint32_t &outHeight,
@@ -223,6 +226,8 @@ private:
uint32_t m_preferredDisplayDestFbp = 0;
bool m_hasPreferredDisplaySource = false;
std::vector<uint8_t> m_hostPresentationFrame;
std::shared_ptr<GSGpuFrame> m_hostPresentationGpuFrame;
float m_hostPresentationAspectRatio = 0.0f;
uint32_t m_hostPresentationWidth = 0;
uint32_t m_hostPresentationHeight = 0;
uint32_t m_hostPresentationDisplayFbp = 0;
@@ -0,0 +1,6 @@
#pragma once
#include "runtime/gs/gs_backend.h"
struct GSRegisters;
std::unique_ptr<GSRasterBackend> CreateParallelGSBackend(GSRegisters &registers);
+15 -1
View File
@@ -4,6 +4,7 @@
#include <array>
#include <cstdint>
#include <vector>
#include <memory>
enum GSPrimType : uint8_t
{
@@ -277,6 +278,8 @@ struct GSTransferSnapshot
struct GSPresentationRequest
{
uint64_t pmode = 0;
uint64_t smode1 = 0;
uint64_t syncv = 0;
uint64_t smode2 = 0;
uint64_t dispfb1 = 0;
uint64_t display1 = 0;
@@ -284,24 +287,35 @@ struct GSPresentationRequest
uint64_t display2 = 0;
uint64_t bgcolor = 0;
uint64_t vsyncTick = 0;
uint32_t field = 0;
GSFrameReg contextFrames[2]{};
GSFrameReg preferredSource{};
uint32_t preferredDestFbp = 0;
bool hasPreferredSource = false;
};
class GSGpuFrame
{
public:
virtual ~GSGpuFrame() = default;
virtual uint32_t AcquireTexture() = 0;
virtual void ReleaseTexture() = 0;
};
struct PresentationFrame
{
std::vector<uint8_t> pixels;
std::shared_ptr<GSGpuFrame> gpu;
uint32_t width = 0;
uint32_t height = 0;
uint32_t displayFbp = 0;
uint32_t sourceFbp = 0;
bool usedPreferred = false;
float aspectRatio = 0.0f;
explicit operator bool() const
{
return !pixels.empty() && width != 0u && height != 0u;
return (gpu || !pixels.empty()) && width != 0u && height != 0u;
}
};
@@ -23,10 +23,12 @@ struct GifArbiterPacket
class GifArbiter
{
public:
using ProcessPacketFn = std::function<void(const uint8_t *, uint32_t)>;
using ProcessPacketFn = std::function<void(GifPathId, const uint8_t *, uint32_t)>;
GifArbiter() = default;
explicit GifArbiter(ProcessPacketFn processFn);
explicit GifArbiter(std::function<void(const uint8_t *, uint32_t)> processFn)
: GifArbiter([fn = std::move(processFn)](GifPathId, const uint8_t *data, uint32_t size) { fn(data, size); }) {}
void setProcessPacketFn(ProcessPacketFn fn) { m_processFn = std::move(fn); }
+16 -1
View File
@@ -210,8 +210,23 @@ struct GSRegisters
uint64_t imr; // Interrupt mask
uint64_t busdir; // Bus direction
uint64_t siglblid; // Signal label ID
// Host diagnostics, not guest registers. Count enabled CRTC buffer switches,
// independently of host redraws and without counting both circuits as frames.
std::atomic<uint64_t> displayFlipCount[2]{};
// Completed libgs swaps can present a frame without changing DISPFB.FBP.
std::atomic<uint64_t> sdkPresentCount{0};
void writeDisplayFramebuffer(unsigned circuit, uint64_t value)
{
uint64_t &reg = circuit == 0 ? dispfb1 : dispfb2;
// FBP only: field offsets and display configuration changes are not flips.
if (((reg ^ value) & 0x1ffu) != 0 && (pmode & (1ull << circuit)))
displayFlipCount[circuit].fetch_add(1, std::memory_order_relaxed);
reg = value;
}
};
static_assert(sizeof(GSRegisters) == (20u * sizeof(uint64_t)), "GSRegisters layout changed unexpectedly");
static_assert(offsetof(GSRegisters, displayFlipCount) == (20u * sizeof(uint64_t)), "GSRegisters register layout changed unexpectedly");
static_assert(alignof(GSRegisters) == alignof(uint64_t), "GSRegisters alignment must remain 64-bit");
static_assert(std::atomic<uint64_t>::is_always_lock_free, "GS CSR atomic must be lock-free on all supported targets");
+68 -13
View File
@@ -2,6 +2,9 @@
#define PS2_VU1_H
#include <array>
#include <bit>
#include <cstddef>
#include <limits>
#include <cstdint>
class GS;
@@ -102,6 +105,11 @@ private:
bool reserved = false;
};
static constexpr uint32_t kVfReadyCount = 32u * 4u;
static constexpr uint32_t kViReadyBase = kVfReadyCount;
static constexpr uint32_t kAccReadyBase = kViReadyBase + 16u;
static constexpr uint32_t kRegisterReadyCount = kAccReadyBase + 4u;
struct DecodedInstructionPair
{
uint32_t lower = 0;
@@ -113,8 +121,9 @@ private:
bool mBit = false;
bool dBit = false;
bool tBit = false;
uint8_t upperVfShadowReg = 0;
uint8_t suppressedLowerVf = 0;
std::array<uint8_t, 4u * 4u + 15u + 4u> readDependencies{};
uint8_t readDependencyCount = 0;
};
struct FlagPipelineEntry
@@ -189,6 +198,13 @@ private:
uint64_t issueCycle = 0;
bool active = false;
bool currentTagEop = false;
void reset()
{
sourceAddress = totalBytes = copiedBytes = currentTagEnd = cycleCredit = 0;
issueCycle = 0;
active = currentTagEop = false;
}
};
static constexpr uint32_t kFmacLatency = 4u;
@@ -203,6 +219,8 @@ private:
Unit m_unit;
VU1State m_state;
std::array<DecodedInstructionPair, kMaxDecodedPairs> m_decodedCodeCache{};
std::array<uint64_t, kMaxDecodedPairs / 64u> m_decodedPairValid{};
DecodedInstructionPair m_uncachedDecoded{};
const uint8_t *m_cachedVuCode = nullptr;
const PS2Memory *m_cachedMemory = nullptr;
uint32_t m_cachedCodeSize = 0;
@@ -216,10 +234,21 @@ private:
std::array<PendingVfWrite, kMaxPendingVfWrites> m_vfWritePipeline{};
std::array<PendingViWrite, kMaxPendingViWrites> m_viWritePipeline{};
std::array<PendingAccWrite, kMaxPendingAccWrites> m_accWritePipeline{};
uint32_t m_flagActive = 0;
uint32_t m_efuActive = 0;
uint32_t m_storeActive = 0;
uint32_t m_vfWriteActive = 0;
uint32_t m_viWriteActive = 0;
uint32_t m_accWriteActive = 0;
static constexpr uint64_t kNoPipelineEvent = std::numeric_limits<uint64_t>::max();
uint64_t m_nextPipelineCycle = kNoPipelineEvent;
bool m_schedulerClean = true;
XgkickPipeline m_xgkick{};
std::array<std::array<uint64_t, 4>, 32> m_vfReady{};
std::array<uint64_t, 16> m_viReady{};
std::array<uint64_t, 4> m_accReady{};
std::array<uint64_t, kRegisterReadyCount> m_registerReady{};
std::array<std::array<uint64_t, 4>, 32> m_vfLatestWrite{};
std::array<uint64_t, 16> m_viLatestWrite{};
std::array<uint64_t, 4> m_accLatestWrite{};
@@ -229,6 +258,10 @@ private:
uint64_t m_efuResourceReady = 0;
uint32_t m_workingClip = 0;
uint32_t m_currentUpperInstruction = 0;
struct UpperOperands
{
float vs[4], vt[4], acc[4], q, i;
} m_upperOperands{};
int32_t m_viBranchBackupValue = 0;
uint8_t m_viBranchBackupReg = 0;
bool m_viBranchBackupValid = false;
@@ -248,18 +281,15 @@ private:
InstructionUsage decodeLowerUsage(uint32_t lower) const;
static void addVfRead(InstructionUsage &usage, uint8_t reg, uint8_t lanes);
static void addVfWrite(InstructionUsage &usage, uint8_t reg, uint8_t lanes);
static uint8_t vfReadLanes(const InstructionUsage &usage, uint8_t reg);
DecodedInstructionPair decodeInstructionPair(const uint8_t *vuCode, uint32_t pc) const;
DecodedInstructionPair getDecodedInstructionPairForPc(const uint8_t *vuCode, uint32_t codeSize, PS2Memory *memory, uint32_t pc);
void rebuildDecodedCodeCache(const uint8_t *vuCode, uint32_t codeSize, const PS2Memory *memory, uint64_t generation);
const DecodedInstructionPair &getDecodedInstructionPairForPc(const uint8_t *vuCode, uint32_t codeSize, PS2Memory *memory, uint32_t pc);
void invalidateDecodedCodeCache(const uint8_t *vuCode, uint32_t codeSize, const PS2Memory *memory, uint64_t generation);
void execUpper(uint32_t instr);
void execUpper(uint32_t instr, float *vfResult, float *accResult);
void execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr);
void applyDest(float *dst, const float *result, uint8_t dest);
void applyDestAcc(const float *result, uint8_t dest);
void applyFmacDest(float *dst, float *result, uint8_t dest);
void applyFmacDestAcc(float *result, uint8_t dest);
void normalizeFmacResult(float *result, uint8_t dest, uint8_t laneFlags[4]);
bool calculateFmacExactResult(uint32_t component, long double &result) const;
uint8_t normalizeFmacExactResult(float &value, long double exactResult) const;
@@ -276,24 +306,49 @@ private:
void queueAccWrite(uint8_t laneMask, const float value[4], uint32_t latency);
void startXgkick(uint32_t qwordAddress);
template <typename Entry, std::size_t Capacity>
Entry *allocatePipelineEntry(std::array<Entry, Capacity> &entries, uint32_t &active, uint64_t readyCycle)
{
static_assert(Capacity > 0 && Capacity <= 32);
const uint32_t slot = std::countr_zero(~active);
if (slot >= Capacity)
return nullptr;
active |= 1u << slot;
Entry &entry = entries[slot];
entry = {};
entry.valid = true;
entry.readyCycle = readyCycle;
if (readyCycle < m_nextPipelineCycle)
m_nextPipelineCycle = readyCycle;
return &entry;
}
void resetScheduler();
void commitReadyPipelines();
void advanceOneCycle();
void advanceTo(uint64_t targetCycle);
void flushPipelines();
void progressXgkick();
void progressXgkick(uint32_t elapsedCycles = 1u);
void finishXgkick();
uint64_t calculatePairReadyCycle(const DecodedInstructionPair &decoded) const;
void markPairWrites(const DecodedInstructionPair &decoded);
bool pipelinesPending() const;
float normalizeOperand(float value) const;
static float normalizeOperand(float value)
{
uint32_t bits = std::bit_cast<uint32_t>(value);
const uint32_t exponent = bits & 0x7F800000u;
if (exponent == 0u)
bits &= 0x80000000u;
else if (exponent == 0x7F800000u)
bits = (bits & 0x80000000u) | 0x7F7FFFFFu;
return std::bit_cast<float>(bits);
}
float normalizeResult(float value, uint32_t &laneFlags) const;
uint32_t microAddressMask() const;
int32_t readBranchVi(uint8_t reg) const;
void recordViWriteForBranch(uint8_t reg, int32_t oldValue);
void reportReservedInstruction(bool upper, uint32_t instruction);
float broadcast(const float *vf, uint8_t bc);
};
#endif
+23 -78
View File
@@ -1,5 +1,6 @@
#include "Common.h"
#include "GS.h"
#include "../Syscalls/System.h"
#include "ps2_log.h"
#include "runtime/gs/ps2_gs_common.h"
#include "runtime/gs/ps2_gs_psmct16.h"
@@ -11,8 +12,7 @@ namespace ps2_stubs
{
uint64_t makeClearPrim(bool useContext2)
{
return static_cast<uint64_t>(GS_PRIM_SPRITE) |
(static_cast<uint64_t>(useContext2 ? 1u : 0u) << 9);
return static_cast<uint64_t>(GS_PRIM_SPRITE) | (static_cast<uint64_t>(useContext2 ? 1u : 0u) << 9);
}
uint64_t makeClearRgbaq(uint32_t rgba)
@@ -22,8 +22,7 @@ namespace ps2_stubs
uint64_t makeClearXyz(int32_t x, int32_t y)
{
return static_cast<uint64_t>(static_cast<uint16_t>(x << 4)) |
(static_cast<uint64_t>(static_cast<uint16_t>(y << 4)) << 16);
return static_cast<uint64_t>(static_cast<uint16_t>(x << 4)) | (static_cast<uint64_t>(static_cast<uint16_t>(y << 4)) << 16);
}
void seedGsClearPacket(GsClearMem &clear,
@@ -798,55 +797,20 @@ namespace ps2_stubs
return;
}
g_gparam.interlace = static_cast<uint8_t>(interlace & 0x1);
g_gparam.omode = static_cast<uint8_t>(omode & 0xFF);
g_gparam.ffmode = static_cast<uint8_t>(ffmode & 0x1);
g_gparam.interlace = static_cast<uint16_t>(interlace & 0x1);
g_gparam.omode = static_cast<uint16_t>(omode & 0xFF);
g_gparam.ffmode = static_cast<uint16_t>(ffmode & 0x1);
writeGsGParamToScratch(runtime);
uint64_t pmode = makePmode(1, 0, 0, 0, 0, 0x80);
uint64_t smode2 = (interlace & 0x1) | ((ffmode & 0x1) << 1);
uint64_t dispfb = makeDispFb(0, 10, 0, 0, 0);
uint64_t display = makeDisplay(0, 0, 0, 0, 639, 447);
uint64_t bgcolor = 0ULL;
if (runtime)
{
uint32_t pktAddr = runtime->guestMalloc(128u, 16u);
if (pktAddr != 0u)
{
uint8_t *pkt = getMemPtr(rdram, pktAddr);
if (pkt)
{
uint64_t *q = reinterpret_cast<uint64_t *>(pkt);
q[0] = makeGiftagAplusD(7u);
q[1] = 0xEULL;
q[2] = pmode;
q[3] = 0x41ULL;
q[4] = smode2;
q[5] = 0x42ULL;
q[6] = dispfb;
q[7] = 0x59ULL;
q[8] = display;
q[9] = 0x5aULL;
q[10] = dispfb;
q[11] = 0x5bULL;
q[12] = display;
q[13] = 0x5cULL;
q[14] = bgcolor;
q[15] = 0x5fULL;
constexpr uint32_t GIF_CHANNEL = 0x1000A000;
constexpr uint32_t CHCR_STR_MODE0 = 0x101u;
auto &mem = runtime->memory();
mem.writeIORegister(GIF_CHANNEL + 0x10u, pktAddr);
mem.writeIORegister(GIF_CHANNEL + 0x20u, 8u);
mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0);
mem.processPendingTransfers();
runtime->guestFree(pktAddr);
}
else
{
runtime->guestFree(pktAddr);
}
}
auto &regs = runtime->memory().gs();
ps2_syscalls::configureGsCrt(regs, interlace & 1u, omode & 0xffu, ffmode & 1u);
regs.pmode = makePmode(1, 0, 0, 0, 0, 0x80);
regs.dispfb1 = regs.dispfb2 = makeDispFb(0, 10, 0, 0, 0);
regs.display1 = regs.display2 = makeDefaultGsDispEnv(0, 640, 448).display;
regs.bgcolor = 0;
}
}
@@ -887,11 +851,7 @@ namespace ps2_stubs
}
const uint32_t fbw = std::max<uint32_t>(1u, (w + 63u) / 64u);
const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u);
const uint64_t smode2 =
(static_cast<uint64_t>(g_gparam.interlace & 0x1u) << 0) |
(static_cast<uint64_t>(g_gparam.ffmode & 0x1u) << 1);
const uint64_t display = makeDisplay(636u, 32u, 0u, 0u, w - 1u, h - 1u);
const GsDispEnvMem displayEnv = makeDefaultGsDispEnv(psm, w, h);
const int32_t drawWidth = static_cast<int32_t>(w);
const int32_t drawHeight = static_cast<int32_t>(h);
@@ -908,11 +868,8 @@ namespace ps2_stubs
const uint64_t dispfb1 = makeDispFb(0u, fbw, psm, 0u, 0u);
GsDBuffDcMem db{};
db.disp[0].pmode = pmode;
db.disp[0].smode2 = smode2;
db.disp[0] = displayEnv;
db.disp[0].dispfb = dispfb0;
db.disp[0].display = display;
db.disp[0].bgcolor = 0u;
db.disp[1] = db.disp[0];
db.disp[1].dispfb = dispfb1;
@@ -958,12 +915,7 @@ namespace ps2_stubs
}
const uint32_t fbw = std::max<uint32_t>(1u, (w + 63u) / 64u);
const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u);
const uint64_t smode2 =
(static_cast<uint64_t>(g_gparam.interlace & 0x1u) << 0) |
(static_cast<uint64_t>(g_gparam.ffmode & 0x1u) << 1);
const uint64_t dispfb = makeDispFb(0u, fbw, psm, 0u, 0u);
const uint64_t display = makeDisplay(636u, 32u, 0u, 0u, w - 1u, h - 1u);
const GsDispEnvMem displayEnv = makeDefaultGsDispEnv(psm, w, h);
const int32_t drawWidth = static_cast<int32_t>(w);
const int32_t drawHeight = static_cast<int32_t>(h);
@@ -976,11 +928,7 @@ namespace ps2_stubs
}
GsDBuffMem db{};
db.disp[0].pmode = pmode;
db.disp[0].smode2 = smode2;
db.disp[0].dispfb = dispfb;
db.disp[0].display = display;
db.disp[0].bgcolor = 0u;
db.disp[0] = displayEnv;
db.disp[1] = db.disp[0];
db.giftag0 = {makeGiftagAplusD(14u), 0x0E0E0E0E0E0E0E0EULL};
@@ -1002,21 +950,16 @@ namespace ps2_stubs
uint32_t psm = getRegU32(ctx, 5);
uint32_t w = getRegU32(ctx, 6);
uint32_t h = getRegU32(ctx, 7);
const GsTrailingArgs2 trailing = decodeGsTrailingArgs2(rdram, ctx);
uint32_t dx = trailing.arg0;
uint32_t dy = trailing.arg1;
const int32_t dx = static_cast<int16_t>(getRegU32(ctx, 8));
const int32_t dy = static_cast<int16_t>(getRegU32(ctx, 9));
if (w == 0)
w = 640;
if (h == 0)
h = 448;
uint32_t fbw = (w + 63) / 64;
uint64_t dispfb = makeDispFb(0, fbw, psm, 0, 0);
uint64_t display = makeDisplay(dx, dy, 0, 0, w - 1, h - 1);
writeGsDispEnv(rdram, envAddr, display, dispfb);
setReturnS32(ctx, 0);
const GsDispEnvMem env = makeDefaultGsDispEnv(psm, w, h, dx, dy);
setReturnS32(ctx, writeGsDispEnv(rdram, envAddr, env) ? 0 : -1);
}
void sceGsSetDefDrawEnv(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
@@ -1183,6 +1126,7 @@ namespace ps2_stubs
applyGsClearPacket(runtime, db.clear1);
}
runtime->memory().gs().sdkPresentCount.fetch_add(1, std::memory_order_relaxed);
setReturnS32(ctx, static_cast<int32_t>(which ^ 1u));
}
@@ -1208,6 +1152,7 @@ namespace ps2_stubs
applyGsRegPairs(runtime, reinterpret_cast<const GsRegPairMem *>(&db.draw1), 8u);
}
runtime->memory().gs().sdkPresentCount.fetch_add(1, std::memory_order_relaxed);
setReturnS32(ctx, static_cast<int32_t>(which ^ 1u));
}
@@ -1486,11 +1486,12 @@ namespace
{
struct GsGParam
{
uint8_t interlace;
uint8_t omode;
uint8_t ffmode;
uint8_t version;
uint16_t interlace;
uint16_t omode;
uint16_t ffmode;
uint16_t version;
};
static_assert(sizeof(GsGParam) == 8, "sceGsGetGParam exposes four 16-bit fields");
struct GsDispEnvMem
{
@@ -1624,6 +1625,35 @@ namespace
(static_cast<uint64_t>(dh & 0x07FF) << 44);
}
static GsDispEnvMem makeDefaultGsDispEnv(uint32_t psm, uint32_t width, uint32_t height,
int32_t dx = 0, int32_t dy = 0)
{
GsDispEnvMem env{};
env.pmode = 0x66; // EN2, CRTMD=1, MMOD=1, AMOD=1 (libgs default).
env.smode2 = g_gparam.interlace ? (1u | ((g_gparam.ffmode & 1u) << 1)) : 2u;
env.dispfb = makeDispFb(0, (width + 63u) / 64u, psm, 0, 0);
if (g_gparam.omode == 2 || g_gparam.omode == 3)
{
// Note libgs DISPLAY dimensions are output clocks/scanlines. In analog
// modes 640 framebuffer pixels span 2560 clocks (MAGH=3).
const uint32_t magnification = (width + 2559u) / width;
const int32_t originX = g_gparam.omode == 2 ? 636 : 656;
const int32_t originY = g_gparam.omode == 2 ? 25 : 36;
const uint32_t displayHeight =
g_gparam.interlace && g_gparam.ffmode ? height * 2u : height;
env.display = makeDisplay(originX + dx * static_cast<int32_t>(magnification),
originY * (g_gparam.interlace ? 2 : 1) + dy,
magnification - 1u, 0, width * magnification - 1u,
displayHeight - 1u);
}
else
{
env.display = makeDisplay(dx, dy, 0, 0, width - 1u, height - 1u);
}
return env;
}
static uint64_t makeFrame(uint32_t fbp, uint32_t fbw, uint32_t psm, uint32_t fbmsk)
{
return (static_cast<uint64_t>(fbp & 0x1FFu) << 0) |
@@ -1805,15 +1835,11 @@ namespace
return true;
}
static bool writeGsDispEnv(uint8_t *rdram, uint32_t addr, uint64_t display, uint64_t dispfb)
static bool writeGsDispEnv(uint8_t *rdram, uint32_t addr, const GsDispEnvMem &env)
{
uint8_t *ptr = getMemPtr(rdram, addr);
if (!ptr)
return false;
GsDispEnvMem env{};
std::memcpy(&env, ptr, sizeof(env));
env.dispfb = dispfb;
env.display = display;
std::memcpy(ptr, &env, sizeof(env));
return true;
}
@@ -1881,9 +1907,9 @@ namespace
auto &regs = runtime->memory().gs();
regs.pmode = env.pmode;
regs.smode2 = env.smode2;
regs.dispfb1 = env.dispfb;
regs.writeDisplayFramebuffer(0, env.dispfb);
regs.display1 = env.display;
regs.dispfb2 = env.dispfb;
regs.writeDisplayFramebuffer(1, env.dispfb);
regs.display2 = env.display;
regs.bgcolor = env.bgcolor;
}
+17 -13
View File
@@ -3,26 +3,30 @@
namespace ps2_syscalls
{
void configureGsCrt(GSRegisters &gs, uint32_t interlaced, uint32_t videoMode, uint32_t frameMode)
{
gs.smode2 = (static_cast<uint64_t>(interlaced) & 0x1ull) | ((static_cast<uint64_t>(frameMode) & 0x1ull) << 1);
const uint64_t cmod = videoMode == 3 ? 3u : videoMode == 2 ? 2u : 0u;
const uint64_t lc = (videoMode == 0x51 || videoMode == 0x52) ? 22u :
(videoMode >= 0x1a && videoMode <= 0x4a) ? 15u : 32u;
gs.smode1 = (gs.smode1 & ~((127ull << 3) | (3ull << 13))) | (lc << 3) | (cmod << 13);
if ((gs.pmode & 0x3ull) == 0ull)
{
gs.pmode |= 0x1ull;
}
}
void GsSetCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
int interlaced = getRegU32(ctx, 4); // $a0 - 0=non-interlaced, 1=interlaced
int videoMode = getRegU32(ctx, 5); // $a1 - 0=NTSC, 1=PAL, 2=VESA, 3=HiVision
int videoMode = getRegU32(ctx, 5); // $a1 - GS CRT mode (2=NTSC, 3=PAL, 0x50=480p)
int frameMode = getRegU32(ctx, 6); // $a2 - 0=field, 1=frame
if (runtime)
{
auto &gs = runtime->memory().gs();
const uint64_t smode2 =
(static_cast<uint64_t>(interlaced) & 0x1ull) |
((static_cast<uint64_t>(frameMode) & 0x1ull) << 1);
gs.smode2 = smode2;
// Keep CRT1 enabled after the BIOS syscall selects a display mode.
if ((gs.pmode & 0x3ull) == 0ull)
{
gs.pmode |= 0x1ull;
}
configureGsCrt(runtime->memory().gs(), interlaced, videoMode, frameMode);
}
RUNTIME_LOG("PS2 GsSetCrt: interlaced=" << interlaced
@@ -4,6 +4,7 @@
namespace ps2_syscalls
{
void configureGsCrt(GSRegisters &gs, uint32_t interlaced, uint32_t videoMode, uint32_t frameMode);
bool dispatchSyscallOverride(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
void GsSetCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
void SetGsCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
@@ -518,6 +518,8 @@ 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");
RUNTIME_WARNING("GS CPU backend is experimental and may not be fully compatible with all games.");
std::lock_guard<std::mutex> lock(m_mutex);
m_vram = vram;
m_vramSize = vramSize;
+62 -5
View File
@@ -168,6 +168,7 @@ void GS::reset()
{
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
m_hostPresentationFrame.clear();
m_hostPresentationGpuFrame.reset();
m_hostPresentationWidth = 0u;
m_hostPresentationHeight = 0u;
m_hostPresentationDisplayFbp = 0u;
@@ -260,6 +261,8 @@ GSDebugSnapshot GS::getDebugSnapshot() const
snapshot.hasHostPresentationFrame = m_hasHostPresentationFrame;
}
snapshot.localToHostPendingBytes = transfer.localToHostPendingBytes;
if (m_backend && m_backend->UsesRawCommands())
m_backend->ReadRegisterState(snapshot);
return snapshot;
}
@@ -509,6 +512,9 @@ GSPresentationRequest GS::buildPresentationRequestUnlocked() const
if (!m_privRegs)
return request;
request.pmode = m_privRegs->pmode;
request.smode1 = m_privRegs->smode1;
request.syncv = m_privRegs->syncv;
request.field = (m_privRegs->csr.load() >> 13u) & 1u;
request.smode2 = m_privRegs->smode2;
request.dispfb1 = m_privRegs->dispfb1;
request.display1 = m_privRegs->display1;
@@ -533,6 +539,7 @@ void GS::latchHostPresentationFrame()
{
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
m_hostPresentationFrame.clear();
m_hostPresentationGpuFrame.reset();
m_hasHostPresentationFrame = false;
m_hostPresentationWidth = m_hostPresentationHeight = 0u;
return;
@@ -560,6 +567,8 @@ void GS::latchHostPresentationFrame()
{
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
m_hostPresentationFrame = std::move(frame.pixels);
m_hostPresentationGpuFrame = std::move(frame.gpu);
m_hostPresentationAspectRatio = frame.aspectRatio;
m_hostPresentationWidth = width;
m_hostPresentationHeight = height;
m_hostPresentationDisplayFbp = displayFbp;
@@ -638,9 +647,16 @@ bool GS::copyLatchedHostPresentationFrame(std::vector<uint8_t> &outPixels,
return true;
}
void GS::processGIFPacket(const uint8_t *data, uint32_t sizeBytes)
void GS::processGIFPacket(const uint8_t *data, uint32_t sizeBytes, uint32_t path)
{
std::lock_guard<std::recursive_mutex> lock(m_stateMutex);
if (m_backend && m_backend->UsesRawCommands())
{
if (data && sizeBytes && (sizeBytes & 15u) == 0u)
m_backend->ProcessGIF(path, data, sizeBytes);
return;
}
if (!data || sizeBytes < 16 || !m_backend)
return;
@@ -745,6 +761,13 @@ bool GS::processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes)
if (!validatePackedGifPacket(data, sizeBytes))
return false;
if (m_backend->UsesRawCommands())
{
m_backend->ProcessGIF(3, data, sizeBytes);
++m_nativePackedGIFPacketCount;
return true;
}
const bool processed = visitPackedGifPacket(data, sizeBytes, [&](const PackedGifPacketTag &tag)
{
m_curQ = 1.0f;
@@ -1067,6 +1090,11 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value)
void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value)
{
if (m_backend && m_backend->UsesRawCommands())
{
m_backend->WriteRegisterRaw(regAddr, value);
return;
}
const bool interestingReg =
regAddr == GS_REG_PRIM ||
regAddr == GS_REG_RGBAQ ||
@@ -1499,7 +1527,7 @@ void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value)
}
case 0x59:
if (m_privRegs)
m_privRegs->dispfb1 = value;
m_privRegs->writeDisplayFramebuffer(0, value);
break;
case 0x5a:
if (m_privRegs)
@@ -1507,7 +1535,7 @@ void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value)
break;
case 0x5b:
if (m_privRegs)
m_privRegs->dispfb2 = value;
m_privRegs->writeDisplayFramebuffer(1, value);
break;
case 0x5c:
if (m_privRegs)
@@ -1617,13 +1645,14 @@ void GS::processImageData(const uint8_t *data, uint32_t sizeBytes)
bool GS::clearFramebufferContext(uint32_t contextIndex, uint32_t rgba)
{
std::lock_guard<std::recursive_mutex> lock(m_stateMutex);
return m_backend && m_backend->ClearFramebuffer(m_ctx[(contextIndex != 0u) ? 1 : 0], rgba);
return m_backend && m_backend->ClearFramebuffer(getDebugSnapshot().ctx[(contextIndex != 0u) ? 1 : 0], rgba);
}
bool GS::clearActiveFramebuffer(uint32_t rgba)
{
std::lock_guard<std::recursive_mutex> lock(m_stateMutex);
return m_backend && m_backend->ClearFramebuffer(activeContext(), rgba);
const auto state = getDebugSnapshot();
return m_backend && m_backend->ClearFramebuffer(state.ctx[state.prim.ctxt ? 1 : 0], rgba);
}
uint32_t GS::consumeLocalToHostBytes(uint8_t *dst, uint32_t maxBytes)
@@ -1733,3 +1762,31 @@ void GS::updatePreferredDisplaySourceForDraw(const GSPrimitiveBatch &batch)
m_hasPreferredDisplaySource = true;
}
}
std::shared_ptr<GSGpuFrame> GS::getLatchedGpuFrame(uint32_t &width, uint32_t &height, float &aspectRatio) const
{
std::lock_guard<std::mutex> lock(m_presentationMutex);
if (!m_hostPresentationGpuFrame)
return {};
width = m_hostPresentationWidth;
height = m_hostPresentationHeight;
aspectRatio = m_hostPresentationAspectRatio;
return m_hostPresentationGpuFrame;
}
void GS::shutdownBackend()
{
std::lock_guard<std::recursive_mutex> stateLock(m_stateMutex);
std::lock_guard<std::mutex> backendLock(m_backendLifetimeMutex);
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
m_hostPresentationGpuFrame.reset();
m_hostPresentationFrame.clear();
m_hasHostPresentationFrame = false;
m_backend.reset();
}
uint64_t GS::getReadbackCount() const
{
std::lock_guard<std::recursive_mutex> lock(m_stateMutex);
return m_backend ? m_backend->GetReadbackCount() : 0;
}
+410
View File
@@ -0,0 +1,410 @@
#include "gs_gl_interop.h"
#include "context.hpp"
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
#include <array>
#include <cstring>
#include <stdexcept>
#include <string>
#ifdef _WIN32
#include <windows.h>
#define GL_CALL __stdcall
#else
#include <unistd.h>
#define GL_CALL
#endif
namespace
{
constexpr unsigned Texture2D = 0x0DE1, ShaderRead = 0x9591, General = 0x958D;
constexpr auto MemoryHandle = Vulkan::ExternalHandle::get_opaque_memory_handle_type();
constexpr auto SemaphoreHandle = Vulkan::ExternalHandle::get_opaque_semaphore_handle_type();
constexpr VkImageUsageFlags ImageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
[[noreturn]] void fail(const std::string &message)
{
throw std::runtime_error("parallel-gs interop: " + message);
}
template <typename T>
T load(const char *name)
{
auto proc = glfwGetProcAddress(name);
if (!proc)
fail(std::string("missing OpenGL entry point ") + name);
return reinterpret_cast<T>(proc);
}
struct GL
{
#define GL_FN(ret, name, args) \
using name##Proc = ret(GL_CALL *) args; \
name##Proc name = load<name##Proc>("gl" #name)
GL_FN(void, GetIntegerv, (unsigned, int *));
GL_FN(void, GetUnsignedBytei_vEXT, (unsigned, unsigned, unsigned char *));
GL_FN(unsigned, GetError, ());
GL_FN(void, CreateMemoryObjectsEXT, (int, unsigned *));
GL_FN(void, DeleteMemoryObjectsEXT, (int, const unsigned *));
GL_FN(void, MemoryObjectParameterivEXT, (unsigned, unsigned, const int *));
GL_FN(void, GenTextures, (int, unsigned *));
GL_FN(void, DeleteTextures, (int, const unsigned *));
GL_FN(void, BindTexture, (unsigned, unsigned));
GL_FN(void, TexParameteri, (unsigned, unsigned, int));
GL_FN(void, TexStorageMem2DEXT, (unsigned, int, unsigned, int, int, unsigned, uint64_t));
GL_FN(void, GenSemaphoresEXT, (int, unsigned *));
GL_FN(void, DeleteSemaphoresEXT, (int, const unsigned *));
GL_FN(void, WaitSemaphoreEXT, (unsigned, unsigned, const unsigned *, unsigned, const unsigned *, const unsigned *));
GL_FN(void, SignalSemaphoreEXT, (unsigned, unsigned, const unsigned *, unsigned, const unsigned *, const unsigned *));
GL_FN(void, Flush, ());
GL_FN(void, Finish, ());
#ifdef _WIN32
GL_FN(void, ImportMemoryWin32HandleEXT, (unsigned, uint64_t, unsigned, void *));
GL_FN(void, ImportSemaphoreWin32HandleEXT, (unsigned, unsigned, void *));
#else
GL_FN(void, ImportMemoryFdEXT, (unsigned, uint64_t, unsigned, int));
GL_FN(void, ImportSemaphoreFdEXT, (unsigned, unsigned, int));
#endif
#undef GL_FN
void check(const char *operation)
{
auto error = GetError();
if (error)
fail(std::string(operation) + " (GL error " + std::to_string(error) + ")");
}
void import(unsigned object, Vulkan::ExternalHandle handle, uint64_t size = 0)
{
if (!handle)
fail("could not export Vulkan handle");
#ifdef _WIN32
if (size)
ImportMemoryWin32HandleEXT(object, size, 0x9587, handle.handle);
else
ImportSemaphoreWin32HandleEXT(object, 0x9587, handle.handle);
CloseHandle(handle.handle); // Win32 imports retain their own reference.
#else
if (size)
ImportMemoryFdEXT(object, size, 0x9586, handle.handle);
else
ImportSemaphoreFdEXT(object, 0x9586, handle.handle);
// A successful fd import transfers ownership to OpenGL.
auto error = GetError();
if (error)
{
close(handle.handle);
fail("fd import failed (GL error " + std::to_string(error) + ")");
}
#endif
check("external object import");
}
};
struct DeviceState
{
GL gl;
Vulkan::Context context;
Vulkan::Device device;
DeviceState()
{
if (!Vulkan::Context::init_loader(nullptr))
fail("Vulkan loader unavailable");
context.set_num_thread_indices(1);
if (!context.init_instance(nullptr, 0))
fail("Vulkan instance initialization failed");
int uuidCount = 0;
gl.GetIntegerv(0x9596, &uuidCount); // GL_NUM_DEVICE_UUIDS_EXT
std::vector<std::array<unsigned char, VK_UUID_SIZE>> uuids(uuidCount);
for (int i = 0; i < uuidCount; ++i)
gl.GetUnsignedBytei_vEXT(0x9597, i, uuids[i].data());
gl.check("device UUID query");
uint32_t count = 0;
vkEnumeratePhysicalDevices(context.get_instance(), &count, nullptr);
std::vector<VkPhysicalDevice> devices(count);
vkEnumeratePhysicalDevices(context.get_instance(), &count, devices.data());
VkPhysicalDevice selected = VK_NULL_HANDLE;
for (auto gpu : devices)
{
VkPhysicalDeviceIDProperties id{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES};
VkPhysicalDeviceProperties2 props{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &id};
vkGetPhysicalDeviceProperties2(gpu, &props);
for (const auto &uuid : uuids)
if (memcmp(uuid.data(), id.deviceUUID, VK_UUID_SIZE) == 0)
selected = gpu;
}
if (!selected)
fail("no Vulkan device matches the OpenGL device UUID");
VkPhysicalDeviceExternalImageFormatInfo external{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO};
external.handleType = MemoryHandle;
VkPhysicalDeviceImageFormatInfo2 format{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, &external};
format.format = VK_FORMAT_R8G8B8A8_UNORM;
format.type = VK_IMAGE_TYPE_2D;
format.tiling = VK_IMAGE_TILING_OPTIMAL;
format.usage = ImageUsage;
VkExternalImageFormatProperties externalProps{VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES};
VkImageFormatProperties2 props{VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, &externalProps};
if (vkGetPhysicalDeviceImageFormatProperties2(selected, &format, &props) != VK_SUCCESS ||
!(externalProps.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT))
fail("RGBA8 external image memory is not exportable");
VkPhysicalDeviceExternalSemaphoreInfo semInfo{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_SEMAPHORE_INFO};
semInfo.handleType = SemaphoreHandle;
VkExternalSemaphoreProperties semProps{VK_STRUCTURE_TYPE_EXTERNAL_SEMAPHORE_PROPERTIES};
vkGetPhysicalDeviceExternalSemaphoreProperties(selected, &semInfo, &semProps);
if (!(semProps.externalSemaphoreFeatures & VK_EXTERNAL_SEMAPHORE_FEATURE_EXPORTABLE_BIT))
fail("binary external semaphores are not exportable");
// Granite enables the platform external-object extensions when available.
if (!context.init_device(selected, VK_NULL_HANDLE, nullptr, 0))
fail("Vulkan device initialization failed");
device.set_context(context);
device.init_frame_contexts(2);
device.next_frame_context();
}
};
struct Slot final : GSGpuFrame
{
std::shared_ptr<DeviceState> owner;
Vulkan::ImageHandle image;
Vulkan::Semaphore ready, released;
unsigned texture = 0, memory = 0, glReady = 0, glReleased = 0;
bool produced = false, pendingReady = false, pendingRelease = false, acquired = false;
explicit Slot(std::shared_ptr<DeviceState> state) : owner(std::move(state))
{
}
Vulkan::ExternalHandle exportSemaphore(const Vulkan::Semaphore &semaphore)
{
// Opaque handles share the permanent payload, so export before the first
// signal is legal. Granite's helper also caters for SYNC_FD and forbids it.
auto &d = owner->device;
Vulkan::ExternalHandle handle;
#ifdef _WIN32
VkSemaphoreGetWin32HandleInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_GET_WIN32_HANDLE_INFO_KHR};
info.semaphore = semaphore->get_semaphore();
info.handleType = SemaphoreHandle;
if (d.get_device_table().vkGetSemaphoreWin32HandleKHR(d.get_device(), &info, &handle.handle) != VK_SUCCESS)
fail("could not export Win32 semaphore");
#else
VkSemaphoreGetFdInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR};
info.semaphore = semaphore->get_semaphore();
info.handleType = SemaphoreHandle;
if (d.get_device_table().vkGetSemaphoreFdKHR(d.get_device(), &info, &handle.handle) != VK_SUCCESS)
fail("could not export semaphore fd");
#endif
return handle;
}
void initialize(unsigned width, unsigned height)
{
auto &d = owner->device;
auto &gl = owner->gl;
auto info = Vulkan::ImageCreateInfo::immutable_2d_image(width, height, VK_FORMAT_R8G8B8A8_UNORM, false);
info.usage = ImageUsage;
info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
info.misc = Vulkan::IMAGE_MISC_EXTERNAL_MEMORY_BIT;
image = d.create_image(info);
if (!image)
fail("external image allocation failed");
ready = d.request_semaphore_external(VK_SEMAPHORE_TYPE_BINARY, SemaphoreHandle);
released = d.request_semaphore_external(VK_SEMAPHORE_TYPE_BINARY, SemaphoreHandle);
if (!ready || !released)
fail("external semaphore allocation failed");
gl.CreateMemoryObjectsEXT(1, &memory);
const int dedicated = 1;
gl.MemoryObjectParameterivEXT(memory, 0x9581, &dedicated);
gl.check("memory object parameters");
gl.import(memory, image->export_handle(), image->get_allocation().get_size());
int previous = 0;
gl.GetIntegerv(0x8069, &previous);
gl.GenTextures(1, &texture);
gl.BindTexture(Texture2D, texture);
gl.TexStorageMem2DEXT(Texture2D, 1, 0x8058, width, height, memory, image->get_allocation().get_offset());
gl.TexParameteri(Texture2D, 0x2801, 0x2601);
gl.TexParameteri(Texture2D, 0x2800, 0x2601);
gl.TexParameteri(Texture2D, 0x2802, 0x812F);
gl.TexParameteri(Texture2D, 0x2803, 0x812F);
gl.BindTexture(Texture2D, previous);
gl.check("shared texture storage");
gl.GenSemaphoresEXT(1, &glReady);
gl.GenSemaphoresEXT(1, &glReleased);
gl.check("GL semaphore creation");
gl.import(glReady, exportSemaphore(ready));
gl.import(glReleased, exportSemaphore(released));
gl.check("shared texture creation");
}
~Slot() override
{
// Slots are retired only on the GL thread after both APIs have finished.
auto &gl = owner->gl;
if (texture)
gl.DeleteTextures(1, &texture);
if (memory)
gl.DeleteMemoryObjectsEXT(1, &memory);
if (glReady)
gl.DeleteSemaphoresEXT(1, &glReady);
if (glReleased)
gl.DeleteSemaphoresEXT(1, &glReleased);
}
uint32_t AcquireTexture() override
{
auto &gl = owner->gl;
if (pendingReady)
{
gl.WaitSemaphoreEXT(glReady, 0, nullptr, 1, &texture, &ShaderRead);
pendingReady = false;
}
else if (pendingRelease)
{
// Repeated host frame: consume the previous GL signal before signalling again.
gl.WaitSemaphoreEXT(glReleased, 0, nullptr, 1, &texture, &General);
pendingRelease = false;
}
acquired = true;
gl.check("frame acquire");
return texture;
}
void ReleaseTexture() override
{
if (!acquired)
fail("release without acquire");
owner->gl.SignalSemaphoreEXT(glReleased, 0, nullptr, 1, &texture, &General);
owner->gl.Flush();
owner->gl.check("frame release");
pendingRelease = true;
acquired = false;
}
void copy(const Vulkan::Image &source)
{
auto &d = owner->device;
if (acquired)
fail("attempted to overwrite an acquired frame");
if (pendingReady)
{
AcquireTexture();
ReleaseTexture();
}
if (pendingRelease)
{
auto wait = d.request_semaphore(VK_SEMAPHORE_TYPE_BINARY, released->get_semaphore(), false);
wait->signal_external();
wait->set_signal_is_foreign_queue();
d.add_wait_semaphore(Vulkan::CommandBuffer::Type::Generic, std::move(wait), VK_PIPELINE_STAGE_2_TRANSFER_BIT, true);
pendingRelease = false;
}
auto cmd = d.request_command_buffer();
if (produced)
cmd->acquire_image_barrier(*image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
else
cmd->image_barrier(*image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_2_NONE, 0, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
cmd->copy_image(*image, source);
cmd->release_image_barrier(*image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
d.submit(cmd);
// Borrow the owned external binary semaphore for this one submission.
auto signal = d.request_semaphore(VK_SEMAPHORE_TYPE_BINARY, ready->get_semaphore(), false);
d.submit_empty(Vulkan::CommandBuffer::Type::Generic, nullptr, signal.get());
signal->wait_external();
produced = pendingReady = true;
}
};
}
class GSGLInterop::Impl
{
public:
std::shared_ptr<DeviceState> state;
std::array<std::shared_ptr<Slot>, 2> slots;
unsigned next = 0;
Impl()
{
if (!glfwGetCurrentContext())
fail("a current OpenGL context is required");
const char *required[] = {"GL_EXT_memory_object", "GL_EXT_semaphore",
#ifdef _WIN32
"GL_EXT_memory_object_win32", "GL_EXT_semaphore_win32"
#else
"GL_EXT_memory_object_fd", "GL_EXT_semaphore_fd"
#endif
};
for (auto name : required)
if (!glfwExtensionSupported(name))
fail(std::string("missing ") + name);
state = std::make_shared<DeviceState>();
// Validate actual GL import before boot, even if the game never scans out.
for (auto &slot : slots)
{
slot = std::make_shared<Slot>(state);
slot->initialize(1, 1);
}
}
~Impl()
{
state->gl.Finish();
state->device.wait_idle();
}
};
GSGLInterop::GSGLInterop() : m_impl(std::make_unique<Impl>())
{
}
GSGLInterop::~GSGLInterop() = default;
Vulkan::Device &GSGLInterop::device()
{
return m_impl->state->device;
}
PresentationFrame GSGLInterop::present(const ParallelGS::ScanoutResult &scanout)
{
if (!scanout.image)
return {};
if (scanout.image->get_format() != VK_FORMAT_R8G8B8A8_UNORM)
fail("unexpected scanout format");
const auto width = scanout.image->get_width(), height = scanout.image->get_height();
auto &slot = m_impl->slots[m_impl->next++ % 2];
if (slot->image->get_width() != width || slot->image->get_height() != height)
{
m_impl->state->gl.Finish();
device().wait_idle();
slot = std::make_shared<Slot>(m_impl->state);
slot->initialize(width, height);
}
slot->copy(*scanout.image);
PresentationFrame frame;
frame.gpu = slot;
frame.width = width;
frame.height = height;
frame.aspectRatio = 4.0f / 3.0f;
if (scanout.mode_width && scanout.mode_height && scanout.internal_height)
frame.aspectRatio *= (float(scanout.internal_width) / scanout.mode_width) /
(float(scanout.internal_height) / scanout.mode_height);
return frame;
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "runtime/gs/gs_types.h"
#include "gs_renderer.hpp"
class GSGLInterop
{
public:
GSGLInterop();
~GSGLInterop();
Vulkan::Device &device();
PresentationFrame present(const ParallelGS::ScanoutResult &scanout);
private:
class Impl;
std::unique_ptr<Impl> m_impl;
};
@@ -0,0 +1,501 @@
#include "runtime/gs/gs_parallel_backend.h"
#include "runtime/gs/gs_frontend.h"
#include "runtime/gs/ps2_gs_memory.h"
#include "runtime/ps2_memory.h"
#include "ps2_log.h"
#include "gs_gl_interop.h"
#include "gs_interface.hpp"
#include "thread_id.hpp"
#include <algorithm>
#include <cstring>
#include <mutex>
#include <stdexcept>
namespace
{
constexpr size_t VramSize = 4 * 1024 * 1024;
uint64_t word(const void *data)
{
uint64_t value;
std::memcpy(&value, data, 8);
return value;
}
unsigned bitsPerPixel(unsigned psm)
{
switch (psm & 63)
{
case GS_PSM_CT24:
case GS_PSM_Z24:
return 24;
case GS_PSM_CT16:
case GS_PSM_CT16S:
case GS_PSM_Z16:
case GS_PSM_Z16S:
return 16;
case GS_PSM_T8:
case GS_PSM_T8H:
return 8;
case GS_PSM_T4:
case GS_PSM_T4HL:
case GS_PSM_T4HH:
return 4;
default:
return 32;
}
}
using Reader = uint32_t (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t);
using Writer = void (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t);
struct PixelAccess
{
Reader read;
Writer write;
};
PixelAccess pixelAccess(unsigned psm)
{
using namespace GSMem;
switch (psm & 63)
{
#define PSM(id, name) \
case GS_PSM_##id: \
return {Read##name, Write##name}
PSM(CT32, CT32);
PSM(CT24, CT24);
PSM(CT16, CT16);
PSM(CT16S, CT16S);
PSM(T8, P8);
PSM(T8H, P8H);
PSM(T4, P4);
PSM(T4HL, P4HL);
PSM(T4HH, P4HH);
PSM(Z32, Z32);
PSM(Z24, Z24);
PSM(Z16, Z16);
PSM(Z16S, Z16S);
#undef PSM
default:
return {ReadNull, WriteNull};
}
}
class ParallelBackend final : public GSRasterBackend, private ParallelGS::SignalInterface
{
// All GSInterface/Device entry points are serialized, including presentation.
mutable std::recursive_mutex mutex;
GSGLInterop interop;
mutable ParallelGS::GSInterface gs;
GSRegisters &priv;
struct Lock
{
std::lock_guard<std::recursive_mutex> guard;
explicit Lock(std::recursive_mutex &m) : guard(m) { Util::register_thread_index(0); }
};
uint64_t bitblt = 0, trxreg = 0;
GSTransferSnapshot transfer;
size_t transferBytes = 0, uploadedBytes = 0;
std::array<uint8_t, 8> uploadTail{};
size_t uploadTailSize = 0;
std::array<uint8_t, 16> fifoTail{};
size_t fifoOffset = 16;
mutable uint64_t readbacks = 0;
void observeRegister(uint8_t addr, uint64_t value)
{
if (addr == GS_REG_BITBLTBUF)
bitblt = value;
if (addr == GS_REG_TRXREG)
trxreg = value;
if (addr == GS_REG_TRXDIR)
{
transfer = {};
transfer.direction = value & 3;
transfer.totalPixels = uint32_t(trxreg & 0xfff) * uint32_t((trxreg >> 32) & 0xfff);
unsigned psm = unsigned(bitblt >> (transfer.direction == 1 ? 24 : 56)) & 63;
transferBytes = (size_t(transfer.totalPixels) * bitsPerPixel(psm) + 7) / 8;
transfer.localToHostPendingBytes = transfer.direction == 1 ? transferBytes : 0;
if (transfer.direction == 1 && transferBytes)
++readbacks;
transfer.copiedPixels = transfer.direction == 2 ? transfer.totalPixels : 0;
uploadedBytes = uploadTailSize = 0;
fifoOffset = fifoTail.size();
}
if (addr == GS_REG_HWREG)
observeImage(8);
}
void observeImage(size_t bytes)
{
if (transfer.direction != 0)
return;
uploadedBytes = std::min(transferBytes, uploadedBytes + bytes);
transfer.copiedPixels = std::min<size_t>(transfer.totalPixels, uploadedBytes * 8 / bitsPerPixel(unsigned(bitblt >> 56)));
auto width = uint32_t(trxreg & 0xfff);
if (width)
{
transfer.x = transfer.copiedPixels % width;
transfer.y = transfer.copiedPixels / width;
}
}
// Observe transfer boundaries only; the upstream decoder alone executes commands.
// Start from upstream's saved PATH state, including packets split across DMA calls.
void observeGIF(uint32_t path, const uint8_t *data, size_t size)
{
auto state = gs.get_gif_path(path);
for (size_t offset = 0; offset < size;)
{
if (state.loop == state.tag.NLOOP)
{
std::memcpy(&state.tag, data + offset, 16);
state.reg = state.loop = 0;
offset += 16;
continue;
}
unsigned nreg = state.tag.NREG ? state.tag.NREG : 16;
if (state.tag.FLG >= 2)
{
size_t count = std::min<size_t>(state.tag.NLOOP - state.loop, (size - offset) / 16);
observeImage(count * 16);
state.loop += uint32_t(count);
offset += count * 16;
}
else
{
// Only packed A+D can address the transfer registers (>= 0x50).
const uint64_t descriptors = word(reinterpret_cast<const uint8_t *>(&state.tag) + 8);
if (state.tag.FLG == 0 && ((descriptors >> (state.reg * 4)) & 15) == 14)
observeRegister(uint8_t(word(data + offset + 8) & 127), word(data + offset));
unsigned count = state.tag.FLG == 0 ? 1 : 2;
while (count-- && state.loop < state.tag.NLOOP)
if (++state.reg == nreg)
{
state.reg = 0;
++state.loop;
}
offset += 16;
}
}
}
bool on_signal(uint64_t value) override
{
auto mask = uint32_t(value >> 32);
auto id = (uint32_t(priv.siglblid) & ~mask) | (uint32_t(value) & mask);
priv.siglblid = (priv.siglblid & 0xffffffff00000000ull) | id;
priv.csr.fetch_or(1);
return false;
}
bool on_finish(uint64_t) override
{
gs.flush();
interop.device().wait_idle();
priv.csr.fetch_or(2);
return false;
}
bool on_label(uint64_t value) override
{
auto mask = uint32_t(value >> 32);
auto id = (uint32_t(priv.siglblid >> 32) & ~mask) | (uint32_t(value) & mask);
priv.siglblid = (uint64_t(id) << 32) | uint32_t(priv.siglblid);
return false;
}
public:
explicit ParallelBackend(GSRegisters &registers) : priv(registers)
{
if (!gs.init(&interop.device(), ParallelGS::GSOptions{}))
throw std::runtime_error("parallel-gs: GS initialization failed; check Vulkan feature diagnostics above");
gs.set_signal_interface(this);
}
~ParallelBackend() override
{
Lock lock(mutex);
gs.flush();
interop.device().wait_idle();
}
bool UsesRawCommands() const override
{
return true;
}
uint64_t GetReadbackCount() const override
{
Lock lock(mutex);
return readbacks;
}
void Initialize(uint8_t *vram, uint32_t size) override
{
Lock lock(mutex);
if (!vram || size < VramSize)
throw std::invalid_argument("parallel-gs requires 4 MiB initial VRAM");
std::memcpy(gs.map_vram_write(0, VramSize), vram, VramSize);
gs.end_vram_write(0, VramSize);
}
void Reset() override
{
Lock lock(mutex);
gs.flush();
interop.device().wait_idle();
gs.reset_context_state();
bitblt = trxreg = transferBytes = uploadedBytes = uploadTailSize = 0;
fifoOffset = 16;
transfer = {};
}
void ProcessGIF(uint32_t path, const uint8_t *data, uint32_t size) override
{
Lock lock(mutex);
if (path < 1 || path > 3 || size % 16)
throw std::invalid_argument("parallel-gs: invalid GIF path or size");
if (!data || !size)
return;
observeGIF(path, data, size);
gs.gif_transfer(path, data, size);
}
void WriteRegisterRaw(uint8_t addr, uint64_t value) override
{
Lock lock(mutex);
if (addr >= 128)
return; // Reserved addresses never index upstream's 128-entry table.
observeRegister(addr, value);
// Compatibility aliases used by native stubs, not actual GIF registers.
switch (addr)
{
case 0x59:
priv.writeDisplayFramebuffer(0, value);
return;
case 0x5a:
priv.display1 = value;
return;
case 0x5b:
priv.writeDisplayFramebuffer(1, value);
return;
case 0x5c:
priv.display2 = value;
return;
case 0x5f:
priv.bgcolor = value;
return;
}
gs.write_register(static_cast<ParallelGS::RegisterAddr>(addr), value);
}
void Submit(const GSPrimitiveBatch &) override
{
throw std::logic_error("parallel-gs received a CPU primitive batch");
}
void LoadClut(const GSTex0Reg &, const GSTexClutReg &) override
{
}
void BeginTransfer(const GSTransferCommand &) override
{
throw std::logic_error("parallel-gs requires raw transfer registers");
}
void UploadImage(const uint8_t *data, uint32_t size) override
{
Lock lock(mutex);
if (!data || transfer.direction != 0)
return;
size = uint32_t(std::min<size_t>(size, transferBytes - uploadedBytes));
observeImage(size);
while (size)
{
auto n = std::min<size_t>(size, 8 - uploadTailSize);
std::memcpy(uploadTail.data() + uploadTailSize, data, n);
uploadTailSize += n;
data += n;
size -= uint32_t(n);
if (uploadTailSize == 8 || uploadedBytes == transferBytes && size == 0)
{
std::fill(uploadTail.begin() + uploadTailSize, uploadTail.end(), 0);
gs.write_register(ParallelGS::RegisterAddr::HWREG, word(uploadTail.data()));
uploadTailSize = 0;
}
}
}
void Flush() override
{
Lock lock(mutex);
gs.flush();
}
void TextureFlush() override
{
WriteRegisterRaw(GS_REG_TEXFLUSH, 0);
}
void Sync(GSSyncReason reason) override
{
Lock lock(mutex);
if (reason == GSSyncReason::Finish || reason == GSSyncReason::Reset)
{
gs.flush();
interop.device().wait_idle();
}
// map_vram_read and the upstream FIFO synchronize their own readbacks.
}
PresentationFrame Present(const GSPresentationRequest &request) override
{
Lock lock(mutex);
gs.flush();
auto &p = gs.get_priv_register_state();
p.qwords_lo[0] = request.pmode;
p.qwords_lo[2] = request.smode1;
p.qwords_lo[4] = request.smode2;
p.qwords_lo[12] = request.syncv;
p.qwords_lo[14] = request.dispfb1;
p.qwords_lo[16] = request.display1;
p.qwords_lo[18] = request.dispfb2;
p.qwords_lo[20] = request.display2;
p.qwords_lo[28] = request.bgcolor;
ParallelGS::VSyncInfo info{};
info.phase = request.field;
info.dst_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
info.dst_stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
info.dst_access = VK_ACCESS_2_TRANSFER_READ_BIT;
info.crtc_offsets = true;
return interop.present(gs.vsync(info));
}
uint32_t ReadVram(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y) const override
{
Lock lock(mutex);
auto *mapped = const_cast<uint8_t *>(static_cast<const uint8_t *>(gs.map_vram_read(0, VramSize)));
++readbacks;
return pixelAccess(psm).read(mapped, base, bw, x, y);
}
void WriteVram(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y, uint32_t value) override
{
Lock lock(mutex);
gs.map_vram_read(0, VramSize); // Preserve untouched pixels and packed-format lanes.
auto *mapped = static_cast<uint8_t *>(gs.map_vram_write(0, VramSize));
++readbacks;
pixelAccess(psm).write(mapped, base, bw, x, y, value);
gs.end_vram_write(0, VramSize);
}
void SnapshotVram(std::vector<uint8_t> &out) const override
{
Lock lock(mutex);
auto *mapped = static_cast<const uint8_t *>(gs.map_vram_read(0, VramSize));
++readbacks;
out.assign(mapped, mapped + VramSize);
}
bool ClearFramebuffer(const GSContext &ctx, uint32_t rgba) override
{
Lock lock(mutex);
if (!ctx.frame.fbw || (ctx.frame.psm != GS_PSM_CT32 && ctx.frame.psm != GS_PSM_CT24 &&
ctx.frame.psm != GS_PSM_CT16 && ctx.frame.psm != GS_PSM_CT16S))
return false;
gs.map_vram_read(0, VramSize); // FBMSK and untouched VRAM require current contents.
auto *mapped = static_cast<uint8_t *>(gs.map_vram_write(0, VramSize));
++readbacks;
auto access = pixelAccess(ctx.frame.psm);
if (ctx.fba & 1)
rgba |= 0x80000000u;
if (bitsPerPixel(ctx.frame.psm) == 16)
rgba = ((rgba >> 3) & 31) | ((rgba >> 6) & 0x3e0) | ((rgba >> 9) & 0x7c00) | ((rgba >> 16) & 0x8000);
for (uint32_t y = ctx.scissor.y0; y <= ctx.scissor.y1; ++y)
for (uint32_t x = ctx.scissor.x0; x <= ctx.scissor.x1; ++x)
{
auto old = access.read(mapped, ctx.frame.fbp * 32, ctx.frame.fbw, x, y);
access.write(mapped, ctx.frame.fbp * 32, ctx.frame.fbw, x, y,
(rgba & ~ctx.frame.fbmsk) | (old & ctx.frame.fbmsk));
}
gs.end_vram_write(0, VramSize);
return true;
}
uint32_t ConsumeLocalToHostBytes(uint8_t *dst, uint32_t maxBytes) override
{
Lock lock(mutex);
if (!dst)
return 0;
auto count = uint32_t(std::min<size_t>(maxBytes, transfer.localToHostPendingBytes));
for (uint32_t i = 0; i < count; ++i)
{
if (fifoOffset == 16)
{
gs.read_transfer_fifo(fifoTail.data(), 1);
fifoOffset = 0;
}
dst[i] = fifoTail[fifoOffset++];
}
transfer.localToHostPendingBytes -= count;
return count;
}
GSTransferSnapshot GetTransferSnapshot() const override
{
Lock lock(mutex);
return transfer;
}
void ReadRegisterState(GSDebugSnapshot &s) const override
{
Lock lock(mutex);
const auto &r = gs.get_register_state();
for (unsigned i = 0; i < 2; ++i)
{
auto &c = s.ctx[i];
const auto &v = r.ctx[i];
auto f = v.frame.bits, t = v.tex0.bits, sc = v.scissor.bits, z = v.zbuf.bits;
c.frame = {uint32_t(f & 511), uint32_t((f >> 16) & 63), uint8_t((f >> 24) & 63), uint32_t(f >> 32)};
c.zbuf = {uint32_t(z & 511), uint8_t(((z >> 24) & 15) | 0x30), bool((z >> 32) & 1)};
c.scissor = {uint16_t(sc & 2047), uint16_t((sc >> 16) & 2047), uint16_t((sc >> 32) & 2047), uint16_t((sc >> 48) & 2047)};
c.xyoffset = {uint16_t(v.xyoffset.bits), uint16_t(v.xyoffset.bits >> 32)};
c.tex0 = {uint32_t(t & 16383), uint8_t((t >> 14) & 63), uint8_t((t >> 20) & 63), uint8_t((t >> 26) & 15),
uint8_t((t >> 30) & 15), uint8_t((t >> 34) & 1), uint8_t((t >> 35) & 3), uint32_t((t >> 37) & 16383),
uint8_t((t >> 51) & 15), uint8_t((t >> 55) & 1), uint8_t((t >> 56) & 31), uint8_t(t >> 61)};
c.tex1 = v.tex1.bits;
c.miptbp1 = v.miptbl_1_3.bits;
c.miptbp2 = v.miptbl_4_6.bits;
c.clamp = v.clamp.bits;
c.alpha = v.alpha.bits;
c.test = v.test.bits;
c.fba = v.fba.bits;
}
auto p = r.prim.bits;
s.prim = {GSPrimType(p & 7), bool(p & 8), bool(p & 16), bool(p & 32), bool(p & 64), bool(p & 128), bool(p & 256), bool(p & 512), bool(p & 1024)};
auto b = r.bitbltbuf.bits, t = r.trxpos.bits, a = r.texa.bits, c = r.texclut.bits;
s.texa = {uint8_t(a), bool(a & 0x8000), uint8_t(a >> 32)};
s.texclut = {uint8_t(c & 63), uint8_t((c >> 6) & 63), uint16_t((c >> 12) & 1023)};
s.scanmsk = r.scanmsk.bits;
s.dimx = r.dimx.bits;
s.dthe = r.dthe.bits;
s.colclamp = r.colclamp.bits;
s.bitbltbuf = {uint32_t(b & 16383), uint8_t((b >> 16) & 63), uint8_t((b >> 24) & 63),
uint32_t((b >> 32) & 16383), uint8_t((b >> 48) & 63), uint8_t((b >> 56) & 63)};
s.trxpos = {uint16_t(t & 2047), uint16_t((t >> 16) & 2047), uint16_t((t >> 32) & 2047), uint16_t((t >> 48) & 2047), uint8_t((t >> 59) & 3)};
s.trxreg = {uint16_t(r.trxreg.bits & 4095), uint16_t((r.trxreg.bits >> 32) & 4095)};
s.trxdir = uint32_t(r.trxdir.bits & 3);
}
};
}
std::unique_ptr<GSRasterBackend> CreateParallelGSBackend(GSRegisters &registers)
{
RUNTIME_WARNING("GS Parallel backend is experimental and may not be fully compatible with all games.");
return std::make_unique<ParallelBackend>(registers);
}
+1 -1
View File
@@ -56,7 +56,7 @@ void GifArbiter::drain()
auto &pkt = m_queue[i];
if (!pkt.data.empty())
{
m_processFn(pkt.data.data(), static_cast<uint32_t>(pkt.data.size()));
m_processFn(pkt.pathId, pkt.data.data(), static_cast<uint32_t>(pkt.data.size()));
}
}
m_queue.clear();
+42
View File
@@ -2182,6 +2182,7 @@ void PS2DebugPanel::initialize()
if (!m_initialized)
{
rlImGuiSetup(true);
m_fpsSampleStart = {};
m_initialized = true;
}
#endif
@@ -2205,6 +2206,40 @@ void PS2DebugPanel::draw(PS2Runtime &runtime)
{
return;
}
const auto now = std::chrono::steady_clock::now();
const auto &regs = runtime.memory().gs();
const uint64_t sdkPresents = regs.sdkPresentCount.load(std::memory_order_relaxed);
const std::array<uint64_t, 2> flips{
regs.displayFlipCount[0].load(std::memory_order_relaxed),
regs.displayFlipCount[1].load(std::memory_order_relaxed)};
if (m_fpsSampleStart == std::chrono::steady_clock::time_point{} ||
flips[0] < m_lastDisplayFlips[0] || flips[1] < m_lastDisplayFlips[1] || sdkPresents < m_lastSdkPresents)
{
m_fpsSampleStart = now;
m_lastDisplayFlips = flips;
m_lastSdkPresents = sdkPresents;
m_hostFramesInSample = 0;
m_gameFps = m_hostFps = 0.0;
}
else
{
++m_hostFramesInSample;
const double seconds = std::chrono::duration<double>(now - m_fpsSampleStart).count();
if (seconds >= 1.0)
{
// Once SDK swaps are observed, keep using their counter even when
// it stops: a stalled game must report zero, not host activity.
const uint64_t gameFrames = sdkPresents ? sdkPresents - m_lastSdkPresents :
std::max(flips[0] - m_lastDisplayFlips[0], flips[1] - m_lastDisplayFlips[1]);
m_gameFps = double(gameFrames) / seconds;
m_hostFps = double(m_hostFramesInSample) / seconds;
m_lastDisplayFlips = flips;
m_lastSdkPresents = sdkPresents;
m_hostFramesInSample = 0;
m_fpsSampleStart = now;
}
}
if (IsKeyPressed(KEY_F1))
{
@@ -2227,6 +2262,13 @@ void PS2DebugPanel::draw(PS2Runtime &runtime)
ImGui::EndMenuBar();
}
ImGui::Text("Game FPS (%s): %.1f | Host FPS: %.1f", sdkPresents ? "SDK swaps" : "estimated flips", m_gameFps, m_hostFps);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Game: completed sceGsSwapDBuff/sceGsSwapDBuffDc calls per second, when used.\n"
"Without SDK swaps: estimate from framebuffer base changes on the busiest GS circuit.\n"
"These sources are never added together. This measures presentation, not simulation FPS.\n"
"Direct fixed-buffer rendering may show 0; raster effects may overcount the estimate.\n"
"Host: window redraws per second. Both rates use a one-second sample.");
if (ImGui::BeginTabBar("debug-tabs"))
{
if (ImGui::BeginTabItem("CPU"))
+13 -3
View File
@@ -97,6 +97,16 @@ namespace
}
}
inline void writeGsRegister(GSRegisters &gs, uint64_t *reg, uint64_t value)
{
if (reg == &gs.dispfb1)
gs.writeDisplayFramebuffer(0, value);
else if (reg == &gs.dispfb2)
gs.writeDisplayFramebuffer(1, value);
else
*reg = value;
}
constexpr uint32_t kGsCsrRegOffset = 0x1000u;
// Atomically apply a 32-bit write to one half (off=0 low dword, off=4 high
@@ -965,7 +975,7 @@ void PS2Memory::write32(uint32_t address, uint32_t value)
{
uint64_t mask = 0xFFFFFFFFULL << (off * 8);
uint64_t newVal = (*reg & ~mask) | ((uint64_t)value << (off * 8));
*reg = newVal;
writeGsRegister(gs_regs, reg, newVal);
}
return;
}
@@ -1022,7 +1032,7 @@ void PS2Memory::write64(uint32_t address, uint64_t value)
}
else if (uint64_t *reg = gsRegPtr(gs_regs, address))
{
*reg = value;
writeGsRegister(gs_regs, reg, value);
}
return;
}
@@ -1173,7 +1183,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value)
else if (uint64_t *reg = gsRegPtr(gs_regs, address))
{
const uint64_t mask = 0xFFFFFFFFull << (off * 8u);
*reg = (*reg & ~mask) | (static_cast<uint64_t>(value) << (off * 8u));
writeGsRegister(gs_regs, reg, (*reg & ~mask) | (static_cast<uint64_t>(value) << (off * 8u)));
}
m_gsWriteCount.fetch_add(1, std::memory_order_relaxed);
return true;
+109 -64
View File
@@ -5,12 +5,18 @@
#include "game_overrides.h"
#include "ps2_runtime_macros.h"
#include "runtime/gs/gs_frontend.h"
#ifdef PS2X_GS_PARALLEL
#include "runtime/gs/gs_parallel_backend.h"
#endif
#include "runtime/ee_scheduler.h"
#include "ThreadNaming.h"
#include "Kernel/Stubs/Audio.h"
#include "Kernel/Stubs/GS.h"
#include "Kernel/Stubs/MPEG.h"
#include "ps2_host_backend.h"
#ifdef PS2X_GS_PARALLEL
#include "rlgl.h"
#endif
#include "ps2_iop_host.h"
#include "ps2x/iop/iop_subsystem.h"
@@ -22,6 +28,7 @@
#include <cstring>
#include <limits>
#include <chrono>
#include <cstdlib>
#include <atomic>
#include <thread>
#include <unordered_map>
@@ -394,7 +401,12 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint
s_lastPresentationTick = currentTick;
s_hasLatchedInitialFrame = true;
}
else if (s_hasUploadedFrame)
#ifdef PS2X_GS_PARALLEL
float aspect = 0.0f;
rt->gs().getLatchedGpuFrame(outWidth, outHeight, aspect);
return;
#endif
if (!needsLatch && s_hasUploadedFrame)
{
outWidth = (s_lastWidth != 0u) ? s_lastWidth : FB_WIDTH;
outHeight = (s_lastHeight != 0u) ? s_lastHeight : DEFAULT_DISPLAY_HEIGHT;
@@ -549,6 +561,7 @@ PS2Runtime::~PS2Runtime()
if (IsWindowReady())
{
m_gs.shutdownBackend();
CloseWindow();
}
@@ -665,8 +678,8 @@ bool PS2Runtime::syncCoreSubsystems()
}
m_gs.init(gsVram, static_cast<uint32_t>(PS2_GS_VRAM_SIZE), &m_memory.gs());
m_gifArbiter.setProcessPacketFn([this](const uint8_t *data, uint32_t size)
{ m_gs.processGIFPacket(data, size); });
m_gifArbiter.setProcessPacketFn([this](GifPathId path, const uint8_t *data, uint32_t size)
{ m_gs.processGIFPacket(data, size, static_cast<uint32_t>(path)); });
m_memory.setGifArbiter(&m_gifArbiter);
m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t top, uint32_t itop)
{
@@ -735,6 +748,9 @@ bool PS2Runtime::initialize(const char *title)
InitWindow(HOST_WINDOW_WIDTH, HOST_WINDOW_HEIGHT, title);
InitAudioDevice();
m_audioBackend.setAudioReady(IsAudioDeviceReady());
#endif
#ifdef PS2X_GS_PARALLEL
m_gs.setRasterBackend(CreateParallelGSBackend(m_memory.gs()));
#endif
SetTargetFPS(60);
if (m_debugUiInitCallback)
@@ -2386,73 +2402,98 @@ void PS2Runtime::run()
gameThreadFinished.store(true, std::memory_order_release); });
uint64_t tick = 0;
while (!isStopRequested() && !gameThreadFinished.load(std::memory_order_acquire))
std::exception_ptr presentationError;
try
{
PS2_IF_AGRESSIVE_LOGS({
tick++;
if ((tick % 120) == 0)
while (!isStopRequested() && !gameThreadFinished.load(std::memory_order_acquire))
{
PS2_IF_AGRESSIVE_LOGS({
tick++;
if ((tick % 120) == 0)
{
uint64_t curDma = m_memory.dmaStartCount();
uint64_t curGif = m_memory.gifCopyCount();
uint64_t curGs = m_memory.gsWriteCount();
uint64_t curVif = m_memory.vifWriteCount();
const GSRegisters &gs = m_memory.gs();
const uint32_t dbgPc = m_debugPc.load(std::memory_order_relaxed);
const uint32_t dbgRa = m_debugRa.load(std::memory_order_relaxed);
const uint32_t dbgSp = m_debugSp.load(std::memory_order_relaxed);
const uint32_t dbgGp = m_debugGp.load(std::memory_order_relaxed);
const auto eeSnapshot = m_eeScheduler->snapshot();
RUNTIME_LOG("[run:tick] tick=" << tick
<< " pc=0x" << std::hex << dbgPc
<< " ra=0x" << dbgRa
<< " sp=0x" << dbgSp
<< " gp=0x" << dbgGp
<< " dispfb1=0x" << gs.dispfb1
<< " display1=0x" << gs.display1
<< std::dec
<< " activeThreads=" << eeSnapshot.threads.size()
<< " dma=" << curDma
<< " gif=" << curGif
<< " gsw=" << curGs
<< " vif=" << curVif
<< std::endl);
}
});
uint32_t presentWidth = FB_WIDTH;
uint32_t presentHeight = DEFAULT_DISPLAY_HEIGHT;
UploadFrame(frameTex, this, presentWidth, presentHeight);
Texture2D presentationTexture = frameTex;
float aspectRatio = 0.0f;
#ifdef PS2X_GS_PARALLEL
auto gpuFrame = m_gs.getLatchedGpuFrame(presentWidth, presentHeight, aspectRatio);
presentationTexture = {};
if (gpuFrame)
presentationTexture = Texture2D{gpuFrame->AcquireTexture(), int(presentWidth), int(presentHeight), 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8};
#endif
BeginDrawing();
ClearBackground(BLACK);
const float srcWidth = static_cast<float>(std::max<uint32_t>(1u, presentWidth));
const float srcHeight = static_cast<float>(std::max<uint32_t>(1u, presentHeight));
const float screenWidth = static_cast<float>(GetScreenWidth());
const float screenHeight = static_cast<float>(GetScreenHeight());
const float displayWidth = aspectRatio > 0 ? srcHeight * aspectRatio : srcWidth;
const float scale = std::min(screenWidth / displayWidth, screenHeight / srcHeight);
const float dstWidth = displayWidth * scale;
const float dstHeight = srcHeight * scale;
const Rectangle srcRect{0.0f, 0.0f, srcWidth, srcHeight};
const Rectangle dstRect{
(screenWidth - dstWidth) * 0.5f,
(screenHeight - dstHeight) * 0.5f,
dstWidth,
dstHeight};
if (presentationTexture.id)
DrawTexturePro(presentationTexture, srcRect, dstRect, Vector2{0.0f, 0.0f}, 0.0f, WHITE);
#ifdef PS2X_GS_PARALLEL
if (gpuFrame)
{
uint64_t curDma = m_memory.dmaStartCount();
uint64_t curGif = m_memory.gifCopyCount();
uint64_t curGs = m_memory.gsWriteCount();
uint64_t curVif = m_memory.vifWriteCount();
const GSRegisters &gs = m_memory.gs();
const uint32_t dbgPc = m_debugPc.load(std::memory_order_relaxed);
const uint32_t dbgRa = m_debugRa.load(std::memory_order_relaxed);
const uint32_t dbgSp = m_debugSp.load(std::memory_order_relaxed);
const uint32_t dbgGp = m_debugGp.load(std::memory_order_relaxed);
const auto eeSnapshot = m_eeScheduler->snapshot();
RUNTIME_LOG("[run:tick] tick=" << tick
<< " pc=0x" << std::hex << dbgPc
<< " ra=0x" << dbgRa
<< " sp=0x" << dbgSp
<< " gp=0x" << dbgGp
<< " dispfb1=0x" << gs.dispfb1
<< " display1=0x" << gs.display1
<< std::dec
<< " activeThreads=" << eeSnapshot.threads.size()
<< " dma=" << curDma
<< " gif=" << curGif
<< " gsw=" << curGs
<< " vif=" << curVif
<< std::endl);
rlDrawRenderBatchActive();
gpuFrame->ReleaseTexture();
}
});
uint32_t presentWidth = FB_WIDTH;
uint32_t presentHeight = DEFAULT_DISPLAY_HEIGHT;
UploadFrame(frameTex, this, presentWidth, presentHeight);
#endif
if (m_debugUiInitialized && m_debugUiDrawCallback)
{
m_debugUiDrawCallback(*this, m_debugUiUserData);
}
EndDrawing();
BeginDrawing();
ClearBackground(BLACK);
const float srcWidth = static_cast<float>(std::max<uint32_t>(1u, presentWidth));
const float srcHeight = static_cast<float>(std::max<uint32_t>(1u, presentHeight));
const float screenWidth = static_cast<float>(GetScreenWidth());
const float screenHeight = static_cast<float>(GetScreenHeight());
const float scale = std::min(screenWidth / srcWidth, screenHeight / srcHeight);
const float dstWidth = srcWidth * scale;
const float dstHeight = srcHeight * scale;
const Rectangle srcRect{0.0f, 0.0f, srcWidth, srcHeight};
const Rectangle dstRect{
(screenWidth - dstWidth) * 0.5f,
(screenHeight - dstHeight) * 0.5f,
dstWidth,
dstHeight};
DrawTexturePro(frameTex, srcRect, dstRect, Vector2{0.0f, 0.0f}, 0.0f, WHITE);
if (m_debugUiInitialized && m_debugUiDrawCallback)
{
m_debugUiDrawCallback(*this, m_debugUiUserData);
}
EndDrawing();
if (WindowShouldClose())
{
RUNTIME_LOG("[run] window close requested, breaking out of loop");
requestStop();
break;
if (WindowShouldClose())
{
RUNTIME_LOG("[run] window close requested, breaking out of loop");
requestStop();
break;
}
}
}
catch (...)
{
presentationError = std::current_exception();
}
requestStop();
if (gameThread.joinable())
@@ -2466,7 +2507,11 @@ void PS2Runtime::run()
m_debugUiInitialized = false;
}
UnloadTexture(frameTex);
m_gs.shutdownBackend();
CloseWindow();
if (presentationError)
std::rethrow_exception(presentationError);
RUNTIME_LOG("[run] exiting loop");
}
File diff suppressed because it is too large Load Diff
+50 -48
View File
@@ -21,30 +21,32 @@ namespace
// ============================================================================
// Upper instructions (FMAC pipeline)
// ============================================================================
void VU1Interpreter::execUpper(uint32_t instr)
void VU1Interpreter::execUpper(uint32_t instr, float *vfResult, float *accResult)
{
m_currentUpperInstruction = instr;
uint8_t dest = DEST(instr);
uint8_t ft = FT(instr);
uint8_t fs = FS(instr);
uint8_t fd = FD(instr);
uint8_t op = instr & 0x3F;
float *vd = m_state.vf[fd];
float normalizedVs[4];
float normalizedVt[4];
float normalizedAcc[4];
// NOP has no operands or flag effects.
const uint8_t special = static_cast<uint8_t>((instr & 3u) | ((instr >> 4) & 0x7Cu));
if (op >= 0x3Cu && (special == 0x2Fu || special == 0x30u))
return;
float *vd = vfResult;
auto &operands = m_upperOperands;
for (uint32_t component = 0; component < 4u; ++component)
{
normalizedVs[component] = normalizeOperand(m_state.vf[fs][component]);
normalizedVt[component] = normalizeOperand(m_state.vf[ft][component]);
normalizedAcc[component] = normalizeOperand(m_state.acc[component]);
operands.vs[component] = normalizeOperand(m_state.vf[fs][component]);
operands.vt[component] = normalizeOperand(m_state.vf[ft][component]);
operands.acc[component] = normalizeOperand(m_state.acc[component]);
}
const float *vs = normalizedVs;
const float *vt = normalizedVt;
const float *acc = normalizedAcc;
const float q = normalizeOperand(m_state.q);
const float i = normalizeOperand(m_state.i);
const float *vs = operands.vs;
const float *vt = operands.vt;
const float *acc = operands.acc;
const float q = operands.q = normalizeOperand(m_state.q);
const float i = operands.i = normalizeOperand(m_state.i);
float result[4];
// Upper opcode decoding (bits 5:0 of upper word)
@@ -55,7 +57,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x02:
case 0x03: // ADDbc
{
float bc = broadcast(vt, op & 3);
float bc = vt[op & 3];
for (int c = 0; c < 4; c++)
result[c] = vs[c] + bc;
applyFmacDest(vd, result, dest);
@@ -66,7 +68,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x06:
case 0x07: // SUBbc
{
float bc = broadcast(vt, op & 3);
float bc = vt[op & 3];
for (int c = 0; c < 4; c++)
result[c] = vs[c] - bc;
applyFmacDest(vd, result, dest);
@@ -77,7 +79,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x0A:
case 0x0B: // MADDbc
{
float bc = broadcast(vt, op & 3);
float bc = vt[op & 3];
for (int c = 0; c < 4; c++)
result[c] = acc[c] + vs[c] * bc;
applyFmacDest(vd, result, dest);
@@ -88,7 +90,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x0E:
case 0x0F: // MSUBbc
{
float bc = broadcast(vt, op & 3);
float bc = vt[op & 3];
for (int c = 0; c < 4; c++)
result[c] = acc[c] - vs[c] * bc;
applyFmacDest(vd, result, dest);
@@ -99,7 +101,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x12:
case 0x13: // MAXbc
{
float bc = broadcast(vt, op & 3);
float bc = vt[op & 3];
for (int c = 0; c < 4; c++)
result[c] = (vs[c] > bc) ? vs[c] : bc;
applyDest(vd, result, dest);
@@ -110,7 +112,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x16:
case 0x17: // MINIbc
{
float bc = broadcast(vt, op & 3);
float bc = vt[op & 3];
for (int c = 0; c < 4; c++)
result[c] = (vs[c] < bc) ? vs[c] : bc;
applyDest(vd, result, dest);
@@ -121,7 +123,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x1A:
case 0x1B: // MULbc
{
float bc = broadcast(vt, op & 3);
float bc = vt[op & 3];
for (int c = 0; c < 4; c++)
result[c] = vs[c] * bc;
applyFmacDest(vd, result, dest);
@@ -240,7 +242,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x3F:
{
const uint8_t specialOp = static_cast<uint8_t>((instr & 0x3u) | ((instr >> 4) & 0x7Cu));
float *vtDest = m_state.vf[ft];
float *vtDest = vfResult;
switch (specialOp)
{
@@ -249,10 +251,10 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x02:
case 0x03: // ADDAbc
{
float bc = broadcast(vt, specialOp & 3);
float bc = vt[specialOp & 3];
for (int c = 0; c < 4; c++)
result[c] = vs[c] + bc;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
}
case 0x04:
@@ -260,10 +262,10 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x06:
case 0x07: // SUBAbc
{
float bc = broadcast(vt, specialOp & 3);
float bc = vt[specialOp & 3];
for (int c = 0; c < 4; c++)
result[c] = vs[c] - bc;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
}
case 0x08:
@@ -271,10 +273,10 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x0A:
case 0x0B: // MADDAbc
{
float bc = broadcast(vt, specialOp & 3);
float bc = vt[specialOp & 3];
for (int c = 0; c < 4; c++)
result[c] = acc[c] + vs[c] * bc;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
}
case 0x0C:
@@ -282,10 +284,10 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x0E:
case 0x0F: // MSUBAbc
{
float bc = broadcast(vt, specialOp & 3);
float bc = vt[specialOp & 3];
for (int c = 0; c < 4; c++)
result[c] = acc[c] - vs[c] * bc;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
}
case 0x10: // ITOF0
@@ -361,16 +363,16 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x1A:
case 0x1B: // MULAbc
{
float bc = broadcast(vt, specialOp & 3);
float bc = vt[specialOp & 3];
for (int c = 0; c < 4; c++)
result[c] = vs[c] * bc;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
}
case 0x1C: // MULAq
for (int c = 0; c < 4; c++)
result[c] = vs[c] * q;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x1D: // ABS
for (int c = 0; c < 4; c++)
@@ -380,7 +382,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x1E: // MULAi
for (int c = 0; c < 4; c++)
result[c] = vs[c] * i;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x1F: // CLIP
{
@@ -417,74 +419,74 @@ void VU1Interpreter::execUpper(uint32_t instr)
case 0x20: // ADDAq
for (int c = 0; c < 4; c++)
result[c] = vs[c] + q;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x21: // MADDAq
for (int c = 0; c < 4; c++)
result[c] = acc[c] + vs[c] * q;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x22: // ADDAi
for (int c = 0; c < 4; c++)
result[c] = vs[c] + i;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x23: // MADDAi
for (int c = 0; c < 4; c++)
result[c] = acc[c] + vs[c] * i;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x24: // SUBAq
for (int c = 0; c < 4; c++)
result[c] = vs[c] - q;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x25: // MSUBAq
for (int c = 0; c < 4; c++)
result[c] = acc[c] - vs[c] * q;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x26: // SUBAi
for (int c = 0; c < 4; c++)
result[c] = vs[c] - i;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x27: // MSUBAi
for (int c = 0; c < 4; c++)
result[c] = acc[c] - vs[c] * i;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x28: // ADDA
for (int c = 0; c < 4; c++)
result[c] = vs[c] + vt[c];
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x29: // MADDA
for (int c = 0; c < 4; c++)
result[c] = acc[c] + vs[c] * vt[c];
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x2A: // MULA
for (int c = 0; c < 4; c++)
result[c] = vs[c] * vt[c];
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x2C: // SUBA
for (int c = 0; c < 4; c++)
result[c] = vs[c] - vt[c];
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x2D: // MSUBA
for (int c = 0; c < 4; c++)
result[c] = acc[c] - vs[c] * vt[c];
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x2E: // OPMULA
result[0] = vs[1] * vt[2];
result[1] = vs[2] * vt[0];
result[2] = vs[0] * vt[1];
result[3] = 0.0f;
applyFmacDestAcc(result, dest);
applyFmacDest(accResult, result, dest);
return;
case 0x2F:
case 0x30: // NOP
+6
View File
@@ -8,6 +8,12 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(CTest)
if(BUILD_TESTING)
add_subdirectory(gs_cache)
if(PS2X_GS_BACKEND STREQUAL "PARALLEL_GS")
add_executable(ps2_gs_parallel_tests gs_parallel_tests.cpp)
target_link_libraries(ps2_gs_parallel_tests PRIVATE ps2_runtime)
add_test(NAME gs.parallel_gpu COMMAND ps2_gs_parallel_tests)
set_tests_properties(gs.parallel_gpu PROPERTIES LABELS "gs;gpu;interop" TIMEOUT 120)
endif()
endif()
# Static library with test logic (no main), used by ps2xStudio
+3
View File
@@ -15,6 +15,7 @@ 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"
"${PS2_GS_RUNTIME_DIR}/src/lib/gs/ps2_gif_arbiter.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)
@@ -41,6 +42,8 @@ function(add_gs_cache_suite target source prefix)
endforeach()
endfunction()
add_gs_cache_suite(ps2_gs_raw_routing_tests gs_raw_routing_tests.cpp routing raw_commands)
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
+170
View File
@@ -0,0 +1,170 @@
#include "runtime/gs/gs_frontend.h"
#include "runtime/gs/ps2_gif_arbiter.h"
#include "runtime/ps2_memory.h"
#include <cstring>
#include <iostream>
#include <stdexcept>
namespace
{
void check(bool condition, const char *message)
{
if (!condition)
throw std::runtime_error(message);
}
struct RawBackend final : GSRasterBackend
{
std::vector<GifArbiterPacket> packets;
std::vector<std::pair<uint8_t, uint64_t>> registers;
std::vector<uint8_t> image;
unsigned batches = 0, cluts = 0, transfers = 0, snapshots = 0;
void Initialize(uint8_t *, uint32_t) override
{
}
void Reset() override
{
}
bool UsesRawCommands() const override
{
return true;
}
void ProcessGIF(uint32_t path, const uint8_t *data, uint32_t size) override
{
packets.push_back({GifPathId(path), false, false, {data, data + size}});
}
void WriteRegisterRaw(uint8_t addr, uint64_t value) override
{
registers.emplace_back(addr, value);
}
void ReadRegisterState(GSDebugSnapshot &s) const override
{
s.ctx[0].frame.fbp = 37;
}
void Submit(const GSPrimitiveBatch &) override
{
++batches;
}
void LoadClut(const GSTex0Reg &, const GSTexClutReg &) override
{
++cluts;
}
void BeginTransfer(const GSTransferCommand &) override
{
++transfers;
}
void UploadImage(const uint8_t *data, uint32_t size) override
{
image.insert(image.end(), data, data + size);
}
void Flush() override
{
}
void TextureFlush() override
{
}
void Sync(GSSyncReason) override
{
}
PresentationFrame Present(const GSPresentationRequest &) override
{
return {};
}
bool ClearFramebuffer(const GSContext &, uint32_t) override
{
return true;
}
uint32_t ConsumeLocalToHostBytes(uint8_t *, uint32_t) override
{
return 0;
}
uint32_t ReadVram(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) const override
{
return 0;
}
void WriteVram(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) override
{
}
void SnapshotVram(std::vector<uint8_t> &) const override
{
throw std::runtime_error("unexpected raw VRAM snapshot");
}
GSTransferSnapshot GetTransferSnapshot() const override
{
return {};
}
};
}
int main()
{
try
{
std::vector<uint8_t> vram(4 * 1024 * 1024);
GSRegisters priv{};
GS gs;
gs.init(vram.data(), uint32_t(vram.size()), &priv);
auto backend = std::make_unique<RawBackend>();
auto &raw = *backend;
gs.setRasterBackend(std::move(backend));
const uint64_t packed[] = {1ull | (1ull << 15) | (1ull << 60), 14, 0xff112233, GS_REG_RGBAQ};
auto *packet = reinterpret_cast<const uint8_t *>(packed);
GifArbiter arbiter([&](GifPathId path, const uint8_t *data, uint32_t size)
{ gs.processGIFPacket(data, size, uint32_t(path)); });
for (auto path : {GifPathId::Path3, GifPathId::Path1, GifPathId::Path2})
arbiter.submit(path, packet, sizeof(packed));
arbiter.drain();
arbiter.drain();
check(raw.packets.size() == 3, "arbiter duplicated or lost a packet");
for (unsigned i = 0; i < 3; ++i)
{
check(uint32_t(raw.packets[i].pathId) == i + 1, "arbiter lost PATH identity");
check(raw.packets[i].data == std::vector<uint8_t>(packet, packet + sizeof(packed)), "arbiter changed GIF bytes");
}
check(gs.processNativePackedGIFPacket(packet, sizeof(packed)), "native packed rejected");
check(raw.packets.size() == 4 && raw.packets.back().pathId == GifPathId::Path3, "native packed route incorrect");
check(raw.registers.empty(), "GIF was also decoded by CPU frontend");
gs.processGIFPacket(packet, 16, 1);
gs.processGIFPacket(packet + 16, 16, 1);
check(raw.packets.size() == 6 && raw.packets[4].data.size() == 16, "split GIF was discarded");
const uint8_t pixels[] = {1, 2, 3, 4, 5};
gs.uploadImageNative(1, 2, 3, 0, pixels, sizeof(pixels));
check(raw.registers == std::vector<std::pair<uint8_t, uint64_t>>{{GS_REG_BITBLTBUF, 1}, {GS_REG_TRXPOS, 2}, {GS_REG_TRXREG, 3}, {GS_REG_TRXDIR, 0}}, "native upload duplicated setup");
check(raw.image == std::vector<uint8_t>(pixels, pixels + sizeof(pixels)), "native upload changed bytes");
gs.writeRegister(GS_REG_SIGNAL, ~0ull);
gs.writeRegister(GS_REG_FINISH, 0);
gs.writeRegister(GS_REG_LABEL, ~0ull);
check(priv.csr == 0 && priv.siglblid == 0, "frontend executed raw backend IRQ effects twice");
gs.writeRegister(GS_REG_TEX0_1, 0);
gs.writeRegister(GS_REG_XYZ2, 0);
check(raw.batches == 0 && raw.cluts == 0 && raw.transfers == 0, "CPU work leaked into raw route");
check(gs.getDebugSnapshot().ctx[0].frame.fbp == 37 && gs.getContextFrame(0).fbp == 37, "diagnostics used stale CPU state");
gs.latchHostPresentationFrame();
gs.shutdownBackend();
std::cout << "Raw GIF routing, PATH, native shortcuts and side-effect ownership passed\n";
return 0;
}
catch (const std::exception &e)
{
std::cerr << e.what() << '\n';
return 1;
}
}
+34
View File
@@ -0,0 +1,34 @@
cmake_minimum_required(VERSION 3.21)
project(PS2ParallelGSTests LANGUAGES C CXX)
include(CTest)
include(FetchContent)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
FetchContent_Declare(raylib GIT_REPOSITORY https://github.com/raysan5/raylib.git GIT_TAG 5.5)
FetchContent_MakeAvailable(raylib)
get_filename_component(runtime "../../ps2xRuntime" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_LIST_DIR}")
add_library(ps2_runtime STATIC
"${runtime}/src/lib/gs/gs_frontend.cpp"
"${runtime}/src/lib/gs/gs_cpu_backend.cpp"
"${runtime}/src/lib/gs/ps2_gs_memory.cpp")
target_include_directories(ps2_runtime PUBLIC "${runtime}/include")
target_link_libraries(ps2_runtime PUBLIC raylib)
if(NOT MSVC AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
target_compile_options(ps2_runtime PRIVATE -msse4.1)
endif()
include("${runtime}/cmake/ParallelGS.cmake")
add_executable(ps2_gs_parallel_tests ../gs_parallel_tests.cpp)
target_link_libraries(ps2_gs_parallel_tests PRIVATE ps2_runtime)
add_test(NAME gs.parallel_gpu COMMAND ps2_gs_parallel_tests)
set_tests_properties(gs.parallel_gpu PROPERTIES LABELS "gs;gpu;interop" TIMEOUT 120)
+312
View File
@@ -0,0 +1,312 @@
#include "runtime/gs/gs_frontend.h"
#include "runtime/gs/gs_parallel_backend.h"
#include "runtime/ps2_memory.h"
#include "raylib.h"
#include "rlgl.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <iostream>
#include <stdexcept>
#include <string>
namespace
{
void check(bool ok, const char *message)
{
if (!ok)
throw std::runtime_error(message);
}
void setupTransfer(GS &gs, unsigned psm, unsigned width, unsigned height, unsigned direction, unsigned src = 0, unsigned dst = 0)
{
gs.writeRegister(GS_REG_BITBLTBUF, uint64_t(src) | (1ull << 16) | (uint64_t(psm) << 24) |
(uint64_t(dst) << 32) | (1ull << 48) | (uint64_t(psm) << 56));
gs.writeRegister(GS_REG_TRXPOS, 0);
gs.writeRegister(GS_REG_TRXREG, width | (uint64_t(height) << 32));
gs.writeRegister(GS_REG_TRXDIR, direction);
}
void testTransfer(GS &gs, unsigned psm, unsigned bpp)
{
constexpr unsigned count = 13;
const auto bytes = (count * bpp + 7) / 8;
std::vector<uint8_t> input(bytes), output(bytes);
for (unsigned i = 0; i < bytes; ++i)
input[i] = uint8_t(31 + i * 7);
if (bpp == 4)
input.back() &= 15;
uint64_t bitblt = (1ull << 16) | (uint64_t(psm) << 24) | (1ull << 48) | (uint64_t(psm) << 56);
// Exercise the native upload and its final incomplete HWREG word.
gs.uploadImageNative(bitblt, 0, count | (1ull << 32), 0, input.data(), unsigned(input.size()));
if (bpp == 4)
{
// GS FIFO does not support 4-bit readback; check these uploads/copies in VRAM.
for (unsigned base : {0u, 64u})
{
if (base)
setupTransfer(gs, psm, count, 1, 2, 0, base);
for (unsigned x = 0; x < count; ++x)
check(gs.ReadVram(psm, base, 1, x, 0) == ((input[x / 2] >> ((x & 1) * 4)) & 15), "4-bit transfer mismatch");
}
std::cout << "PSM " << psm << ": indexed upload and local copy passed\n";
return;
}
setupTransfer(gs, psm, count, 1, 1);
check(gs.consumeLocalToHostBytes(output.data(), 3) == 3, "short FIFO read failed");
check(gs.consumeLocalToHostBytes(output.data() + 3, bytes - 3) == bytes - 3, "FIFO tail lost");
if (input != output)
throw std::runtime_error("upload/FIFO mismatch for PSM " + std::to_string(psm));
check(gs.consumeLocalToHostBytes(output.data(), 1) == 0, "FIFO exceeded transfer bounds");
setupTransfer(gs, psm, count, 1, 2, 0, 64);
setupTransfer(gs, psm, count, 1, 1, 64);
check(gs.consumeLocalToHostBytes(output.data(), bytes) == bytes && input == output, "local copy mismatch");
std::cout << "PSM " << psm << ": native upload, partial FIFO, local copy passed\n";
}
void sprite(GS &gs, uint32_t color, uint32_t depth = 0, bool textured = false)
{
gs.writeRegister(GS_REG_PRIM, GS_PRIM_SPRITE | (textured ? 0x110 : 0));
gs.writeRegister(GS_REG_RGBAQ, color | (uint64_t(0x3f800000) << 32));
gs.writeRegister(GS_REG_UV, 0);
gs.writeRegister(GS_REG_XYZ2, uint64_t(depth) << 32);
gs.writeRegister(GS_REG_UV, (2ull * 16) | ((2ull * 16) << 16));
gs.writeRegister(GS_REG_XYZ2, (8ull * 16) | ((8ull * 16) << 16) | (uint64_t(depth) << 32));
}
void testRaster(GS &gs)
{
gs.writeRegister(GS_REG_FRAME_1, 1ull << 16);
gs.writeRegister(GS_REG_SCISSOR_1, (63ull << 16) | (63ull << 48));
gs.writeRegister(GS_REG_XYOFFSET_1, 0);
gs.writeRegister(GS_REG_ZBUF_1, 32 | (1ull << 32));
gs.writeRegister(GS_REG_TEST_1, 1ull << 16 | 1ull << 17);
gs.writeRegister(GS_REG_PRMODECONT, 1);
sprite(gs, 0x80402010);
check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80402010, "solid sprite raster failed");
gs.writeRegister(GS_REG_ZBUF_1, 32);
gs.writeRegister(GS_REG_TEST_1, 1ull << 16 | 2ull << 17); // GEQUAL
gs.WriteVram(GS_PSM_Z32, 32 * 32, 1, 2, 2, 100);
sprite(gs, 0x80ffffff, 99);
check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80402010, "depth rejection failed");
sprite(gs, 0x80a0b0c0, 101);
check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80a0b0c0, "depth acceptance failed");
gs.writeRegister(GS_REG_TEST_1, 1ull << 16 | 1ull << 17);
gs.writeRegister(GS_REG_ZBUF_1, 32 | (1ull << 32));
for (unsigned y = 0; y < 2; ++y)
for (unsigned x = 0; x < 2; ++x)
gs.WriteVram(GS_PSM_T8, 256, 1, x, y, 1);
gs.WriteVram(GS_PSM_CT32, 512, 1, 1, 0, 0x80224466);
gs.writeRegister(GS_REG_TEXCLUT, 1);
gs.writeRegister(GS_REG_TEX0_1, 256 | (1ull << 14) | (uint64_t(GS_PSM_T8) << 20) |
(1ull << 26) | (1ull << 30) | (1ull << 34) | (1ull << 35) |
(512ull << 37) | (1ull << 55) | (1ull << 61));
sprite(gs, 0x80808080, 0, true);
check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80224466, "indexed texture/CLUT failed");
// Fixed-factor blending: (Cs - Cd) * FIX / 128 + Cd, FIX = 64.
gs.writeRegister(GS_REG_ALPHA_1, (1ull << 2) | (2ull << 4) | (1ull << 6) | (64ull << 32));
gs.writeRegister(GS_REG_PRIM, GS_PRIM_SPRITE | 64);
gs.writeRegister(GS_REG_RGBAQ, 0x806688aa);
gs.writeRegister(GS_REG_XYZ2, 0);
gs.writeRegister(GS_REG_XYZ2, 128 | (128ull << 16));
check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80446688, "blending failed");
gs.writeRegister(GS_REG_SIGNAL, (0xffffffffull << 32) | 123);
gs.writeRegister(GS_REG_LABEL, (0xffffffffull << 32) | 456);
gs.writeRegister(GS_REG_FINISH, 0);
std::cout << "Raster, depth, CLUT/indexed texture, blending and FINISH passed\n";
}
void testGIF(GS &gs)
{
const uint64_t tag[] = {1ull | (1ull << 15) | (1ull << 60), 14};
const uint64_t one[] = {17ull << 16, GS_REG_FRAME_1};
const uint64_t two[] = {23ull << 16, GS_REG_FRAME_2};
// Incomplete tags on independent paths must survive interleaving.
gs.processGIFPacket(reinterpret_cast<const uint8_t *>(tag), 16, 1);
gs.processGIFPacket(reinterpret_cast<const uint8_t *>(tag), 16, 2);
gs.processGIFPacket(reinterpret_cast<const uint8_t *>(two), 16, 2);
gs.processGIFPacket(reinterpret_cast<const uint8_t *>(one), 16, 1);
auto snapshot = gs.getDebugSnapshot();
check(snapshot.ctx[0].frame.fbw == 17 && snapshot.ctx[1].frame.fbw == 23, "interleaved GIF paths corrupted register state");
uint64_t packet[] = {
4ull | (1ull << 60),
14,
(1ull << 48) | (128ull << 32),
GS_REG_BITBLTBUF,
0,
GS_REG_TRXPOS,
3ull | (1ull << 32),
GS_REG_TRXREG,
0,
GS_REG_TRXDIR,
1ull | (1ull << 15) | (2ull << 58),
0,
0x1020304050607080ull,
0x90a0b0c0ull,
};
gs.processGIFPacket(reinterpret_cast<const uint8_t *>(packet), sizeof(packet), 3);
check(gs.ReadVram(0, 128, 1, 0, 0) == 0x50607080 &&
gs.ReadVram(0, 128, 1, 1, 0) == 0x10203040 &&
gs.ReadVram(0, 128, 1, 2, 0) == 0x90a0b0c0,
"GIF IMAGE differs from native upload");
std::cout << "Interleaved PATH1/2 and GIF IMAGE transfer passed\n";
}
void testPendingPresentation(GS &gs, GSRasterBackend &backend)
{
GSPresentationRequest request{};
request.pmode = 1 | (1ull << 5) | (0xffull << 8);
request.smode1 = 32ull << 3;
request.dispfb1 = 10ull << 9;
request.display1 = 318 | (50ull << 12) | (1ull << 23) | (1279ull << 32) | (447ull << 44);
std::vector<uint32_t> pixels(640 * 448);
const auto before = backend.GetReadbackCount();
for (unsigned pass = 0; pass < 8; ++pass)
{
backend.Flush();
// Force the legal interleaving of a game upload between the frontend's
// Flush and Present, without relying on thread scheduling or sleeps.
const uint32_t color = pass & 1 ? 0xff225488 : 0xff6688aa;
std::fill(pixels.begin(), pixels.end(), color);
gs.uploadImageNative(10ull << 48, 0, 640ull | (448ull << 32), 0,
reinterpret_cast<const uint8_t *>(pixels.data()),
uint32_t(pixels.size() * sizeof(uint32_t)));
std::cout << "Present with pending GS upload: " << pass << std::endl;
auto frame = backend.Present(request);
check(frame.gpu && frame.width && frame.height, "pending upload produced no scanout");
Texture2D texture{frame.gpu->AcquireTexture(), int(frame.width), int(frame.height), 1,
PIXELFORMAT_UNCOMPRESSED_R8G8B8A8};
Image image = LoadImageFromTexture(texture);
Color center = GetImageColor(image, int(frame.width / 2), int(frame.height / 2));
UnloadImage(image);
rlDrawRenderBatchActive();
frame.gpu->ReleaseTexture();
check(center.r == uint8_t(color) && center.g == uint8_t(color >> 8) &&
center.b == uint8_t(color >> 16),
"pending upload missing from scanout");
}
check(backend.GetReadbackCount() == before, "pending presentation requested VRAM readback");
std::cout << "Uploads between Flush and Present passed\n";
}
void testPresentation(GS &gs, GSRegisters &priv)
{
// 480p CRTC, only circuit 1 enabled. Use a known color for interop readback.
gs.writeRegister(GS_REG_FRAME_1, 10ull << 16);
gs.writeRegister(GS_REG_SCISSOR_1, (639ull << 16) | (447ull << 48));
check(gs.clearFramebufferContext(0, 0xff225488), "scanout setup clear failed");
gs.writeRegister(GS_REG_SCISSOR_1, (639ull << 16) | (223ull << 48));
check(gs.clearFramebufferContext(0, 0xff6688aa), "scanout top-band clear failed");
gs.writeRegister(GS_REG_FRAME_2, 160 | (10ull << 16));
gs.writeRegister(GS_REG_SCISSOR_2, (639ull << 16) | (447ull << 48));
check(gs.clearFramebufferContext(1, 0xff448822), "second circuit clear failed");
// CRTC ALP uses 0..255, unlike the drawing ALPHA.FIX factor (0x80 = 1).
priv.pmode = 1 | (1ull << 5) | (0xffull << 8);
priv.smode1 = 32ull << 3;
priv.smode2 = 0;
priv.dispfb1 = 10ull << 9;
priv.display1 = 318 | (50ull << 12) | (1ull << 23) | (1279ull << 32) | (447ull << 44);
const auto before = gs.getReadbackCount();
for (unsigned pass = 0; pass < 6; ++pass)
{
if (pass == 2)
{
priv.pmode = 3 | (1ull << 5) | (0x80ull << 8);
priv.dispfb2 = 160 | (10ull << 9);
priv.display2 = priv.display1;
}
if (pass == 3)
{
priv.pmode &= ~2ull;
priv.smode1 |= 2ull << 13;
priv.smode2 = 3;
priv.display1 = 636 | (50ull << 12) | (3ull << 23) | (2559ull << 32) | (447ull << 44);
}
priv.csr = (pass & 1) << 13;
priv.vsyncTick++;
gs.latchHostPresentationFrame();
uint32_t w = 0, h = 0;
float aspect = 0;
auto gpu = gs.getLatchedGpuFrame(w, h, aspect);
check(gpu && w && h && aspect > 0, "CRTC returned no GPU frame");
std::vector<uint8_t> pixels;
check(!gs.copyLatchedHostPresentationFrame(pixels, w, h), "presentation produced CPU pixels");
gpu = gs.getLatchedGpuFrame(w, h, aspect);
for (unsigned repeat = 0; repeat < 2; ++repeat)
{
Texture2D texture{gpu->AcquireTexture(), int(w), int(h), 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8};
if ((pass == 0 || pass == 2) && repeat == 0)
{
// Test-only explicit readback proves that imported GL memory contains scanout.
Image image = LoadImageFromTexture(texture);
Color center = GetImageColor(image, int(w / 2), int(h * 3 / 4));
Color top = GetImageColor(image, int(w / 2), int(h / 4));
UnloadImage(image);
std::cout << "Scanout pass " << pass << ": " << w << 'x' << h
<< " center RGB=" << unsigned(center.r) << ',' << unsigned(center.g) << ',' << unsigned(center.b)
<< " top RGB=" << unsigned(top.r) << ',' << unsigned(top.g) << ',' << unsigned(top.b) << '\n';
if (pass == 0)
{
check(center.r == 0x88 && center.g == 0x54 && center.b == 0x22, "interop scanout pixels mismatch");
check(top.r == 0xaa && top.g == 0x88 && top.b == 0x66, "scanout orientation mismatch");
}
else
check(center.r == 0x55 && center.g == 0x6e && center.b == 0x33, "dual-circuit composition mismatch");
}
BeginDrawing();
ClearBackground(BLACK);
DrawTexturePro(texture, {0, 0, float(w), float(h)}, {0, 0, 320, 240}, {0, 0}, 0, WHITE);
rlDrawRenderBatchActive();
gpu->ReleaseTexture();
EndDrawing();
}
if (pass == 2)
SetWindowSize(400, 300);
}
check(gs.getReadbackCount() == before, "presentation requested VRAM readback");
std::cout << "Progressive/interlaced scanout, repeated frames and resize passed\n";
}
}
int main()
{
try
{
GSRegisters regs{};
auto backend = CreateParallelGSBackend(regs);
return 1;
}
catch (const std::runtime_error &)
{
} // Initialization before a context must fail explicitly.
SetConfigFlags(FLAG_WINDOW_HIDDEN);
InitWindow(320, 240, "parallel-gs validation");
int result = 0;
try
{
GS gs;
GSRegisters priv{};
std::vector<uint8_t> vram(4 * 1024 * 1024);
gs.init(vram.data(), uint32_t(vram.size()), &priv);
auto backend = CreateParallelGSBackend(priv);
auto *rawBackend = backend.get();
gs.setRasterBackend(std::move(backend));
priv.pmode = 3;
gs.writeRegister(0x59, 32);
gs.writeRegister(0x59, 32); // Repeated register write is not another flip.
gs.writeRegister(0x5b, 32);
check(priv.displayFlipCount[0].load() == 1 && priv.displayFlipCount[1].load() == 1,
"parallel backend display flips missing or duplicated");
testPendingPresentation(gs, *rawBackend);
for (auto [psm, bits] : std::array<std::pair<unsigned, unsigned>, 13>{{{0, 32}, {1, 24}, {2, 16}, {10, 16}, {19, 8}, {20, 4}, {27, 8}, {36, 4}, {44, 4}, {48, 32}, {49, 24}, {50, 16}, {58, 16}}})
testTransfer(gs, psm, bits);
testGIF(gs);
testRaster(gs);
check((priv.csr & 3) == 3 && priv.siglblid == (456ull << 32 | 123), "IRQ side effects incorrect");
testPresentation(gs, priv);
gs.reset();
testRaster(gs);
gs.shutdownBackend();
std::cout << "Reset and shutdown passed\n";
}
catch (const std::exception &e)
{
std::cerr << "GPU validation failed: " << e.what() << '\n';
result = 1;
}
CloseWindow();
return result;
}
+114 -6
View File
@@ -503,7 +503,7 @@ void register_ps2_gs_tests()
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
R5900Context ctx{};
setRegU32(ctx, 4, 1u); // interlaced
setRegU32(ctx, 5, 0u); // NTSC
setRegU32(ctx, 5, 2u); // NTSC (GS CRT mode 0x02)
setRegU32(ctx, 6, 0u); // field mode
runtime.memory().gs().pmode = 0u;
@@ -512,6 +512,10 @@ void register_ps2_gs_tests()
t.Equals(runtime.memory().gs().smode2, 0x1ull,
"GsSetCrt should publish interlaced field mode through SMODE2");
t.Equals((runtime.memory().gs().smode1 >> 3) & 127ull, 32ull,
"GsSetCrt should program the analog clock for the hardware CRTC");
t.Equals((runtime.memory().gs().smode1 >> 13) & 3ull, 2ull,
"GsSetCrt should program NTSC in SMODE1");
t.Equals(runtime.memory().gs().pmode & 0x3ull, 0x1ull,
"GsSetCrt should leave CRT1 enabled for presentation");
t.Equals(getRegU32Test(ctx, 2), 0u,
@@ -557,8 +561,9 @@ void register_ps2_gs_tests()
std::memcpy(&xyoffset10Addr, rdram.data() + kEnvAddr + kXYOffset1AddrOffset, sizeof(xyoffset10Addr));
t.Equals((dispfb0 >> 9) & 0x3Fu, 10ull, "dbuff display env should seed FBW from width");
t.Equals((display0 >> 32) & 0x0FFFull, 639ull, "dbuff display env should seed DW from width");
t.Equals((display0 >> 44) & 0x07FFull, 447ull, "dbuff display env should seed DH from height");
t.Equals((display0 >> 23) & 0x0Full, 3ull, "NTSC should use four output clocks per pixel");
t.Equals((display0 >> 32) & 0x0FFFull, 2559ull, "DW is measured in output clocks, not framebuffer pixels");
t.Equals((display0 >> 44) & 0x07FFull, 895ull, "interlaced frame mode should double the supplied height");
t.Equals((frame10 >> 16) & 0x3Full, 10ull, "dbuff draw env should seed FRAME FBW from width");
t.Equals(frame10Addr, 0x4Cull, "dbuff draw env should seed FRAME_1 register id");
t.Equals(xyoffset10 & 0xFFFFull, 0x6C00ull, "dbuff draw env should seed OFX in 12.4 fixed point");
@@ -572,6 +577,12 @@ void register_ps2_gs_tests()
dispfb1 = (dispfb1 & ~0x1FFull) | 151ull;
std::memcpy(rdram.data() + kEnvAddr + kDispEnvSize + kDispFbOffset, &dispfb1, sizeof(dispfb1));
// Exercise both circuits so the SDK shortcut must update the same
// per-circuit counters as native GS and MMIO writes.
const uint64_t pmode = 3u;
std::memcpy(rdram.data() + kEnvAddr, &pmode, sizeof(pmode));
std::memcpy(rdram.data() + kEnvAddr + kDispEnvSize, &pmode, sizeof(pmode));
std::memset(&ctx, 0, sizeof(ctx));
setRegU32(ctx, 4, kEnvAddr);
setRegU32(ctx, 5, 1u);
@@ -579,8 +590,92 @@ void register_ps2_gs_tests()
t.Equals(runtime.memory().gs().dispfb1 & 0x1FFull, 151ull,
"sceGsSwapDBuffDc should program GS to the selected display page");
t.Equals((runtime.memory().gs().display1 >> 32) & 0x0FFFull, 639ull,
t.Equals((runtime.memory().gs().display1 >> 32) & 0x0FFFull, 2559ull,
"sceGsSwapDBuffDc should preserve the display width from the seeded env");
auto &regs = runtime.memory().gs();
t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "SDK swap must count the first display flip");
t.Equals(regs.displayFlipCount[1].load(), uint64_t(1), "SDK swap must count the second circuit independently");
ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime);
t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "repeating the SDK swap must not add a flip");
setRegU32(ctx, 5, 0u);
ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime);
t.Equals(regs.displayFlipCount[0].load(), uint64_t(2), "SDK swap back to the other page must count");
t.Equals(regs.displayFlipCount[1].load(), uint64_t(2), "SDK swap back must count for both circuits");
t.Equals(regs.sdkPresentCount.load(), uint64_t(3), "each completed SDK swap counts once, not once per circuit");
// Retail games can alternate draw buffers while both display envs
// point at one scanout page. FBP changes cannot measure their FPS.
std::memcpy(rdram.data() + kEnvAddr + kDispEnvSize + kDispFbOffset, &dispfb0, sizeof(dispfb0));
setRegU32(ctx, 5, 1u);
ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime);
setRegU32(ctx, 5, 0u);
ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime);
t.Equals(regs.sdkPresentCount.load(), uint64_t(5), "fixed scanout page must still count SDK presentations");
t.Equals(regs.displayFlipCount[0].load(), uint64_t(2), "SDK presentation must not fabricate a buffer flip");
ps2_stubs::sceGsSwapDBuff(rdram.data(), &ctx, &runtime);
t.Equals(regs.sdkPresentCount.load(), uint64_t(6), "non-DC SDK swap also counts once");
ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, nullptr);
t.Equals(regs.sdkPresentCount.load(), uint64_t(6), "failed swap must not count as a presentation");
});
tc.Run("sceGsSetDefDispEnv matches libgs display timing and initializes every register", [](TestCase &t)
{
PS2Runtime runtime;
t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed");
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
constexpr uint32_t envAddr = 0x4000u;
struct ModeCase
{
uint32_t mode, interlace, frame, width, height;
int32_t dx, dy;
uint64_t smode2, expectedDx, expectedDy, magh, dw, dh;
};
// Expected fields from sceGsSetDefDispEnv at 0x100248 in SLUS_201.84.
const ModeCase cases[] = {
{2, 1, 1, 640, 224, 0, 0, 3, 636, 50, 3, 2559, 447},
{2, 1, 0, 640, 448, 0, 0, 1, 636, 50, 3, 2559, 447},
{2, 0, 0, 640, 224, 0, 0, 2, 636, 25, 3, 2559, 223},
{3, 1, 1, 512, 256, 0, 0, 3, 656, 72, 4, 2559, 511},
{3, 0, 1, 640, 256, 0, 0, 2, 656, 36, 3, 2559, 255},
{2, 1, 1, 640, 224, -4, -2, 3, 620, 48, 3, 2559, 447},
};
for (const auto &mode : cases)
{
R5900Context ctx{};
setRegU32(ctx, 4, 0u);
setRegU32(ctx, 5, mode.interlace);
setRegU32(ctx, 6, mode.mode);
setRegU32(ctx, 7, mode.frame);
ps2_stubs::sceGsResetGraph(rdram.data(), &ctx, &runtime);
ps2_stubs::sceGsGetGParam(rdram.data(), &ctx, &runtime);
const uint32_t params = getRegU32Test(ctx, 2);
t.Equals(runtime.memory().read16(params), static_cast<uint16_t>(mode.interlace), "GParam interlace occupies a halfword");
t.Equals(runtime.memory().read16(params + 2), static_cast<uint16_t>(mode.mode), "GParam output mode is at offset 2");
t.Equals(runtime.memory().read16(params + 4), static_cast<uint16_t>(mode.frame), "GParam frame mode is at offset 4");
t.Equals(runtime.memory().read16(params + 6), uint16_t(3), "GParam version is at offset 6");
std::memset(rdram.data() + envAddr, 0xcd, 40);
setRegU32(ctx, 4, envAddr);
setRegU32(ctx, 5, 0u);
setRegU32(ctx, 6, mode.width);
setRegU32(ctx, 7, mode.height);
setRegU32(ctx, 8, static_cast<uint32_t>(mode.dx));
setRegU32(ctx, 9, static_cast<uint32_t>(mode.dy));
setRegU32(ctx, 29, 0x3000u);
const uint32_t staleStackArgs[2] = {123u, 456u};
std::memcpy(rdram.data() + 0x3010, staleStackArgs, sizeof(staleStackArgs));
ps2_stubs::sceGsSetDefDispEnv(rdram.data(), &ctx, &runtime);
uint64_t regs[5]{};
std::memcpy(regs, rdram.data() + envAddr, sizeof(regs));
t.Equals(regs[0], 0x66ull, "PMODE should enable circuit 2 with fixed alpha");
t.Equals(regs[1], mode.smode2, "SMODE2 should follow the libgs mode");
t.Equals(regs[2], uint64_t((mode.width + 63) / 64) << 9, "DISPFB should encode the framebuffer stride");
t.Equals(regs[3] & 0xfffull, mode.expectedDx, "DX should include the CRT origin and scaled offset");
t.Equals((regs[3] >> 12) & 0x7ffull, mode.expectedDy, "DY should include the CRT origin");
t.Equals((regs[3] >> 23) & 15ull, mode.magh, "MAGH should match the output clock multiplier");
t.Equals((regs[3] >> 32) & 0xfffull, mode.dw, "DW should cover the output clocks");
t.Equals((regs[3] >> 44) & 0x7ffull, mode.dh, "DH should match field or frame mode");
t.Equals(regs[4], 0ull, "BGCOLOR should be initialized even in dirty guest memory");
}
});
tc.Run("sceGsSetDefDBuffDc seeds a clear packet and swap clears the draw buffer", [](TestCase &t)
@@ -4061,7 +4156,7 @@ void register_ps2_gs_tests()
"TRXDIR payload must encode dir=0 (host-to-local)");
});
tc.Run("sceGsResetGraph frees its temporary GIF packet", [](TestCase &t)
tc.Run("sceGsResetGraph programs the CRT without sending privileged registers through GIF", [](TestCase &t)
{
PS2Runtime runtime;
t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed");
@@ -4076,8 +4171,21 @@ void register_ps2_gs_tests()
t.Equals(static_cast<int32_t>(getRegU32Test(ctx, 2)), 0,
"sceGsResetGraph should succeed in reset mode");
const auto &regs = runtime.memory().gs();
t.Equals((regs.smode1 >> 3) & 127ull, 32ull, "reset should select the analog clock");
t.Equals((regs.smode1 >> 13) & 3ull, 2ull, "reset should select NTSC");
t.Equals(regs.smode2, 3ull, "reset should select interlaced frame mode");
t.Equals(regs.pmode & 3ull, 1ull, "reset should enable circuit 1");
t.Equals(regs.dispfb1, 10ull << 9, "reset should set the display buffer width");
t.Equals(regs.dispfb2, regs.dispfb1, "reset should initialize both display buffers");
t.Equals(runtime.gs().getDebugSnapshot().ctx[1].alpha, 0ull,
"PMODE must not be sent to GIF register 0x41 (ALPHA_2)");
setRegU32(ctx, 6, 3u);
ps2_stubs::sceGsResetGraph(rdram.data(), &ctx, &runtime);
t.Equals((regs.smode1 >> 13) & 3ull, 3ull, "reset should also select PAL");
expectGuestHeapReusable(t, runtime,
"sceGsResetGraph should free its temporary GIF packet");
"sceGsResetGraph should leave the guest heap reusable");
});
tc.Run("sceGsSyncV resumes through the scheduler with deterministic field parity", [](TestCase &t)
+34
View File
@@ -159,6 +159,40 @@ void register_ps2_memory_tests()
{
MiniTest::Case("PS2Memory", [](TestCase &tc)
{
tc.Run("GS display flip counters track guest buffer changes independently of host VSync", [](TestCase &t)
{
PS2Memory mem;
t.IsTrue(mem.initialize(), "memory initialization");
auto &regs = mem.gs();
mem.write64(0x12000000u, 3); // Enable both display circuits.
mem.write64(0x12000070u, 32 | (10ull << 9));
mem.write32(0x12000090u, 32 | (10u << 9));
t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "64-bit flip counted");
t.Equals(regs.displayFlipCount[1].load(), uint64_t(1), "32-bit flip counted independently");
mem.write32(0x12000074u, 1u << 11); // DBY/field offset only.
mem.write32(0x12000070u, 32 | (20u << 9)); // Same FBP, different width.
for (unsigned i = 0; i < 60; ++i)
{
++regs.vsyncTick;
mem.write32(0x12000070u, 32 | (20u << 9));
}
t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "repeats, field offsets and host VSync are not flips");
mem.write64(0x12000000u, 0);
mem.write64(0x12000070u, 64);
t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "disabled display excluded");
mem.write64(0x12000000u, 3);
GS gs;
gs.init(mem.getGSVRAM(), PS2_GS_VRAM_SIZE, &regs);
gs.writeRegister(0x59, 96);
gs.writeRegister(0x5b, 96);
t.Equals(regs.displayFlipCount[0].load(), uint64_t(2), "native GS register path counted");
t.Equals(regs.displayFlipCount[1].load(), uint64_t(2), "second native circuit counted");
gs.shutdownBackend();
t.IsTrue(mem.initialize(), "memory reset");
t.Equals(regs.displayFlipCount[0].load(), uint64_t(0), "reset clears counters");
t.Equals(regs.displayFlipCount[1].load(), uint64_t(0), "reset clears both circuits");
});
tc.Run("uncached aliases map to same RDRAM bytes", [](TestCase &t)
{
PS2Memory mem;
+208
View File
@@ -5,6 +5,7 @@
#include "runtime/ps2_memory.h"
#include "runtime/ps2_vu1.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
@@ -1690,6 +1691,213 @@ void register_ps2_vu1_tests()
"the underflowing product should set sticky Z and U");
});
tc.Run("cycle budget splitting preserves pipelines and PATH1 observations", [](TestCase &t)
{
Vu1Fixture fx;
t.IsTrue(fx.initialize(), "VU1 fixture should initialize");
std::vector<std::vector<uint8_t>> packets;
fx.mem.setGifPacketCallback([&](const uint8_t *data, uint32_t size)
{
packets.emplace_back(data, data + size);
});
writeTrackedVuInstructionPair(fx, 0u, makeVuLowerSpecial(0x6Cu, 0u), kVuUpperNop);
writeTrackedVuInstructionPair(fx, 8u, makeVuSq(0xFu, 4u, 0u, 1),
makeVuUpper(0x29u, 0xFu, 2u, 1u, 3u));
writeTrackedVuInstructionPair(fx, 16u, makeVuDiv(1u, 2u, 0u, 0u), kVuUpperNop);
writeTrackedVuInstructionPair(fx, 24u, makeVuLowerSpecial(0x7Cu, 1u), kVuUpperNop);
writeTrackedVuInstructionPair(fx, 32u, makeVuLowerSpecial(0x7Bu, 0u), kVuUpperNop);
writeTrackedVuInstructionPair(fx, 40u, makeVuSq(0xFu, 3u, 0u, 12),
makeVuUpper(0x1Cu, 0xFu, 0u, 3u, 5u));
for (uint32_t pc = 48u; pc < 512u; pc += 8u)
writeTrackedVuInstructionPair(fx, pc, 0u, kVuUpperNop);
const auto prepare = [&](VU1Interpreter &vu)
{
std::memset(fx.data, 0xAB, PS2_VU1_DATA_SIZE);
const uint64_t tag = makeGifTag(8u, GIF_FMT_IMAGE, 0u, true);
std::memset(fx.data, 0, 16u);
std::memcpy(fx.data, &tag, sizeof(tag));
const float a[4] = {0.25f, 0.5f, 0.75f, 1.0f};
const float b[4] = {2.0f, -2.0f, 4.0f, -4.0f};
std::memcpy(vu.state().vf[1], a, sizeof(a));
std::memcpy(vu.state().vf[2], b, sizeof(b));
std::memcpy(vu.state().vf[4], b, sizeof(b));
};
VU1Interpreter whole;
prepare(whole);
whole.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE,
fx.gs, &fx.mem, 0u, 0u, 0u, 70u);
const VU1State expected = whole.state();
const std::vector<uint8_t> expectedData(fx.data, fx.data + PS2_VU1_DATA_SIZE);
const auto expectedPackets = packets;
t.Equals(expectedPackets.size(), size_t(1u), "reference run should emit one packet");
for (const uint32_t slice : {1u, 2u, 3u, 7u, 13u})
{
VU1Interpreter sliced;
prepare(sliced);
packets.clear();
sliced.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE,
fx.gs, &fx.mem, 0u, 0u, 0u, 0u);
for (uint32_t elapsed = 0; elapsed < 70u;)
{
const uint32_t budget = std::min(slice, 70u - elapsed);
sliced.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE,
fx.gs, &fx.mem, 0u, 0u, budget);
elapsed += budget;
t.Equals(sliced.state().cycles, uint64_t(elapsed), "resume must stop at its budget");
}
const auto &actual = sliced.state();
t.IsTrue(std::memcmp(actual.vf, expected.vf, sizeof(actual.vf)) == 0, "VF bits must match");
t.IsTrue(std::memcmp(actual.vi, expected.vi, sizeof(actual.vi)) == 0, "VI values must match");
t.IsTrue(std::memcmp(actual.acc, expected.acc, sizeof(actual.acc)) == 0, "ACC bits must match");
t.Equals(actual.q, expected.q, "Q result must match");
t.Equals(actual.p, expected.p, "P result must match");
t.Equals(actual.pc, expected.pc, "PC must match");
t.Equals(actual.mac, expected.mac, "MAC flags must match");
t.Equals(actual.status, expected.status, "STATUS flags must match");
t.Equals(actual.clip, expected.clip, "CLIP flags must match");
t.IsTrue(std::memcmp(fx.data, expectedData.data(), expectedData.size()) == 0, "store bytes must match");
t.IsTrue(packets == expectedPackets, "PATH1 bytes and packet order must match");
}
});
tc.Run("XGKICK reuse submits only the new packet across reset and memory wrap", [](TestCase &t)
{
Vu1Fixture fx;
t.IsTrue(fx.initialize(), "VU1 fixture should initialize");
std::vector<std::vector<uint8_t>> packets;
fx.mem.setGifPacketCallback([&](const uint8_t *data, uint32_t size)
{
packets.emplace_back(data, data + size);
});
writeTrackedVuInstructionPair(fx, 0u, makeVuLowerSpecial(0x6Cu, 1u), kVuUpperNop | (1u << 30));
writeTrackedVuInstructionPair(fx, 8u, 0u, kVuUpperNop);
VU1Interpreter vu;
unsigned run = 0;
for (const uint32_t qwords : {128u, 1u, 0u, 64u, 2u})
{
const uint32_t start = PS2_VU1_DATA_SIZE - 16u;
const uint64_t tag = makeGifTag(static_cast<uint16_t>(qwords), GIF_FMT_IMAGE, 0u, true);
std::vector<uint8_t> expected((qwords + 1u) * 16u, static_cast<uint8_t>(++run));
std::memset(expected.data(), 0, 16u);
std::memcpy(expected.data(), &tag, sizeof(tag));
for (uint32_t i = 0; i < expected.size(); ++i)
fx.data[(start + i) % PS2_VU1_DATA_SIZE] = expected[i];
if (run % 2u == 0u)
vu.reset();
vu.state().vi[1] = start / 16u;
packets.clear();
vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem);
t.Equals(packets.size(), size_t(1u), "one complete packet should be emitted");
if (!packets.empty())
t.IsTrue(packets.front() == expected, "packet must contain no stale bytes after reuse");
}
});
tc.Run("reset discards pending scalar vector flags and PATH1 work", [](TestCase &t)
{
Vu1Fixture fx;
t.IsTrue(fx.initialize(), "VU1 fixture should initialize");
unsigned packets = 0;
fx.mem.setGifPacketCallback([&](const uint8_t *, uint32_t) { ++packets; });
const uint64_t tag = makeGifTag(128u, GIF_FMT_IMAGE, 0u, true);
std::memcpy(fx.data, &tag, sizeof(tag));
writeTrackedVuInstructionPair(fx, 0u, makeVuLowerSpecial(0x6Cu, 0u), kVuUpperNop);
writeTrackedVuInstructionPair(fx, 8u, makeVuDiv(1u, 2u, 0u, 0u),
makeVuUpper(0x2Au, 0xFu, 1u, 1u, 3u));
writeTrackedVuInstructionPair(fx, 16u, makeVuLowerSpecial(0x7Cu, 1u), kVuUpperNop);
writeTrackedVuInstructionPair(fx, 24u, 0u, kVuUpperNop | (1u << 30));
writeTrackedVuInstructionPair(fx, 32u, 0u, kVuUpperNop);
VU1Interpreter vu;
vu.state().vf[1][0] = std::numeric_limits<float>::max();
vu.state().vf[2][0] = 2.0f;
vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE,
fx.gs, &fx.mem, 0u, 0u, 0u, 3u);
vu.reset();
// The runtime imports VU0 state after reset and before execute.
vu.state().clip = 0x123456u;
vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE,
fx.gs, &fx.mem, 24u);
t.Equals(vu.state().cycles, uint64_t(2u), "reset should discard old event deadlines");
t.Equals(vu.state().vf[3][0], 0.0f, "old vector result must not commit");
t.Equals(vu.state().q, 1.0f, "old Q result must not commit");
t.Equals(vu.state().p, 0.0f, "old P result must not commit");
t.Equals(vu.state().status, 0u, "old flags must not commit");
t.Equals(vu.state().clip, 0x123456u, "imported CLIP must survive execute");
t.Equals(packets, 0u, "abandoned PATH1 packet must not be submitted");
});
tc.Run("FMAC packs each flag plane for every destination mask", [](TestCase &t)
{
Vu1Fixture fx;
t.IsTrue(fx.initialize(), "VU1 fixture should initialize");
const float left[4] = {-std::numeric_limits<float>::max(), std::numeric_limits<float>::min(), 0.0f, 1.0f};
const float right[4] = {2.0f, 0.5f, -1.0f, 2.0f};
const uint32_t flags[4] = {0xAu, 0x5u, 0x3u, 0x0u};
const uint32_t resultBits[4] = {0xFF7FFFFFu, 0u, 0x80000000u, 0x40000000u};
for (uint8_t mask = 0; mask < 16; ++mask)
{
VU1Interpreter vu;
std::memcpy(vu.state().vf[1], left, sizeof(left));
std::memcpy(vu.state().vf[2], right, sizeof(right));
std::fill_n(vu.state().vf[3], 4, 7.0f);
writeTrackedVuInstructionPair(fx, 0u, 0u, makeVuUpper(0x2Au, mask, 2u, 1u, 3u) | (1u << 30));
writeTrackedVuInstructionPair(fx, 8u, 0u, kVuUpperNop);
vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem);
uint32_t expectedMac = 0, expectedStatus = 0;
for (uint32_t lane = 0; lane < 4; ++lane)
{
const uint32_t laneBit = 8u >> lane;
if ((mask & laneBit) != 0)
{
expectedStatus |= flags[lane];
for (uint32_t condition = 0; condition < 4; ++condition)
if ((flags[lane] & (1u << condition)) != 0)
expectedMac |= laneBit << (condition * 4u);
}
const uint32_t expectedBits = (mask & laneBit) ? resultBits[lane] : 0x40E00000u;
uint32_t actualBits;
std::memcpy(&actualBits, &vu.state().vf[3][lane], sizeof(actualBits));
t.Equals(actualBits, expectedBits, "only selected lanes receive the normalized result");
}
t.Equals(vu.state().mac, expectedMac, "Z/S/U/O occupy distinct MAC bit planes");
t.Equals(vu.state().status, expectedStatus | (expectedStatus << 6), "current and sticky conditions must agree");
}
});
tc.Run("code changed during a stall refreshes decoded lane dependencies on resume", [](TestCase &t)
{
for (bool tracked : {false, true})
{
Vu1Fixture fx;
t.IsTrue(fx.initialize(), "VU1 fixture should initialize");
const auto writePair = [&](uint32_t pc, uint32_t upper)
{
if (tracked)
writeTrackedVuInstructionPair(fx, pc, 0u, upper);
else
writeVuInstructionPair(fx.code, pc, 0u, upper);
};
PS2Memory *memory = tracked ? &fx.mem : nullptr;
writePair(0u, makeVuUpper(0x28u, 8u, 2u, 1u, 3u));
writePair(8u, makeVuUpper(0x28u, 8u, 2u, 3u, 4u));
writePair(16u, kVuUpperNop | (1u << 30));
writePair(24u, kVuUpperNop);
VU1Interpreter vu;
vu.state().vf[3][1] = 7.0f;
vu.state().vf[2][1] = 1.0f;
vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, memory, 0u, 0u, 0u, 2u);
t.Equals(vu.state().pc, 8u, "the decoded consumer must stall on the pending X producer");
writePair(8u, makeVuUpper(0x28u, 4u, 2u, 3u, 4u));
vu.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, memory, 0u, 0u, 1u);
t.Equals(vu.state().pc, 16u, "new Y dependency must not wait for the old X producer");
t.Equals(vu.state().cycles, uint64_t(3u), "resume must honor its cycle budget");
vu.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, memory);
t.Equals(vu.state().vf[4][1], 8.0f, "the replacement instruction must use the Y lanes");
}
});
tc.Run("reserved opcodes stop before executing or corrupting state", [](TestCase &t)
{
Vu1Fixture fx;