mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 16:59:35 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e27a658b32 |
@@ -62,6 +62,11 @@ if(PS2X_IOP_BUILD_TESTS)
|
|||||||
target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
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_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()
|
endif()
|
||||||
|
|
||||||
install(TARGETS ps2_iop
|
install(TARGETS ps2_iop
|
||||||
|
|||||||
@@ -53,9 +53,13 @@ namespace ps2x::iop::detail
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool IopCpuCore::executeInstruction(IopCpuState &cpu)
|
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 pc = cpu.pc;
|
||||||
const uint32_t instruction = m_memory.read32(pc);
|
|
||||||
const bool wasDelaySlot = cpu.branchPending;
|
const bool wasDelaySlot = cpu.branchPending;
|
||||||
const uint32_t priorBranchTarget = cpu.branchTarget;
|
const uint32_t priorBranchTarget = cpu.branchTarget;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace ps2x::iop::detail
|
|||||||
explicit IopCpuCore(IopMemory &memory) noexcept;
|
explicit IopCpuCore(IopMemory &memory) noexcept;
|
||||||
|
|
||||||
[[nodiscard]] bool executeInstruction(IopCpuState &cpu);
|
[[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;
|
void raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional<uint32_t> badAddress = std::nullopt) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -32,6 +32,20 @@ namespace ps2x::iop::detail
|
|||||||
m_nextSemaphoreId = 1;
|
m_nextSemaphoreId = 1;
|
||||||
m_nextEventFlagId = 1;
|
m_nextEventFlagId = 1;
|
||||||
m_currentThread = nullptr;
|
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)
|
bool IopKernel::dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
|
||||||
@@ -62,6 +76,7 @@ namespace ps2x::iop::detail
|
|||||||
}
|
}
|
||||||
const int id = thread.id;
|
const int id = thread.id;
|
||||||
m_threads.emplace(id, std::move(thread));
|
m_threads.emplace(id, std::move(thread));
|
||||||
|
m_scheduleDirty = true;
|
||||||
setV0(id);
|
setV0(id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -74,6 +89,14 @@ namespace ps2x::iop::detail
|
|||||||
setV0(-1);
|
setV0(-1);
|
||||||
return true;
|
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)
|
if (it->second.stackBase != 0u)
|
||||||
(void)m_memory.freeAllocation(it->second.stackBase);
|
(void)m_memory.freeAllocation(it->second.stackBase);
|
||||||
m_threads.erase(it);
|
m_threads.erase(it);
|
||||||
@@ -98,7 +121,7 @@ namespace ps2x::iop::detail
|
|||||||
thread.cpu.gpr[28] = cpu.gpr[28];
|
thread.cpu.gpr[28] = cpu.gpr[28];
|
||||||
thread.cpu.gpr[29] = alignUp(thread.stackBase + thread.stackSize, 16u) - 16u;
|
thread.cpu.gpr[29] = alignUp(thread.stackBase + thread.stackSize, 16u) - 16u;
|
||||||
thread.cpu.gpr[31] = kThreadReturnSentinel;
|
thread.cpu.gpr[31] = kThreadReturnSentinel;
|
||||||
thread.state = IopThreadState::Ready;
|
setThreadState(thread, IopThreadState::Ready);
|
||||||
setV0(0);
|
setV0(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -106,7 +129,7 @@ namespace ps2x::iop::detail
|
|||||||
case 9: // ExitDeleteThread
|
case 9: // ExitDeleteThread
|
||||||
if (m_currentThread != nullptr)
|
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.stopped = true;
|
||||||
cpu.yielded = true;
|
cpu.yielded = true;
|
||||||
}
|
}
|
||||||
@@ -122,7 +145,7 @@ namespace ps2x::iop::detail
|
|||||||
setV0(-1);
|
setV0(-1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
it->second.state = IopThreadState::Dormant;
|
setThreadState(it->second, IopThreadState::Dormant);
|
||||||
setV0(0);
|
setV0(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -143,6 +166,7 @@ namespace ps2x::iop::detail
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
it->second.priority = std::clamp<uint32_t>(cpu.gpr[5], 1u, 126u);
|
it->second.priority = std::clamp<uint32_t>(cpu.gpr[5], 1u, 126u);
|
||||||
|
m_scheduleDirty = true;
|
||||||
setV0(0);
|
setV0(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -166,7 +190,7 @@ namespace ps2x::iop::detail
|
|||||||
it->second.state == IopThreadState::Semaphore ||
|
it->second.state == IopThreadState::Semaphore ||
|
||||||
it->second.state == IopThreadState::EventFlag)
|
it->second.state == IopThreadState::EventFlag)
|
||||||
{
|
{
|
||||||
it->second.state = IopThreadState::Ready;
|
setThreadState(it->second, IopThreadState::Ready);
|
||||||
}
|
}
|
||||||
setV0(0);
|
setV0(0);
|
||||||
return true;
|
return true;
|
||||||
@@ -202,7 +226,7 @@ namespace ps2x::iop::detail
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (it->second.state == IopThreadState::Sleep)
|
if (it->second.state == IopThreadState::Sleep)
|
||||||
it->second.state = IopThreadState::Ready;
|
setThreadState(it->second, IopThreadState::Ready);
|
||||||
else
|
else
|
||||||
++it->second.wakeupCount;
|
++it->second.wakeupCount;
|
||||||
setV0(0);
|
setV0(0);
|
||||||
@@ -233,7 +257,7 @@ namespace ps2x::iop::detail
|
|||||||
setV0(-1);
|
setV0(-1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
it->second.state = IopThreadState::Suspended;
|
setThreadState(it->second, IopThreadState::Suspended);
|
||||||
if (m_currentThread == &it->second)
|
if (m_currentThread == &it->second)
|
||||||
cpu.yielded = true;
|
cpu.yielded = true;
|
||||||
setV0(0);
|
setV0(0);
|
||||||
@@ -250,7 +274,7 @@ namespace ps2x::iop::detail
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (it->second.state == IopThreadState::Suspended)
|
if (it->second.state == IopThreadState::Suspended)
|
||||||
it->second.state = IopThreadState::Ready;
|
setThreadState(it->second, IopThreadState::Ready);
|
||||||
setV0(0);
|
setV0(0);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -410,7 +434,7 @@ namespace ps2x::iop::detail
|
|||||||
setV0(-419);
|
setV0(-419);
|
||||||
else if (m_currentThread != nullptr)
|
else if (m_currentThread != nullptr)
|
||||||
{
|
{
|
||||||
m_currentThread->state = IopThreadState::Semaphore;
|
setThreadState(*m_currentThread, IopThreadState::Semaphore);
|
||||||
m_currentThread->waitId = id;
|
m_currentThread->waitId = id;
|
||||||
cpu.yielded = true;
|
cpu.yielded = true;
|
||||||
setV0(0);
|
setV0(0);
|
||||||
@@ -466,7 +490,7 @@ namespace ps2x::iop::detail
|
|||||||
if (best != nullptr && semaphore != m_semaphores.end() && semaphore->second.current > 0)
|
if (best != nullptr && semaphore != m_semaphores.end() && semaphore->second.current > 0)
|
||||||
{
|
{
|
||||||
--semaphore->second.current;
|
--semaphore->second.current;
|
||||||
best->state = IopThreadState::Ready;
|
setThreadState(*best, IopThreadState::Ready);
|
||||||
best->waitId = 0;
|
best->waitId = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,7 +516,7 @@ namespace ps2x::iop::detail
|
|||||||
thread.cpu.gpr[2] = 0u;
|
thread.cpu.gpr[2] = 0u;
|
||||||
if ((thread.waitMode & 0x10u) != 0u)
|
if ((thread.waitMode & 0x10u) != 0u)
|
||||||
event.bits = 0u;
|
event.bits = 0u;
|
||||||
thread.state = IopThreadState::Ready;
|
setThreadState(thread, IopThreadState::Ready);
|
||||||
thread.waitId = 0;
|
thread.waitId = 0;
|
||||||
thread.waitBits = 0;
|
thread.waitBits = 0;
|
||||||
thread.waitMode = 0;
|
thread.waitMode = 0;
|
||||||
@@ -553,7 +577,7 @@ namespace ps2x::iop::detail
|
|||||||
{
|
{
|
||||||
if (thread.state == IopThreadState::EventFlag && thread.waitId == id)
|
if (thread.state == IopThreadState::EventFlag && thread.waitId == id)
|
||||||
{
|
{
|
||||||
thread.state = IopThreadState::Ready;
|
setThreadState(thread, IopThreadState::Ready);
|
||||||
thread.waitId = 0;
|
thread.waitId = 0;
|
||||||
thread.cpu.gpr[2] = static_cast<uint32_t>(-1);
|
thread.cpu.gpr[2] = static_cast<uint32_t>(-1);
|
||||||
}
|
}
|
||||||
@@ -609,7 +633,7 @@ namespace ps2x::iop::detail
|
|||||||
setV0(-418);
|
setV0(-418);
|
||||||
else if (m_currentThread != nullptr)
|
else if (m_currentThread != nullptr)
|
||||||
{
|
{
|
||||||
m_currentThread->state = IopThreadState::EventFlag;
|
setThreadState(*m_currentThread, IopThreadState::EventFlag);
|
||||||
m_currentThread->waitId = event->second.id;
|
m_currentThread->waitId = event->second.id;
|
||||||
m_currentThread->waitBits = bits;
|
m_currentThread->waitBits = bits;
|
||||||
m_currentThread->waitMode = mode;
|
m_currentThread->waitMode = mode;
|
||||||
@@ -656,7 +680,7 @@ namespace ps2x::iop::detail
|
|||||||
{
|
{
|
||||||
if (m_currentThread == nullptr)
|
if (m_currentThread == nullptr)
|
||||||
return;
|
return;
|
||||||
m_currentThread->state = IopThreadState::Sleep;
|
setThreadState(*m_currentThread, IopThreadState::Sleep);
|
||||||
cpu.yielded = true;
|
cpu.yielded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,27 +689,33 @@ namespace ps2x::iop::detail
|
|||||||
if (m_currentThread == nullptr)
|
if (m_currentThread == nullptr)
|
||||||
return;
|
return;
|
||||||
m_currentThread->wakeCycle = wakeCycle;
|
m_currentThread->wakeCycle = wakeCycle;
|
||||||
m_currentThread->state = IopThreadState::Delay;
|
setThreadState(*m_currentThread, IopThreadState::Delay);
|
||||||
|
m_scheduleDirty = true;
|
||||||
cpu.yielded = true;
|
cpu.yielded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
IopThread *IopKernel::beginNextReady(uint64_t currentCycle)
|
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)
|
m_nextReady = nullptr;
|
||||||
thread.state = IopThreadState::Ready;
|
m_nextWakeCycle = UINT64_MAX;
|
||||||
}
|
for (auto &[id, thread] : m_threads)
|
||||||
|
{
|
||||||
IopThread *next = nullptr;
|
if (thread.state == IopThreadState::Delay)
|
||||||
for (auto &[id, thread] : m_threads)
|
{
|
||||||
{
|
if (thread.wakeCycle <= currentCycle)
|
||||||
if (thread.state != IopThreadState::Ready)
|
thread.state = IopThreadState::Ready;
|
||||||
continue;
|
else
|
||||||
if (next == nullptr || thread.priority < next->priority ||
|
m_nextWakeCycle = std::min(m_nextWakeCycle, thread.wakeCycle);
|
||||||
(thread.priority == next->priority && thread.id < next->id))
|
}
|
||||||
next = &thread;
|
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)
|
if (next == nullptr)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
@@ -698,6 +728,8 @@ namespace ps2x::iop::detail
|
|||||||
|
|
||||||
uint64_t IopKernel::nextWakeCycle(uint64_t fallback) const
|
uint64_t IopKernel::nextWakeCycle(uint64_t fallback) const
|
||||||
{
|
{
|
||||||
|
if (!m_scheduleDirty)
|
||||||
|
return std::min(fallback, m_nextWakeCycle);
|
||||||
uint64_t nextWake = fallback;
|
uint64_t nextWake = fallback;
|
||||||
for (const auto &[id, thread] : m_threads)
|
for (const auto &[id, thread] : m_threads)
|
||||||
{
|
{
|
||||||
@@ -709,8 +741,8 @@ namespace ps2x::iop::detail
|
|||||||
|
|
||||||
void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel)
|
void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel)
|
||||||
{
|
{
|
||||||
if (thread.cpu.pc == returnSentinel || thread.cpu.stopped)
|
if (thread.state != IopThreadState::Dead && (thread.cpu.pc == returnSentinel || thread.cpu.stopped))
|
||||||
thread.state = IopThreadState::Dormant;
|
setThreadState(thread, IopThreadState::Dormant);
|
||||||
else if (thread.state == IopThreadState::Running)
|
else if (thread.state == IopThreadState::Running)
|
||||||
thread.state = IopThreadState::Ready;
|
thread.state = IopThreadState::Ready;
|
||||||
m_currentThread = nullptr;
|
m_currentThread = nullptr;
|
||||||
@@ -719,6 +751,9 @@ namespace ps2x::iop::detail
|
|||||||
|
|
||||||
void IopKernel::cleanupDeadThreads()
|
void IopKernel::cleanupDeadThreads()
|
||||||
{
|
{
|
||||||
|
if (!m_hasDeadThreads)
|
||||||
|
return;
|
||||||
|
m_hasDeadThreads = false;
|
||||||
for (auto thread = m_threads.begin(); thread != m_threads.end();)
|
for (auto thread = m_threads.begin(); thread != m_threads.end();)
|
||||||
{
|
{
|
||||||
if (thread->second.state != IopThreadState::Dead)
|
if (thread->second.state != IopThreadState::Dead)
|
||||||
@@ -726,6 +761,14 @@ namespace ps2x::iop::detail
|
|||||||
++thread;
|
++thread;
|
||||||
continue;
|
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)
|
if (thread->second.stackBase != 0u)
|
||||||
(void)m_memory.freeAllocation(thread->second.stackBase);
|
(void)m_memory.freeAllocation(thread->second.stackBase);
|
||||||
thread = m_threads.erase(thread);
|
thread = m_threads.erase(thread);
|
||||||
@@ -738,7 +781,7 @@ namespace ps2x::iop::detail
|
|||||||
{
|
{
|
||||||
const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc);
|
const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc);
|
||||||
if (pc >= base && pc < base + size)
|
if (pc >= base && pc < base + size)
|
||||||
thread.state = IopThreadState::Dead;
|
setThreadState(thread, IopThreadState::Dead);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace ps2x::iop::detail
|
namespace ps2x::iop::detail
|
||||||
{
|
{
|
||||||
@@ -90,14 +91,19 @@ namespace ps2x::iop::detail
|
|||||||
void wakeOneSemaphore(int id);
|
void wakeOneSemaphore(int id);
|
||||||
[[nodiscard]] static bool eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode);
|
[[nodiscard]] static bool eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode);
|
||||||
void wakeEventWaiters(EventFlag &event);
|
void wakeEventWaiters(EventFlag &event);
|
||||||
|
void setThreadState(IopThread &thread, IopThreadState state);
|
||||||
|
|
||||||
IopMemory &m_memory;
|
IopMemory &m_memory;
|
||||||
std::map<int, IopThread> m_threads;
|
std::map<int, IopThread> m_threads;
|
||||||
std::map<int, Semaphore> m_semaphores;
|
std::unordered_map<int, Semaphore> m_semaphores;
|
||||||
std::map<int, EventFlag> m_eventFlags;
|
std::unordered_map<int, EventFlag> m_eventFlags;
|
||||||
uint32_t m_nextThreadId = 1;
|
uint32_t m_nextThreadId = 1;
|
||||||
uint32_t m_nextSemaphoreId = 1;
|
uint32_t m_nextSemaphoreId = 1;
|
||||||
uint32_t m_nextEventFlagId = 1;
|
uint32_t m_nextEventFlagId = 1;
|
||||||
IopThread *m_currentThread = nullptr;
|
IopThread *m_currentThread = nullptr;
|
||||||
|
IopThread *m_nextReady = nullptr;
|
||||||
|
uint64_t m_nextWakeCycle = UINT64_MAX;
|
||||||
|
bool m_scheduleDirty = true;
|
||||||
|
bool m_hasDeadThreads = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ namespace ps2x::iop::detail
|
|||||||
void setInterruptMask(uint32_t value) noexcept { m_interruptMask = value; }
|
void setInterruptMask(uint32_t value) noexcept { m_interruptMask = value; }
|
||||||
void setInterruptControl(uint32_t value) noexcept { m_interruptControl = value & 1u; }
|
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::optional<DmaStart> takeDmaStart() noexcept;
|
||||||
[[nodiscard]] std::span<const uint8_t> ram() const noexcept { return m_ram; }
|
[[nodiscard]] std::span<const uint8_t> ram() const noexcept { return m_ram; }
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,11 @@ namespace ps2x::iop::detail
|
|||||||
|
|
||||||
std::optional<IopImportCall> IopImportRegistry::decode(uint32_t pc) const
|
std::optional<IopImportCall> IopImportRegistry::decode(uint32_t pc) const
|
||||||
{
|
{
|
||||||
if (m_memory.read32(pc) != 0x03E00008u)
|
return decode(pc, m_memory.read32(pc));
|
||||||
return std::nullopt;
|
}
|
||||||
|
|
||||||
|
std::optional<IopImportCall> IopImportRegistry::decodeStub(uint32_t pc) const
|
||||||
|
{
|
||||||
const uint32_t delay = m_memory.read32(pc + 4u);
|
const uint32_t delay = m_memory.read32(pc + 4u);
|
||||||
if ((delay & 0xFFFF0000u) != 0x24000000u)
|
if ((delay & 0xFFFF0000u) != 0x24000000u)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ namespace ps2x::iop::detail
|
|||||||
|
|
||||||
void reset();
|
void reset();
|
||||||
[[nodiscard]] std::optional<IopImportCall> decode(uint32_t pc) const;
|
[[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 registerExportTable(uint32_t address);
|
||||||
[[nodiscard]] bool releaseExportTable(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;
|
[[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);
|
void eraseRange(uint32_t base, uint32_t size);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
[[nodiscard]] std::optional<IopImportCall> decodeStub(uint32_t pc) const;
|
||||||
struct ExportLibrary
|
struct ExportLibrary
|
||||||
{
|
{
|
||||||
uint32_t tableAddress = 0;
|
uint32_t tableAddress = 0;
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ namespace ps2x::iop::detail
|
|||||||
timrman.reset();
|
timrman.reset();
|
||||||
ioman.reset();
|
ioman.reset();
|
||||||
pendingDmaInterrupts.clear();
|
pendingDmaInterrupts.clear();
|
||||||
|
nextDmaInterruptCycle = UINT64_MAX;
|
||||||
pendingGuestCallbacks.clear();
|
pendingGuestCallbacks.clear();
|
||||||
nextModuleId = 1;
|
nextModuleId = 1;
|
||||||
moduleCursor = kModuleLoadBase;
|
moduleCursor = kModuleLoadBase;
|
||||||
@@ -177,7 +178,12 @@ namespace ps2x::iop::detail
|
|||||||
void schedulePendingDma()
|
void schedulePendingDma()
|
||||||
{
|
{
|
||||||
if (const auto dma = memory.takeDmaStart())
|
if (const auto dma = memory.takeDmaStart())
|
||||||
|
{
|
||||||
pendingDmaInterrupts[dma->irq] = totalCycles + dma->delayCycles;
|
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
|
bool readRam(uint32_t address, void *destination, size_t size) const
|
||||||
@@ -378,7 +384,8 @@ namespace ps2x::iop::detail
|
|||||||
if (checkInterrupt(cpu))
|
if (checkInterrupt(cpu))
|
||||||
return true;
|
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);
|
const ImportDisposition disposition = dispatchImport(*import, cpu);
|
||||||
++totalInstructions;
|
++totalInstructions;
|
||||||
@@ -390,8 +397,9 @@ namespace ps2x::iop::detail
|
|||||||
return !cpu.stopped;
|
return !cpu.stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool running = cpuCore.executeInstruction(cpu);
|
const bool running = cpuCore.executeInstruction(cpu, instruction);
|
||||||
schedulePendingDma();
|
if (memory.hasDmaStart())
|
||||||
|
schedulePendingDma();
|
||||||
++totalInstructions;
|
++totalInstructions;
|
||||||
++totalCycles;
|
++totalCycles;
|
||||||
return running;
|
return running;
|
||||||
@@ -406,7 +414,7 @@ namespace ps2x::iop::detail
|
|||||||
{
|
{
|
||||||
if (!step(cpu))
|
if (!step(cpu))
|
||||||
break;
|
break;
|
||||||
if (!servicingDmaInterrupts && !pendingDmaInterrupts.empty())
|
if (!servicingDmaInterrupts && totalCycles >= nextDmaInterruptCycle)
|
||||||
servicePendingDmaInterrupts();
|
servicePendingDmaInterrupts();
|
||||||
if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty())
|
if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty())
|
||||||
servicePendingGuestCallbacks();
|
servicePendingGuestCallbacks();
|
||||||
@@ -483,16 +491,18 @@ namespace ps2x::iop::detail
|
|||||||
// Not that good to use exception handling for control flow but will do for now
|
// Not that good to use exception handling for control flow but will do for now
|
||||||
void servicePendingDmaInterrupts()
|
void servicePendingDmaInterrupts()
|
||||||
{
|
{
|
||||||
if (servicingDmaInterrupts || pendingDmaInterrupts.empty())
|
if (servicingDmaInterrupts || totalCycles < nextDmaInterruptCycle)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
servicingDmaInterrupts = true;
|
servicingDmaInterrupts = true;
|
||||||
|
|
||||||
std::vector<int> completed;
|
std::vector<int> completed;
|
||||||
|
nextDmaInterruptCycle = UINT64_MAX;
|
||||||
for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();)
|
for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();)
|
||||||
{
|
{
|
||||||
if (it->second > totalCycles)
|
if (it->second > totalCycles)
|
||||||
{
|
{
|
||||||
|
nextDmaInterruptCycle = std::min(nextDmaInterruptCycle, it->second);
|
||||||
++it;
|
++it;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -567,8 +577,7 @@ namespace ps2x::iop::detail
|
|||||||
if (!next)
|
if (!next)
|
||||||
{
|
{
|
||||||
uint64_t nextWake = kernel.nextWakeCycle(target);
|
uint64_t nextWake = kernel.nextWakeCycle(target);
|
||||||
for (const auto &[irq, completionCycle] : pendingDmaInterrupts)
|
nextWake = std::min(nextWake, nextDmaInterruptCycle);
|
||||||
nextWake = std::min(nextWake, completionCycle);
|
|
||||||
if (!pendingGuestCallbacks.empty())
|
if (!pendingGuestCallbacks.empty())
|
||||||
nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first);
|
nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first);
|
||||||
nextWake = timrman.nextEventCycle(nextWake);
|
nextWake = timrman.nextEventCycle(nextWake);
|
||||||
@@ -696,6 +705,7 @@ namespace ps2x::iop::detail
|
|||||||
IopLoadcore loadcore;
|
IopLoadcore loadcore;
|
||||||
std::map<int, Module> modules;
|
std::map<int, Module> modules;
|
||||||
std::map<int, uint64_t> pendingDmaInterrupts;
|
std::map<int, uint64_t> pendingDmaInterrupts;
|
||||||
|
uint64_t nextDmaInterruptCycle = UINT64_MAX;
|
||||||
std::multimap<uint64_t, ScheduledGuestCallback> pendingGuestCallbacks;
|
std::multimap<uint64_t, ScheduledGuestCallback> pendingGuestCallbacks;
|
||||||
uint32_t nextModuleId = 1;
|
uint32_t nextModuleId = 1;
|
||||||
uint32_t moduleCursor = kModuleLoadBase;
|
uint32_t moduleCursor = kModuleLoadBase;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -274,6 +274,128 @@ namespace
|
|||||||
return passed;
|
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()
|
bool testTimrmanPeriodicCallback()
|
||||||
{
|
{
|
||||||
IopTimrman timrman;
|
IopTimrman timrman;
|
||||||
@@ -331,7 +453,7 @@ namespace
|
|||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() ||
|
if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() ||
|
||||||
!testTimrmanPeriodicCallback())
|
!testTimrmanPeriodicCallback() || !testKernelSchedulingTransitions())
|
||||||
return 1;
|
return 1;
|
||||||
std::cout << "ps2xIOP import tests passed\n";
|
std::cout << "ps2xIOP import tests passed\n";
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -5,13 +5,23 @@ project(PS2Runtime VERSION 0.1.0 LANGUAGES CXX)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
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)
|
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")
|
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_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_SCCACHE "Use sccache as compiler launcher when available" ON)
|
||||||
|
|
||||||
option(PS2X_ENABLE_RUNTIME_LOGS "Enable 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" ON)
|
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_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_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)
|
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
|
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)
|
if(PS2X_ENABLE_RUNTIME_LOGS)
|
||||||
target_compile_definitions(ps2_runtime PUBLIC
|
target_compile_definitions(ps2_runtime PUBLIC
|
||||||
PS2_RUNTIME_LOGS=1
|
PS2_RUNTIME_LOGS=1
|
||||||
|
|||||||
@@ -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}")
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
#ifndef PS2_DEBUG_PANEL_H
|
#ifndef PS2_DEBUG_PANEL_H
|
||||||
#define PS2_DEBUG_PANEL_H
|
#define PS2_DEBUG_PANEL_H
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
class PS2Runtime;
|
class PS2Runtime;
|
||||||
|
|
||||||
class PS2DebugPanel
|
class PS2DebugPanel
|
||||||
@@ -20,6 +24,12 @@ private:
|
|||||||
bool m_showRegisters = true;
|
bool m_showRegisters = true;
|
||||||
unsigned int m_memoryAddress = 0x00100000u;
|
unsigned int m_memoryAddress = 0x00100000u;
|
||||||
unsigned int m_memoryBytes = 0x100u;
|
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
|
#endif // PS2_DEBUG_PANEL_H
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
ps2_log::append_runtime_log_text(_ps2_runtime_error_text); \
|
ps2_log::append_runtime_log_text(_ps2_runtime_error_text); \
|
||||||
} while (0)
|
} while (0)
|
||||||
|
|
||||||
|
#define RUNTIME_WARNING(x) std::cout << x
|
||||||
|
|
||||||
namespace ps2_log
|
namespace ps2_log
|
||||||
{
|
{
|
||||||
struct RuntimeLogEntry
|
struct RuntimeLogEntry
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
struct GSDebugSnapshot;
|
||||||
|
|
||||||
class GSRasterBackend
|
class GSRasterBackend
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -12,6 +14,12 @@ public:
|
|||||||
|
|
||||||
virtual void Initialize(uint8_t *vram, uint32_t vramSize) = 0;
|
virtual void Initialize(uint8_t *vram, uint32_t vramSize) = 0;
|
||||||
virtual void Reset() = 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 Submit(const GSPrimitiveBatch &batch) = 0;
|
||||||
virtual void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) = 0;
|
virtual void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) = 0;
|
||||||
|
|||||||
@@ -105,8 +105,10 @@ public:
|
|||||||
void init(uint8_t *vram, uint32_t vramSize, struct GSRegisters *privRegs = nullptr);
|
void init(uint8_t *vram, uint32_t vramSize, struct GSRegisters *privRegs = nullptr);
|
||||||
void reset();
|
void reset();
|
||||||
void setRasterBackend(std::unique_ptr<GSRasterBackend> backend);
|
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);
|
bool processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes);
|
||||||
void uploadImageNative(uint64_t bitbltbuf,
|
void uploadImageNative(uint64_t bitbltbuf,
|
||||||
uint64_t trxpos,
|
uint64_t trxpos,
|
||||||
@@ -119,9 +121,9 @@ public:
|
|||||||
const uint8_t *lockDisplaySnapshot(uint32_t &outSize);
|
const uint8_t *lockDisplaySnapshot(uint32_t &outSize);
|
||||||
void unlockDisplaySnapshot();
|
void unlockDisplaySnapshot();
|
||||||
uint32_t getLastDisplayBaseBytes() const;
|
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;
|
GSDebugSnapshot getDebugSnapshot() const;
|
||||||
std::vector<GSDebugHistoryEntry> getDebugHistory() const;
|
std::vector<GSDebugHistoryEntry> getDebugHistory() const;
|
||||||
@@ -130,6 +132,7 @@ public:
|
|||||||
void setDebugHistoryPaused(bool paused);
|
void setDebugHistoryPaused(bool paused);
|
||||||
bool getPreferredDisplaySource(GSFrameReg &outSource, uint32_t &outDestFbp) const;
|
bool getPreferredDisplaySource(GSFrameReg &outSource, uint32_t &outDestFbp) const;
|
||||||
void latchHostPresentationFrame();
|
void latchHostPresentationFrame();
|
||||||
|
std::shared_ptr<GSGpuFrame> getLatchedGpuFrame(uint32_t &width, uint32_t &height, float &aspectRatio) const;
|
||||||
bool copyLatchedHostPresentationFrame(std::vector<uint8_t> &outPixels,
|
bool copyLatchedHostPresentationFrame(std::vector<uint8_t> &outPixels,
|
||||||
uint32_t &outWidth,
|
uint32_t &outWidth,
|
||||||
uint32_t &outHeight,
|
uint32_t &outHeight,
|
||||||
@@ -223,6 +226,8 @@ private:
|
|||||||
uint32_t m_preferredDisplayDestFbp = 0;
|
uint32_t m_preferredDisplayDestFbp = 0;
|
||||||
bool m_hasPreferredDisplaySource = false;
|
bool m_hasPreferredDisplaySource = false;
|
||||||
std::vector<uint8_t> m_hostPresentationFrame;
|
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_hostPresentationWidth = 0;
|
||||||
uint32_t m_hostPresentationHeight = 0;
|
uint32_t m_hostPresentationHeight = 0;
|
||||||
uint32_t m_hostPresentationDisplayFbp = 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 ®isters);
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
enum GSPrimType : uint8_t
|
enum GSPrimType : uint8_t
|
||||||
{
|
{
|
||||||
@@ -277,6 +278,8 @@ struct GSTransferSnapshot
|
|||||||
struct GSPresentationRequest
|
struct GSPresentationRequest
|
||||||
{
|
{
|
||||||
uint64_t pmode = 0;
|
uint64_t pmode = 0;
|
||||||
|
uint64_t smode1 = 0;
|
||||||
|
uint64_t syncv = 0;
|
||||||
uint64_t smode2 = 0;
|
uint64_t smode2 = 0;
|
||||||
uint64_t dispfb1 = 0;
|
uint64_t dispfb1 = 0;
|
||||||
uint64_t display1 = 0;
|
uint64_t display1 = 0;
|
||||||
@@ -284,24 +287,35 @@ struct GSPresentationRequest
|
|||||||
uint64_t display2 = 0;
|
uint64_t display2 = 0;
|
||||||
uint64_t bgcolor = 0;
|
uint64_t bgcolor = 0;
|
||||||
uint64_t vsyncTick = 0;
|
uint64_t vsyncTick = 0;
|
||||||
|
uint32_t field = 0;
|
||||||
GSFrameReg contextFrames[2]{};
|
GSFrameReg contextFrames[2]{};
|
||||||
GSFrameReg preferredSource{};
|
GSFrameReg preferredSource{};
|
||||||
uint32_t preferredDestFbp = 0;
|
uint32_t preferredDestFbp = 0;
|
||||||
bool hasPreferredSource = false;
|
bool hasPreferredSource = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class GSGpuFrame
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~GSGpuFrame() = default;
|
||||||
|
virtual uint32_t AcquireTexture() = 0;
|
||||||
|
virtual void ReleaseTexture() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
struct PresentationFrame
|
struct PresentationFrame
|
||||||
{
|
{
|
||||||
std::vector<uint8_t> pixels;
|
std::vector<uint8_t> pixels;
|
||||||
|
std::shared_ptr<GSGpuFrame> gpu;
|
||||||
uint32_t width = 0;
|
uint32_t width = 0;
|
||||||
uint32_t height = 0;
|
uint32_t height = 0;
|
||||||
uint32_t displayFbp = 0;
|
uint32_t displayFbp = 0;
|
||||||
uint32_t sourceFbp = 0;
|
uint32_t sourceFbp = 0;
|
||||||
bool usedPreferred = false;
|
bool usedPreferred = false;
|
||||||
|
float aspectRatio = 0.0f;
|
||||||
|
|
||||||
explicit operator bool() const
|
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
|
class GifArbiter
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using ProcessPacketFn = std::function<void(const uint8_t *, uint32_t)>;
|
using ProcessPacketFn = std::function<void(GifPathId, const uint8_t *, uint32_t)>;
|
||||||
|
|
||||||
GifArbiter() = default;
|
GifArbiter() = default;
|
||||||
explicit GifArbiter(ProcessPacketFn processFn);
|
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); }
|
void setProcessPacketFn(ProcessPacketFn fn) { m_processFn = std::move(fn); }
|
||||||
|
|
||||||
|
|||||||
@@ -210,8 +210,23 @@ struct GSRegisters
|
|||||||
uint64_t imr; // Interrupt mask
|
uint64_t imr; // Interrupt mask
|
||||||
uint64_t busdir; // Bus direction
|
uint64_t busdir; // Bus direction
|
||||||
uint64_t siglblid; // Signal label ID
|
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 ® = 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(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");
|
static_assert(std::atomic<uint64_t>::is_always_lock_free, "GS CSR atomic must be lock-free on all supported targets");
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
#define PS2_VU1_H
|
#define PS2_VU1_H
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <bit>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <limits>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
class GS;
|
class GS;
|
||||||
@@ -102,6 +105,11 @@ private:
|
|||||||
bool reserved = false;
|
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
|
struct DecodedInstructionPair
|
||||||
{
|
{
|
||||||
uint32_t lower = 0;
|
uint32_t lower = 0;
|
||||||
@@ -113,8 +121,9 @@ private:
|
|||||||
bool mBit = false;
|
bool mBit = false;
|
||||||
bool dBit = false;
|
bool dBit = false;
|
||||||
bool tBit = false;
|
bool tBit = false;
|
||||||
uint8_t upperVfShadowReg = 0;
|
|
||||||
uint8_t suppressedLowerVf = 0;
|
uint8_t suppressedLowerVf = 0;
|
||||||
|
std::array<uint8_t, 4u * 4u + 15u + 4u> readDependencies{};
|
||||||
|
uint8_t readDependencyCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FlagPipelineEntry
|
struct FlagPipelineEntry
|
||||||
@@ -189,6 +198,13 @@ private:
|
|||||||
uint64_t issueCycle = 0;
|
uint64_t issueCycle = 0;
|
||||||
bool active = false;
|
bool active = false;
|
||||||
bool currentTagEop = false;
|
bool currentTagEop = false;
|
||||||
|
|
||||||
|
void reset()
|
||||||
|
{
|
||||||
|
sourceAddress = totalBytes = copiedBytes = currentTagEnd = cycleCredit = 0;
|
||||||
|
issueCycle = 0;
|
||||||
|
active = currentTagEop = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr uint32_t kFmacLatency = 4u;
|
static constexpr uint32_t kFmacLatency = 4u;
|
||||||
@@ -203,6 +219,8 @@ private:
|
|||||||
Unit m_unit;
|
Unit m_unit;
|
||||||
VU1State m_state;
|
VU1State m_state;
|
||||||
std::array<DecodedInstructionPair, kMaxDecodedPairs> m_decodedCodeCache{};
|
std::array<DecodedInstructionPair, kMaxDecodedPairs> m_decodedCodeCache{};
|
||||||
|
std::array<uint64_t, kMaxDecodedPairs / 64u> m_decodedPairValid{};
|
||||||
|
DecodedInstructionPair m_uncachedDecoded{};
|
||||||
const uint8_t *m_cachedVuCode = nullptr;
|
const uint8_t *m_cachedVuCode = nullptr;
|
||||||
const PS2Memory *m_cachedMemory = nullptr;
|
const PS2Memory *m_cachedMemory = nullptr;
|
||||||
uint32_t m_cachedCodeSize = 0;
|
uint32_t m_cachedCodeSize = 0;
|
||||||
@@ -216,10 +234,21 @@ private:
|
|||||||
std::array<PendingVfWrite, kMaxPendingVfWrites> m_vfWritePipeline{};
|
std::array<PendingVfWrite, kMaxPendingVfWrites> m_vfWritePipeline{};
|
||||||
std::array<PendingViWrite, kMaxPendingViWrites> m_viWritePipeline{};
|
std::array<PendingViWrite, kMaxPendingViWrites> m_viWritePipeline{};
|
||||||
std::array<PendingAccWrite, kMaxPendingAccWrites> m_accWritePipeline{};
|
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{};
|
XgkickPipeline m_xgkick{};
|
||||||
std::array<std::array<uint64_t, 4>, 32> m_vfReady{};
|
|
||||||
std::array<uint64_t, 16> m_viReady{};
|
std::array<uint64_t, kRegisterReadyCount> m_registerReady{};
|
||||||
std::array<uint64_t, 4> m_accReady{};
|
|
||||||
std::array<std::array<uint64_t, 4>, 32> m_vfLatestWrite{};
|
std::array<std::array<uint64_t, 4>, 32> m_vfLatestWrite{};
|
||||||
std::array<uint64_t, 16> m_viLatestWrite{};
|
std::array<uint64_t, 16> m_viLatestWrite{};
|
||||||
std::array<uint64_t, 4> m_accLatestWrite{};
|
std::array<uint64_t, 4> m_accLatestWrite{};
|
||||||
@@ -229,6 +258,10 @@ private:
|
|||||||
uint64_t m_efuResourceReady = 0;
|
uint64_t m_efuResourceReady = 0;
|
||||||
uint32_t m_workingClip = 0;
|
uint32_t m_workingClip = 0;
|
||||||
uint32_t m_currentUpperInstruction = 0;
|
uint32_t m_currentUpperInstruction = 0;
|
||||||
|
struct UpperOperands
|
||||||
|
{
|
||||||
|
float vs[4], vt[4], acc[4], q, i;
|
||||||
|
} m_upperOperands{};
|
||||||
int32_t m_viBranchBackupValue = 0;
|
int32_t m_viBranchBackupValue = 0;
|
||||||
uint8_t m_viBranchBackupReg = 0;
|
uint8_t m_viBranchBackupReg = 0;
|
||||||
bool m_viBranchBackupValid = false;
|
bool m_viBranchBackupValid = false;
|
||||||
@@ -248,18 +281,15 @@ private:
|
|||||||
InstructionUsage decodeLowerUsage(uint32_t lower) const;
|
InstructionUsage decodeLowerUsage(uint32_t lower) const;
|
||||||
static void addVfRead(InstructionUsage &usage, uint8_t reg, uint8_t lanes);
|
static void addVfRead(InstructionUsage &usage, uint8_t reg, uint8_t lanes);
|
||||||
static void addVfWrite(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 decodeInstructionPair(const uint8_t *vuCode, uint32_t pc) const;
|
||||||
DecodedInstructionPair getDecodedInstructionPairForPc(const uint8_t *vuCode, uint32_t codeSize, PS2Memory *memory, 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);
|
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 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 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 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]);
|
void normalizeFmacResult(float *result, uint8_t dest, uint8_t laneFlags[4]);
|
||||||
bool calculateFmacExactResult(uint32_t component, long double &result) const;
|
bool calculateFmacExactResult(uint32_t component, long double &result) const;
|
||||||
uint8_t normalizeFmacExactResult(float &value, long double exactResult) 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 queueAccWrite(uint8_t laneMask, const float value[4], uint32_t latency);
|
||||||
void startXgkick(uint32_t qwordAddress);
|
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 resetScheduler();
|
||||||
void commitReadyPipelines();
|
void commitReadyPipelines();
|
||||||
void advanceOneCycle();
|
void advanceOneCycle();
|
||||||
void advanceTo(uint64_t targetCycle);
|
void advanceTo(uint64_t targetCycle);
|
||||||
void flushPipelines();
|
void flushPipelines();
|
||||||
void progressXgkick();
|
void progressXgkick(uint32_t elapsedCycles = 1u);
|
||||||
void finishXgkick();
|
void finishXgkick();
|
||||||
uint64_t calculatePairReadyCycle(const DecodedInstructionPair &decoded) const;
|
uint64_t calculatePairReadyCycle(const DecodedInstructionPair &decoded) const;
|
||||||
void markPairWrites(const DecodedInstructionPair &decoded);
|
void markPairWrites(const DecodedInstructionPair &decoded);
|
||||||
bool pipelinesPending() const;
|
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;
|
float normalizeResult(float value, uint32_t &laneFlags) const;
|
||||||
uint32_t microAddressMask() const;
|
uint32_t microAddressMask() const;
|
||||||
int32_t readBranchVi(uint8_t reg) const;
|
int32_t readBranchVi(uint8_t reg) const;
|
||||||
void recordViWriteForBranch(uint8_t reg, int32_t oldValue);
|
void recordViWriteForBranch(uint8_t reg, int32_t oldValue);
|
||||||
void reportReservedInstruction(bool upper, uint32_t instruction);
|
void reportReservedInstruction(bool upper, uint32_t instruction);
|
||||||
float broadcast(const float *vf, uint8_t bc);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "Common.h"
|
#include "Common.h"
|
||||||
#include "GS.h"
|
#include "GS.h"
|
||||||
|
#include "../Syscalls/System.h"
|
||||||
#include "ps2_log.h"
|
#include "ps2_log.h"
|
||||||
#include "runtime/gs/ps2_gs_common.h"
|
#include "runtime/gs/ps2_gs_common.h"
|
||||||
#include "runtime/gs/ps2_gs_psmct16.h"
|
#include "runtime/gs/ps2_gs_psmct16.h"
|
||||||
@@ -11,8 +12,7 @@ namespace ps2_stubs
|
|||||||
{
|
{
|
||||||
uint64_t makeClearPrim(bool useContext2)
|
uint64_t makeClearPrim(bool useContext2)
|
||||||
{
|
{
|
||||||
return static_cast<uint64_t>(GS_PRIM_SPRITE) |
|
return static_cast<uint64_t>(GS_PRIM_SPRITE) | (static_cast<uint64_t>(useContext2 ? 1u : 0u) << 9);
|
||||||
(static_cast<uint64_t>(useContext2 ? 1u : 0u) << 9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t makeClearRgbaq(uint32_t rgba)
|
uint64_t makeClearRgbaq(uint32_t rgba)
|
||||||
@@ -22,8 +22,7 @@ namespace ps2_stubs
|
|||||||
|
|
||||||
uint64_t makeClearXyz(int32_t x, int32_t y)
|
uint64_t makeClearXyz(int32_t x, int32_t y)
|
||||||
{
|
{
|
||||||
return static_cast<uint64_t>(static_cast<uint16_t>(x << 4)) |
|
return static_cast<uint64_t>(static_cast<uint16_t>(x << 4)) | (static_cast<uint64_t>(static_cast<uint16_t>(y << 4)) << 16);
|
||||||
(static_cast<uint64_t>(static_cast<uint16_t>(y << 4)) << 16);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void seedGsClearPacket(GsClearMem &clear,
|
void seedGsClearPacket(GsClearMem &clear,
|
||||||
@@ -798,55 +797,20 @@ namespace ps2_stubs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
g_gparam.interlace = static_cast<uint8_t>(interlace & 0x1);
|
g_gparam.interlace = static_cast<uint16_t>(interlace & 0x1);
|
||||||
g_gparam.omode = static_cast<uint8_t>(omode & 0xFF);
|
g_gparam.omode = static_cast<uint16_t>(omode & 0xFF);
|
||||||
g_gparam.ffmode = static_cast<uint8_t>(ffmode & 0x1);
|
g_gparam.ffmode = static_cast<uint16_t>(ffmode & 0x1);
|
||||||
writeGsGParamToScratch(runtime);
|
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)
|
if (runtime)
|
||||||
{
|
{
|
||||||
uint32_t pktAddr = runtime->guestMalloc(128u, 16u);
|
auto ®s = runtime->memory().gs();
|
||||||
if (pktAddr != 0u)
|
ps2_syscalls::configureGsCrt(regs, interlace & 1u, omode & 0xffu, ffmode & 1u);
|
||||||
{
|
|
||||||
uint8_t *pkt = getMemPtr(rdram, pktAddr);
|
regs.pmode = makePmode(1, 0, 0, 0, 0, 0x80);
|
||||||
if (pkt)
|
regs.dispfb1 = regs.dispfb2 = makeDispFb(0, 10, 0, 0, 0);
|
||||||
{
|
regs.display1 = regs.display2 = makeDefaultGsDispEnv(0, 640, 448).display;
|
||||||
uint64_t *q = reinterpret_cast<uint64_t *>(pkt);
|
regs.bgcolor = 0;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,11 +851,7 @@ namespace ps2_stubs
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uint32_t fbw = std::max<uint32_t>(1u, (w + 63u) / 64u);
|
const uint32_t fbw = std::max<uint32_t>(1u, (w + 63u) / 64u);
|
||||||
const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u);
|
const GsDispEnvMem displayEnv = makeDefaultGsDispEnv(psm, w, h);
|
||||||
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 int32_t drawWidth = static_cast<int32_t>(w);
|
const int32_t drawWidth = static_cast<int32_t>(w);
|
||||||
const int32_t drawHeight = static_cast<int32_t>(h);
|
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);
|
const uint64_t dispfb1 = makeDispFb(0u, fbw, psm, 0u, 0u);
|
||||||
|
|
||||||
GsDBuffDcMem db{};
|
GsDBuffDcMem db{};
|
||||||
db.disp[0].pmode = pmode;
|
db.disp[0] = displayEnv;
|
||||||
db.disp[0].smode2 = smode2;
|
|
||||||
db.disp[0].dispfb = dispfb0;
|
db.disp[0].dispfb = dispfb0;
|
||||||
db.disp[0].display = display;
|
|
||||||
db.disp[0].bgcolor = 0u;
|
|
||||||
db.disp[1] = db.disp[0];
|
db.disp[1] = db.disp[0];
|
||||||
db.disp[1].dispfb = dispfb1;
|
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 uint32_t fbw = std::max<uint32_t>(1u, (w + 63u) / 64u);
|
||||||
const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u);
|
const GsDispEnvMem displayEnv = makeDefaultGsDispEnv(psm, w, h);
|
||||||
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 int32_t drawWidth = static_cast<int32_t>(w);
|
const int32_t drawWidth = static_cast<int32_t>(w);
|
||||||
const int32_t drawHeight = static_cast<int32_t>(h);
|
const int32_t drawHeight = static_cast<int32_t>(h);
|
||||||
@@ -976,11 +928,7 @@ namespace ps2_stubs
|
|||||||
}
|
}
|
||||||
|
|
||||||
GsDBuffMem db{};
|
GsDBuffMem db{};
|
||||||
db.disp[0].pmode = pmode;
|
db.disp[0] = displayEnv;
|
||||||
db.disp[0].smode2 = smode2;
|
|
||||||
db.disp[0].dispfb = dispfb;
|
|
||||||
db.disp[0].display = display;
|
|
||||||
db.disp[0].bgcolor = 0u;
|
|
||||||
db.disp[1] = db.disp[0];
|
db.disp[1] = db.disp[0];
|
||||||
|
|
||||||
db.giftag0 = {makeGiftagAplusD(14u), 0x0E0E0E0E0E0E0E0EULL};
|
db.giftag0 = {makeGiftagAplusD(14u), 0x0E0E0E0E0E0E0E0EULL};
|
||||||
@@ -1002,21 +950,16 @@ namespace ps2_stubs
|
|||||||
uint32_t psm = getRegU32(ctx, 5);
|
uint32_t psm = getRegU32(ctx, 5);
|
||||||
uint32_t w = getRegU32(ctx, 6);
|
uint32_t w = getRegU32(ctx, 6);
|
||||||
uint32_t h = getRegU32(ctx, 7);
|
uint32_t h = getRegU32(ctx, 7);
|
||||||
const GsTrailingArgs2 trailing = decodeGsTrailingArgs2(rdram, ctx);
|
const int32_t dx = static_cast<int16_t>(getRegU32(ctx, 8));
|
||||||
uint32_t dx = trailing.arg0;
|
const int32_t dy = static_cast<int16_t>(getRegU32(ctx, 9));
|
||||||
uint32_t dy = trailing.arg1;
|
|
||||||
|
|
||||||
if (w == 0)
|
if (w == 0)
|
||||||
w = 640;
|
w = 640;
|
||||||
if (h == 0)
|
if (h == 0)
|
||||||
h = 448;
|
h = 448;
|
||||||
|
|
||||||
uint32_t fbw = (w + 63) / 64;
|
const GsDispEnvMem env = makeDefaultGsDispEnv(psm, w, h, dx, dy);
|
||||||
uint64_t dispfb = makeDispFb(0, fbw, psm, 0, 0);
|
setReturnS32(ctx, writeGsDispEnv(rdram, envAddr, env) ? 0 : -1);
|
||||||
uint64_t display = makeDisplay(dx, dy, 0, 0, w - 1, h - 1);
|
|
||||||
|
|
||||||
writeGsDispEnv(rdram, envAddr, display, dispfb);
|
|
||||||
setReturnS32(ctx, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void sceGsSetDefDrawEnv(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
void sceGsSetDefDrawEnv(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
@@ -1183,6 +1126,7 @@ namespace ps2_stubs
|
|||||||
applyGsClearPacket(runtime, db.clear1);
|
applyGsClearPacket(runtime, db.clear1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runtime->memory().gs().sdkPresentCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
setReturnS32(ctx, static_cast<int32_t>(which ^ 1u));
|
setReturnS32(ctx, static_cast<int32_t>(which ^ 1u));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,6 +1152,7 @@ namespace ps2_stubs
|
|||||||
applyGsRegPairs(runtime, reinterpret_cast<const GsRegPairMem *>(&db.draw1), 8u);
|
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));
|
setReturnS32(ctx, static_cast<int32_t>(which ^ 1u));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1486,11 +1486,12 @@ namespace
|
|||||||
{
|
{
|
||||||
struct GsGParam
|
struct GsGParam
|
||||||
{
|
{
|
||||||
uint8_t interlace;
|
uint16_t interlace;
|
||||||
uint8_t omode;
|
uint16_t omode;
|
||||||
uint8_t ffmode;
|
uint16_t ffmode;
|
||||||
uint8_t version;
|
uint16_t version;
|
||||||
};
|
};
|
||||||
|
static_assert(sizeof(GsGParam) == 8, "sceGsGetGParam exposes four 16-bit fields");
|
||||||
|
|
||||||
struct GsDispEnvMem
|
struct GsDispEnvMem
|
||||||
{
|
{
|
||||||
@@ -1624,6 +1625,35 @@ namespace
|
|||||||
(static_cast<uint64_t>(dh & 0x07FF) << 44);
|
(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)
|
static uint64_t makeFrame(uint32_t fbp, uint32_t fbw, uint32_t psm, uint32_t fbmsk)
|
||||||
{
|
{
|
||||||
return (static_cast<uint64_t>(fbp & 0x1FFu) << 0) |
|
return (static_cast<uint64_t>(fbp & 0x1FFu) << 0) |
|
||||||
@@ -1805,15 +1835,11 @@ namespace
|
|||||||
return true;
|
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);
|
uint8_t *ptr = getMemPtr(rdram, addr);
|
||||||
if (!ptr)
|
if (!ptr)
|
||||||
return false;
|
return false;
|
||||||
GsDispEnvMem env{};
|
|
||||||
std::memcpy(&env, ptr, sizeof(env));
|
|
||||||
env.dispfb = dispfb;
|
|
||||||
env.display = display;
|
|
||||||
std::memcpy(ptr, &env, sizeof(env));
|
std::memcpy(ptr, &env, sizeof(env));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1881,9 +1907,9 @@ namespace
|
|||||||
auto ®s = runtime->memory().gs();
|
auto ®s = runtime->memory().gs();
|
||||||
regs.pmode = env.pmode;
|
regs.pmode = env.pmode;
|
||||||
regs.smode2 = env.smode2;
|
regs.smode2 = env.smode2;
|
||||||
regs.dispfb1 = env.dispfb;
|
regs.writeDisplayFramebuffer(0, env.dispfb);
|
||||||
regs.display1 = env.display;
|
regs.display1 = env.display;
|
||||||
regs.dispfb2 = env.dispfb;
|
regs.writeDisplayFramebuffer(1, env.dispfb);
|
||||||
regs.display2 = env.display;
|
regs.display2 = env.display;
|
||||||
regs.bgcolor = env.bgcolor;
|
regs.bgcolor = env.bgcolor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,30 @@
|
|||||||
|
|
||||||
namespace ps2_syscalls
|
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)
|
void GsSetCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
|
||||||
{
|
{
|
||||||
int interlaced = getRegU32(ctx, 4); // $a0 - 0=non-interlaced, 1=interlaced
|
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
|
int frameMode = getRegU32(ctx, 6); // $a2 - 0=field, 1=frame
|
||||||
|
|
||||||
if (runtime)
|
if (runtime)
|
||||||
{
|
{
|
||||||
auto &gs = runtime->memory().gs();
|
configureGsCrt(runtime->memory().gs(), interlaced, videoMode, frameMode);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RUNTIME_LOG("PS2 GsSetCrt: interlaced=" << interlaced
|
RUNTIME_LOG("PS2 GsSetCrt: interlaced=" << interlaced
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
namespace ps2_syscalls
|
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);
|
bool dispatchSyscallOverride(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime);
|
||||||
void GsSetCrt(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);
|
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)
|
if (vram && vramSize < GSMem::MEMORY_SIZE)
|
||||||
throw std::invalid_argument("GS CPU backend requires at least 4 MiB of VRAM");
|
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);
|
std::lock_guard<std::mutex> lock(m_mutex);
|
||||||
m_vram = vram;
|
m_vram = vram;
|
||||||
m_vramSize = vramSize;
|
m_vramSize = vramSize;
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ void GS::reset()
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
|
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
|
||||||
m_hostPresentationFrame.clear();
|
m_hostPresentationFrame.clear();
|
||||||
|
m_hostPresentationGpuFrame.reset();
|
||||||
m_hostPresentationWidth = 0u;
|
m_hostPresentationWidth = 0u;
|
||||||
m_hostPresentationHeight = 0u;
|
m_hostPresentationHeight = 0u;
|
||||||
m_hostPresentationDisplayFbp = 0u;
|
m_hostPresentationDisplayFbp = 0u;
|
||||||
@@ -260,6 +261,8 @@ GSDebugSnapshot GS::getDebugSnapshot() const
|
|||||||
snapshot.hasHostPresentationFrame = m_hasHostPresentationFrame;
|
snapshot.hasHostPresentationFrame = m_hasHostPresentationFrame;
|
||||||
}
|
}
|
||||||
snapshot.localToHostPendingBytes = transfer.localToHostPendingBytes;
|
snapshot.localToHostPendingBytes = transfer.localToHostPendingBytes;
|
||||||
|
if (m_backend && m_backend->UsesRawCommands())
|
||||||
|
m_backend->ReadRegisterState(snapshot);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,6 +512,9 @@ GSPresentationRequest GS::buildPresentationRequestUnlocked() const
|
|||||||
if (!m_privRegs)
|
if (!m_privRegs)
|
||||||
return request;
|
return request;
|
||||||
request.pmode = m_privRegs->pmode;
|
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.smode2 = m_privRegs->smode2;
|
||||||
request.dispfb1 = m_privRegs->dispfb1;
|
request.dispfb1 = m_privRegs->dispfb1;
|
||||||
request.display1 = m_privRegs->display1;
|
request.display1 = m_privRegs->display1;
|
||||||
@@ -533,6 +539,7 @@ void GS::latchHostPresentationFrame()
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
|
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
|
||||||
m_hostPresentationFrame.clear();
|
m_hostPresentationFrame.clear();
|
||||||
|
m_hostPresentationGpuFrame.reset();
|
||||||
m_hasHostPresentationFrame = false;
|
m_hasHostPresentationFrame = false;
|
||||||
m_hostPresentationWidth = m_hostPresentationHeight = 0u;
|
m_hostPresentationWidth = m_hostPresentationHeight = 0u;
|
||||||
return;
|
return;
|
||||||
@@ -560,6 +567,8 @@ void GS::latchHostPresentationFrame()
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
|
std::lock_guard<std::mutex> presentationLock(m_presentationMutex);
|
||||||
m_hostPresentationFrame = std::move(frame.pixels);
|
m_hostPresentationFrame = std::move(frame.pixels);
|
||||||
|
m_hostPresentationGpuFrame = std::move(frame.gpu);
|
||||||
|
m_hostPresentationAspectRatio = frame.aspectRatio;
|
||||||
m_hostPresentationWidth = width;
|
m_hostPresentationWidth = width;
|
||||||
m_hostPresentationHeight = height;
|
m_hostPresentationHeight = height;
|
||||||
m_hostPresentationDisplayFbp = displayFbp;
|
m_hostPresentationDisplayFbp = displayFbp;
|
||||||
@@ -638,9 +647,16 @@ bool GS::copyLatchedHostPresentationFrame(std::vector<uint8_t> &outPixels,
|
|||||||
return true;
|
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);
|
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)
|
if (!data || sizeBytes < 16 || !m_backend)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -745,6 +761,13 @@ bool GS::processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes)
|
|||||||
if (!validatePackedGifPacket(data, sizeBytes))
|
if (!validatePackedGifPacket(data, sizeBytes))
|
||||||
return false;
|
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)
|
const bool processed = visitPackedGifPacket(data, sizeBytes, [&](const PackedGifPacketTag &tag)
|
||||||
{
|
{
|
||||||
m_curQ = 1.0f;
|
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)
|
void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value)
|
||||||
{
|
{
|
||||||
|
if (m_backend && m_backend->UsesRawCommands())
|
||||||
|
{
|
||||||
|
m_backend->WriteRegisterRaw(regAddr, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const bool interestingReg =
|
const bool interestingReg =
|
||||||
regAddr == GS_REG_PRIM ||
|
regAddr == GS_REG_PRIM ||
|
||||||
regAddr == GS_REG_RGBAQ ||
|
regAddr == GS_REG_RGBAQ ||
|
||||||
@@ -1499,7 +1527,7 @@ void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value)
|
|||||||
}
|
}
|
||||||
case 0x59:
|
case 0x59:
|
||||||
if (m_privRegs)
|
if (m_privRegs)
|
||||||
m_privRegs->dispfb1 = value;
|
m_privRegs->writeDisplayFramebuffer(0, value);
|
||||||
break;
|
break;
|
||||||
case 0x5a:
|
case 0x5a:
|
||||||
if (m_privRegs)
|
if (m_privRegs)
|
||||||
@@ -1507,7 +1535,7 @@ void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value)
|
|||||||
break;
|
break;
|
||||||
case 0x5b:
|
case 0x5b:
|
||||||
if (m_privRegs)
|
if (m_privRegs)
|
||||||
m_privRegs->dispfb2 = value;
|
m_privRegs->writeDisplayFramebuffer(1, value);
|
||||||
break;
|
break;
|
||||||
case 0x5c:
|
case 0x5c:
|
||||||
if (m_privRegs)
|
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)
|
bool GS::clearFramebufferContext(uint32_t contextIndex, uint32_t rgba)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_stateMutex);
|
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)
|
bool GS::clearActiveFramebuffer(uint32_t rgba)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::recursive_mutex> lock(m_stateMutex);
|
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)
|
uint32_t GS::consumeLocalToHostBytes(uint8_t *dst, uint32_t maxBytes)
|
||||||
@@ -1733,3 +1762,31 @@ void GS::updatePreferredDisplaySourceForDraw(const GSPrimitiveBatch &batch)
|
|||||||
m_hasPreferredDisplaySource = true;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 ®isters) : 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 ®isters)
|
||||||
|
{
|
||||||
|
RUNTIME_WARNING("GS Parallel backend is experimental and may not be fully compatible with all games.");
|
||||||
|
return std::make_unique<ParallelBackend>(registers);
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ void GifArbiter::drain()
|
|||||||
auto &pkt = m_queue[i];
|
auto &pkt = m_queue[i];
|
||||||
if (!pkt.data.empty())
|
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();
|
m_queue.clear();
|
||||||
|
|||||||
@@ -2182,6 +2182,7 @@ void PS2DebugPanel::initialize()
|
|||||||
if (!m_initialized)
|
if (!m_initialized)
|
||||||
{
|
{
|
||||||
rlImGuiSetup(true);
|
rlImGuiSetup(true);
|
||||||
|
m_fpsSampleStart = {};
|
||||||
m_initialized = true;
|
m_initialized = true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -2205,6 +2206,40 @@ void PS2DebugPanel::draw(PS2Runtime &runtime)
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const auto ®s = 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))
|
if (IsKeyPressed(KEY_F1))
|
||||||
{
|
{
|
||||||
@@ -2227,6 +2262,13 @@ void PS2DebugPanel::draw(PS2Runtime &runtime)
|
|||||||
ImGui::EndMenuBar();
|
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::BeginTabBar("debug-tabs"))
|
||||||
{
|
{
|
||||||
if (ImGui::BeginTabItem("CPU"))
|
if (ImGui::BeginTabItem("CPU"))
|
||||||
|
|||||||
@@ -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;
|
constexpr uint32_t kGsCsrRegOffset = 0x1000u;
|
||||||
|
|
||||||
// Atomically apply a 32-bit write to one half (off=0 low dword, off=4 high
|
// 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 mask = 0xFFFFFFFFULL << (off * 8);
|
||||||
uint64_t newVal = (*reg & ~mask) | ((uint64_t)value << (off * 8));
|
uint64_t newVal = (*reg & ~mask) | ((uint64_t)value << (off * 8));
|
||||||
*reg = newVal;
|
writeGsRegister(gs_regs, reg, newVal);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1022,7 +1032,7 @@ void PS2Memory::write64(uint32_t address, uint64_t value)
|
|||||||
}
|
}
|
||||||
else if (uint64_t *reg = gsRegPtr(gs_regs, address))
|
else if (uint64_t *reg = gsRegPtr(gs_regs, address))
|
||||||
{
|
{
|
||||||
*reg = value;
|
writeGsRegister(gs_regs, reg, value);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1173,7 +1183,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value)
|
|||||||
else if (uint64_t *reg = gsRegPtr(gs_regs, address))
|
else if (uint64_t *reg = gsRegPtr(gs_regs, address))
|
||||||
{
|
{
|
||||||
const uint64_t mask = 0xFFFFFFFFull << (off * 8u);
|
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);
|
m_gsWriteCount.fetch_add(1, std::memory_order_relaxed);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -5,12 +5,18 @@
|
|||||||
#include "game_overrides.h"
|
#include "game_overrides.h"
|
||||||
#include "ps2_runtime_macros.h"
|
#include "ps2_runtime_macros.h"
|
||||||
#include "runtime/gs/gs_frontend.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 "runtime/ee_scheduler.h"
|
||||||
#include "ThreadNaming.h"
|
#include "ThreadNaming.h"
|
||||||
#include "Kernel/Stubs/Audio.h"
|
#include "Kernel/Stubs/Audio.h"
|
||||||
#include "Kernel/Stubs/GS.h"
|
#include "Kernel/Stubs/GS.h"
|
||||||
#include "Kernel/Stubs/MPEG.h"
|
#include "Kernel/Stubs/MPEG.h"
|
||||||
#include "ps2_host_backend.h"
|
#include "ps2_host_backend.h"
|
||||||
|
#ifdef PS2X_GS_PARALLEL
|
||||||
|
#include "rlgl.h"
|
||||||
|
#endif
|
||||||
#include "ps2_iop_host.h"
|
#include "ps2_iop_host.h"
|
||||||
#include "ps2x/iop/iop_subsystem.h"
|
#include "ps2x/iop/iop_subsystem.h"
|
||||||
|
|
||||||
@@ -22,6 +28,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -394,7 +401,12 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint
|
|||||||
s_lastPresentationTick = currentTick;
|
s_lastPresentationTick = currentTick;
|
||||||
s_hasLatchedInitialFrame = true;
|
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;
|
outWidth = (s_lastWidth != 0u) ? s_lastWidth : FB_WIDTH;
|
||||||
outHeight = (s_lastHeight != 0u) ? s_lastHeight : DEFAULT_DISPLAY_HEIGHT;
|
outHeight = (s_lastHeight != 0u) ? s_lastHeight : DEFAULT_DISPLAY_HEIGHT;
|
||||||
@@ -549,6 +561,7 @@ PS2Runtime::~PS2Runtime()
|
|||||||
|
|
||||||
if (IsWindowReady())
|
if (IsWindowReady())
|
||||||
{
|
{
|
||||||
|
m_gs.shutdownBackend();
|
||||||
CloseWindow();
|
CloseWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,8 +678,8 @@ bool PS2Runtime::syncCoreSubsystems()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_gs.init(gsVram, static_cast<uint32_t>(PS2_GS_VRAM_SIZE), &m_memory.gs());
|
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_gifArbiter.setProcessPacketFn([this](GifPathId path, const uint8_t *data, uint32_t size)
|
||||||
{ m_gs.processGIFPacket(data, size); });
|
{ m_gs.processGIFPacket(data, size, static_cast<uint32_t>(path)); });
|
||||||
m_memory.setGifArbiter(&m_gifArbiter);
|
m_memory.setGifArbiter(&m_gifArbiter);
|
||||||
m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t top, uint32_t itop)
|
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);
|
InitWindow(HOST_WINDOW_WIDTH, HOST_WINDOW_HEIGHT, title);
|
||||||
InitAudioDevice();
|
InitAudioDevice();
|
||||||
m_audioBackend.setAudioReady(IsAudioDeviceReady());
|
m_audioBackend.setAudioReady(IsAudioDeviceReady());
|
||||||
|
#endif
|
||||||
|
#ifdef PS2X_GS_PARALLEL
|
||||||
|
m_gs.setRasterBackend(CreateParallelGSBackend(m_memory.gs()));
|
||||||
#endif
|
#endif
|
||||||
SetTargetFPS(60);
|
SetTargetFPS(60);
|
||||||
if (m_debugUiInitCallback)
|
if (m_debugUiInitCallback)
|
||||||
@@ -2386,73 +2402,98 @@ void PS2Runtime::run()
|
|||||||
gameThreadFinished.store(true, std::memory_order_release); });
|
gameThreadFinished.store(true, std::memory_order_release); });
|
||||||
|
|
||||||
uint64_t tick = 0;
|
uint64_t tick = 0;
|
||||||
while (!isStopRequested() && !gameThreadFinished.load(std::memory_order_acquire))
|
std::exception_ptr presentationError;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
PS2_IF_AGRESSIVE_LOGS({
|
while (!isStopRequested() && !gameThreadFinished.load(std::memory_order_acquire))
|
||||||
tick++;
|
{
|
||||||
if ((tick % 120) == 0)
|
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();
|
rlDrawRenderBatchActive();
|
||||||
uint64_t curGif = m_memory.gifCopyCount();
|
gpuFrame->ReleaseTexture();
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
#endif
|
||||||
uint32_t presentWidth = FB_WIDTH;
|
if (m_debugUiInitialized && m_debugUiDrawCallback)
|
||||||
uint32_t presentHeight = DEFAULT_DISPLAY_HEIGHT;
|
{
|
||||||
UploadFrame(frameTex, this, presentWidth, presentHeight);
|
m_debugUiDrawCallback(*this, m_debugUiUserData);
|
||||||
|
}
|
||||||
|
EndDrawing();
|
||||||
|
|
||||||
BeginDrawing();
|
if (WindowShouldClose())
|
||||||
ClearBackground(BLACK);
|
{
|
||||||
const float srcWidth = static_cast<float>(std::max<uint32_t>(1u, presentWidth));
|
RUNTIME_LOG("[run] window close requested, breaking out of loop");
|
||||||
const float srcHeight = static_cast<float>(std::max<uint32_t>(1u, presentHeight));
|
requestStop();
|
||||||
const float screenWidth = static_cast<float>(GetScreenWidth());
|
break;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
presentationError = std::current_exception();
|
||||||
|
}
|
||||||
|
|
||||||
requestStop();
|
requestStop();
|
||||||
if (gameThread.joinable())
|
if (gameThread.joinable())
|
||||||
@@ -2466,7 +2507,11 @@ void PS2Runtime::run()
|
|||||||
m_debugUiInitialized = false;
|
m_debugUiInitialized = false;
|
||||||
}
|
}
|
||||||
UnloadTexture(frameTex);
|
UnloadTexture(frameTex);
|
||||||
|
m_gs.shutdownBackend();
|
||||||
CloseWindow();
|
CloseWindow();
|
||||||
|
|
||||||
|
if (presentationError)
|
||||||
|
std::rethrow_exception(presentationError);
|
||||||
|
|
||||||
RUNTIME_LOG("[run] exiting loop");
|
RUNTIME_LOG("[run] exiting loop");
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -21,30 +21,32 @@ namespace
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Upper instructions (FMAC pipeline)
|
// Upper instructions (FMAC pipeline)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
void VU1Interpreter::execUpper(uint32_t instr)
|
void VU1Interpreter::execUpper(uint32_t instr, float *vfResult, float *accResult)
|
||||||
{
|
{
|
||||||
m_currentUpperInstruction = instr;
|
m_currentUpperInstruction = instr;
|
||||||
uint8_t dest = DEST(instr);
|
uint8_t dest = DEST(instr);
|
||||||
uint8_t ft = FT(instr);
|
uint8_t ft = FT(instr);
|
||||||
uint8_t fs = FS(instr);
|
uint8_t fs = FS(instr);
|
||||||
uint8_t fd = FD(instr);
|
|
||||||
uint8_t op = instr & 0x3F;
|
uint8_t op = instr & 0x3F;
|
||||||
|
|
||||||
float *vd = m_state.vf[fd];
|
// NOP has no operands or flag effects.
|
||||||
float normalizedVs[4];
|
const uint8_t special = static_cast<uint8_t>((instr & 3u) | ((instr >> 4) & 0x7Cu));
|
||||||
float normalizedVt[4];
|
if (op >= 0x3Cu && (special == 0x2Fu || special == 0x30u))
|
||||||
float normalizedAcc[4];
|
return;
|
||||||
|
|
||||||
|
float *vd = vfResult;
|
||||||
|
auto &operands = m_upperOperands;
|
||||||
for (uint32_t component = 0; component < 4u; ++component)
|
for (uint32_t component = 0; component < 4u; ++component)
|
||||||
{
|
{
|
||||||
normalizedVs[component] = normalizeOperand(m_state.vf[fs][component]);
|
operands.vs[component] = normalizeOperand(m_state.vf[fs][component]);
|
||||||
normalizedVt[component] = normalizeOperand(m_state.vf[ft][component]);
|
operands.vt[component] = normalizeOperand(m_state.vf[ft][component]);
|
||||||
normalizedAcc[component] = normalizeOperand(m_state.acc[component]);
|
operands.acc[component] = normalizeOperand(m_state.acc[component]);
|
||||||
}
|
}
|
||||||
const float *vs = normalizedVs;
|
const float *vs = operands.vs;
|
||||||
const float *vt = normalizedVt;
|
const float *vt = operands.vt;
|
||||||
const float *acc = normalizedAcc;
|
const float *acc = operands.acc;
|
||||||
const float q = normalizeOperand(m_state.q);
|
const float q = operands.q = normalizeOperand(m_state.q);
|
||||||
const float i = normalizeOperand(m_state.i);
|
const float i = operands.i = normalizeOperand(m_state.i);
|
||||||
float result[4];
|
float result[4];
|
||||||
|
|
||||||
// Upper opcode decoding (bits 5:0 of upper word)
|
// Upper opcode decoding (bits 5:0 of upper word)
|
||||||
@@ -55,7 +57,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x02:
|
case 0x02:
|
||||||
case 0x03: // ADDbc
|
case 0x03: // ADDbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, op & 3);
|
float bc = vt[op & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] + bc;
|
result[c] = vs[c] + bc;
|
||||||
applyFmacDest(vd, result, dest);
|
applyFmacDest(vd, result, dest);
|
||||||
@@ -66,7 +68,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x06:
|
case 0x06:
|
||||||
case 0x07: // SUBbc
|
case 0x07: // SUBbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, op & 3);
|
float bc = vt[op & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] - bc;
|
result[c] = vs[c] - bc;
|
||||||
applyFmacDest(vd, result, dest);
|
applyFmacDest(vd, result, dest);
|
||||||
@@ -77,7 +79,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x0A:
|
case 0x0A:
|
||||||
case 0x0B: // MADDbc
|
case 0x0B: // MADDbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, op & 3);
|
float bc = vt[op & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] + vs[c] * bc;
|
result[c] = acc[c] + vs[c] * bc;
|
||||||
applyFmacDest(vd, result, dest);
|
applyFmacDest(vd, result, dest);
|
||||||
@@ -88,7 +90,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x0E:
|
case 0x0E:
|
||||||
case 0x0F: // MSUBbc
|
case 0x0F: // MSUBbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, op & 3);
|
float bc = vt[op & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] - vs[c] * bc;
|
result[c] = acc[c] - vs[c] * bc;
|
||||||
applyFmacDest(vd, result, dest);
|
applyFmacDest(vd, result, dest);
|
||||||
@@ -99,7 +101,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x12:
|
case 0x12:
|
||||||
case 0x13: // MAXbc
|
case 0x13: // MAXbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, op & 3);
|
float bc = vt[op & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = (vs[c] > bc) ? vs[c] : bc;
|
result[c] = (vs[c] > bc) ? vs[c] : bc;
|
||||||
applyDest(vd, result, dest);
|
applyDest(vd, result, dest);
|
||||||
@@ -110,7 +112,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x16:
|
case 0x16:
|
||||||
case 0x17: // MINIbc
|
case 0x17: // MINIbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, op & 3);
|
float bc = vt[op & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = (vs[c] < bc) ? vs[c] : bc;
|
result[c] = (vs[c] < bc) ? vs[c] : bc;
|
||||||
applyDest(vd, result, dest);
|
applyDest(vd, result, dest);
|
||||||
@@ -121,7 +123,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x1A:
|
case 0x1A:
|
||||||
case 0x1B: // MULbc
|
case 0x1B: // MULbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, op & 3);
|
float bc = vt[op & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] * bc;
|
result[c] = vs[c] * bc;
|
||||||
applyFmacDest(vd, result, dest);
|
applyFmacDest(vd, result, dest);
|
||||||
@@ -240,7 +242,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x3F:
|
case 0x3F:
|
||||||
{
|
{
|
||||||
const uint8_t specialOp = static_cast<uint8_t>((instr & 0x3u) | ((instr >> 4) & 0x7Cu));
|
const uint8_t specialOp = static_cast<uint8_t>((instr & 0x3u) | ((instr >> 4) & 0x7Cu));
|
||||||
float *vtDest = m_state.vf[ft];
|
float *vtDest = vfResult;
|
||||||
|
|
||||||
switch (specialOp)
|
switch (specialOp)
|
||||||
{
|
{
|
||||||
@@ -249,10 +251,10 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x02:
|
case 0x02:
|
||||||
case 0x03: // ADDAbc
|
case 0x03: // ADDAbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, specialOp & 3);
|
float bc = vt[specialOp & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] + bc;
|
result[c] = vs[c] + bc;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 0x04:
|
case 0x04:
|
||||||
@@ -260,10 +262,10 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x06:
|
case 0x06:
|
||||||
case 0x07: // SUBAbc
|
case 0x07: // SUBAbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, specialOp & 3);
|
float bc = vt[specialOp & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] - bc;
|
result[c] = vs[c] - bc;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 0x08:
|
case 0x08:
|
||||||
@@ -271,10 +273,10 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x0A:
|
case 0x0A:
|
||||||
case 0x0B: // MADDAbc
|
case 0x0B: // MADDAbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, specialOp & 3);
|
float bc = vt[specialOp & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] + vs[c] * bc;
|
result[c] = acc[c] + vs[c] * bc;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 0x0C:
|
case 0x0C:
|
||||||
@@ -282,10 +284,10 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x0E:
|
case 0x0E:
|
||||||
case 0x0F: // MSUBAbc
|
case 0x0F: // MSUBAbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, specialOp & 3);
|
float bc = vt[specialOp & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] - vs[c] * bc;
|
result[c] = acc[c] - vs[c] * bc;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 0x10: // ITOF0
|
case 0x10: // ITOF0
|
||||||
@@ -361,16 +363,16 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x1A:
|
case 0x1A:
|
||||||
case 0x1B: // MULAbc
|
case 0x1B: // MULAbc
|
||||||
{
|
{
|
||||||
float bc = broadcast(vt, specialOp & 3);
|
float bc = vt[specialOp & 3];
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] * bc;
|
result[c] = vs[c] * bc;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 0x1C: // MULAq
|
case 0x1C: // MULAq
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] * q;
|
result[c] = vs[c] * q;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x1D: // ABS
|
case 0x1D: // ABS
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
@@ -380,7 +382,7 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x1E: // MULAi
|
case 0x1E: // MULAi
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] * i;
|
result[c] = vs[c] * i;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x1F: // CLIP
|
case 0x1F: // CLIP
|
||||||
{
|
{
|
||||||
@@ -417,74 +419,74 @@ void VU1Interpreter::execUpper(uint32_t instr)
|
|||||||
case 0x20: // ADDAq
|
case 0x20: // ADDAq
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] + q;
|
result[c] = vs[c] + q;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x21: // MADDAq
|
case 0x21: // MADDAq
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] + vs[c] * q;
|
result[c] = acc[c] + vs[c] * q;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x22: // ADDAi
|
case 0x22: // ADDAi
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] + i;
|
result[c] = vs[c] + i;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x23: // MADDAi
|
case 0x23: // MADDAi
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] + vs[c] * i;
|
result[c] = acc[c] + vs[c] * i;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x24: // SUBAq
|
case 0x24: // SUBAq
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] - q;
|
result[c] = vs[c] - q;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x25: // MSUBAq
|
case 0x25: // MSUBAq
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] - vs[c] * q;
|
result[c] = acc[c] - vs[c] * q;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x26: // SUBAi
|
case 0x26: // SUBAi
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] - i;
|
result[c] = vs[c] - i;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x27: // MSUBAi
|
case 0x27: // MSUBAi
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] - vs[c] * i;
|
result[c] = acc[c] - vs[c] * i;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x28: // ADDA
|
case 0x28: // ADDA
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] + vt[c];
|
result[c] = vs[c] + vt[c];
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x29: // MADDA
|
case 0x29: // MADDA
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] + vs[c] * vt[c];
|
result[c] = acc[c] + vs[c] * vt[c];
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x2A: // MULA
|
case 0x2A: // MULA
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] * vt[c];
|
result[c] = vs[c] * vt[c];
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x2C: // SUBA
|
case 0x2C: // SUBA
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = vs[c] - vt[c];
|
result[c] = vs[c] - vt[c];
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x2D: // MSUBA
|
case 0x2D: // MSUBA
|
||||||
for (int c = 0; c < 4; c++)
|
for (int c = 0; c < 4; c++)
|
||||||
result[c] = acc[c] - vs[c] * vt[c];
|
result[c] = acc[c] - vs[c] * vt[c];
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x2E: // OPMULA
|
case 0x2E: // OPMULA
|
||||||
result[0] = vs[1] * vt[2];
|
result[0] = vs[1] * vt[2];
|
||||||
result[1] = vs[2] * vt[0];
|
result[1] = vs[2] * vt[0];
|
||||||
result[2] = vs[0] * vt[1];
|
result[2] = vs[0] * vt[1];
|
||||||
result[3] = 0.0f;
|
result[3] = 0.0f;
|
||||||
applyFmacDestAcc(result, dest);
|
applyFmacDest(accResult, result, dest);
|
||||||
return;
|
return;
|
||||||
case 0x2F:
|
case 0x2F:
|
||||||
case 0x30: // NOP
|
case 0x30: // NOP
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
include(CTest)
|
include(CTest)
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
add_subdirectory(gs_cache)
|
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()
|
endif()
|
||||||
|
|
||||||
# Static library with test logic (no main), used by ps2xStudio
|
# Static library with test logic (no main), used by ps2xStudio
|
||||||
|
|||||||
@@ -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_cpu_backend.cpp"
|
||||||
"${PS2_GS_RUNTIME_DIR}/src/lib/gs/gs_frontend.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_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_include_directories(ps2_gs_cache_backend_under_test PUBLIC "${PS2_GS_RUNTIME_DIR}/include")
|
||||||
target_compile_features(ps2_gs_cache_backend_under_test PUBLIC cxx_std_20)
|
target_compile_features(ps2_gs_cache_backend_under_test PUBLIC cxx_std_20)
|
||||||
@@ -41,6 +42,8 @@ function(add_gs_cache_suite target source prefix)
|
|||||||
endforeach()
|
endforeach()
|
||||||
endfunction()
|
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
|
add_gs_cache_suite(ps2_gs_texture_cache_tests gs_texture_cache_tests.cpp texture
|
||||||
unaligned_texture unaligned_wrap stale_mirror page_alternation
|
unaligned_texture unaligned_wrap stale_mirror page_alternation
|
||||||
flush_visibility upload_visibility local_copy_visibility raster_visibility
|
flush_visibility upload_visibility local_copy_visibility raster_visibility
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -503,7 +503,7 @@ void register_ps2_gs_tests()
|
|||||||
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
|
std::vector<uint8_t> rdram(PS2_RAM_SIZE, 0u);
|
||||||
R5900Context ctx{};
|
R5900Context ctx{};
|
||||||
setRegU32(ctx, 4, 1u); // interlaced
|
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
|
setRegU32(ctx, 6, 0u); // field mode
|
||||||
|
|
||||||
runtime.memory().gs().pmode = 0u;
|
runtime.memory().gs().pmode = 0u;
|
||||||
@@ -512,6 +512,10 @@ void register_ps2_gs_tests()
|
|||||||
|
|
||||||
t.Equals(runtime.memory().gs().smode2, 0x1ull,
|
t.Equals(runtime.memory().gs().smode2, 0x1ull,
|
||||||
"GsSetCrt should publish interlaced field mode through SMODE2");
|
"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,
|
t.Equals(runtime.memory().gs().pmode & 0x3ull, 0x1ull,
|
||||||
"GsSetCrt should leave CRT1 enabled for presentation");
|
"GsSetCrt should leave CRT1 enabled for presentation");
|
||||||
t.Equals(getRegU32Test(ctx, 2), 0u,
|
t.Equals(getRegU32Test(ctx, 2), 0u,
|
||||||
@@ -557,8 +561,9 @@ void register_ps2_gs_tests()
|
|||||||
std::memcpy(&xyoffset10Addr, rdram.data() + kEnvAddr + kXYOffset1AddrOffset, sizeof(xyoffset10Addr));
|
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((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 >> 23) & 0x0Full, 3ull, "NTSC should use four output clocks per pixel");
|
||||||
t.Equals((display0 >> 44) & 0x07FFull, 447ull, "dbuff display env should seed DH from height");
|
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((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(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");
|
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;
|
dispfb1 = (dispfb1 & ~0x1FFull) | 151ull;
|
||||||
std::memcpy(rdram.data() + kEnvAddr + kDispEnvSize + kDispFbOffset, &dispfb1, sizeof(dispfb1));
|
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));
|
std::memset(&ctx, 0, sizeof(ctx));
|
||||||
setRegU32(ctx, 4, kEnvAddr);
|
setRegU32(ctx, 4, kEnvAddr);
|
||||||
setRegU32(ctx, 5, 1u);
|
setRegU32(ctx, 5, 1u);
|
||||||
@@ -579,8 +590,92 @@ void register_ps2_gs_tests()
|
|||||||
|
|
||||||
t.Equals(runtime.memory().gs().dispfb1 & 0x1FFull, 151ull,
|
t.Equals(runtime.memory().gs().dispfb1 & 0x1FFull, 151ull,
|
||||||
"sceGsSwapDBuffDc should program GS to the selected display page");
|
"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");
|
"sceGsSwapDBuffDc should preserve the display width from the seeded env");
|
||||||
|
auto ®s = 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)
|
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)");
|
"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;
|
PS2Runtime runtime;
|
||||||
t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed");
|
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,
|
t.Equals(static_cast<int32_t>(getRegU32Test(ctx, 2)), 0,
|
||||||
"sceGsResetGraph should succeed in reset mode");
|
"sceGsResetGraph should succeed in reset mode");
|
||||||
|
const auto ®s = 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,
|
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)
|
tc.Run("sceGsSyncV resumes through the scheduler with deterministic field parity", [](TestCase &t)
|
||||||
|
|||||||
@@ -159,6 +159,40 @@ void register_ps2_memory_tests()
|
|||||||
{
|
{
|
||||||
MiniTest::Case("PS2Memory", [](TestCase &tc)
|
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 ®s = 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, ®s);
|
||||||
|
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)
|
tc.Run("uncached aliases map to same RDRAM bytes", [](TestCase &t)
|
||||||
{
|
{
|
||||||
PS2Memory mem;
|
PS2Memory mem;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "runtime/ps2_memory.h"
|
#include "runtime/ps2_memory.h"
|
||||||
#include "runtime/ps2_vu1.h"
|
#include "runtime/ps2_vu1.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -1690,6 +1691,213 @@ void register_ps2_vu1_tests()
|
|||||||
"the underflowing product should set sticky Z and U");
|
"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)
|
tc.Run("reserved opcodes stop before executing or corrupting state", [](TestCase &t)
|
||||||
{
|
{
|
||||||
Vu1Fixture fx;
|
Vu1Fixture fx;
|
||||||
|
|||||||
Reference in New Issue
Block a user