diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c40f7ec45..4fc884a5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,7 @@ jobs: env: WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7 WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip + SSL_CERT_FILE: C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt defaults: run: shell: bash diff --git a/.gitmodules b/.gitmodules index 995853108..0a5f83fb7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,6 +19,9 @@ [submodule "3rdparty/LibAtrac9"] path = 3rdparty/LibAtrac9 url = https://github.com/shadps4-emu/ext-LibAtrac9.git +[submodule "3rdparty/ffmpeg-core"] + path = 3rdparty/ffmpeg-core + url = https://github.com/KytyPS5/ext-ffmpeg-core.git [submodule "3rdparty/freetype"] path = 3rdparty/freetype url = https://github.com/freetype/freetype.git diff --git a/3rdparty/ffmpeg-core b/3rdparty/ffmpeg-core new file mode 160000 index 000000000..9ac4cfd19 --- /dev/null +++ b/3rdparty/ffmpeg-core @@ -0,0 +1 @@ +Subproject commit 9ac4cfd195f192ed8b08566f49c28dbb48d08341 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3751edef8..b17426dec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,13 @@ set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "") add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") +set(ARCHITECTURE x86_64) +add_subdirectory(3rdparty/ffmpeg-core) +if(MINGW) + target_link_libraries(ffmpeg INTERFACE bcrypt ole32 user32) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(ffmpeg INTERFACE LINKER:--exclude-libs,ALL) +endif() set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE) set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index a3e7b1519..f44ae80e3 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -458,6 +458,15 @@ if(BUILD_TESTING) set_tests_properties(mspace PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_executable(guest_formatting_tests tests/Formatting.cpp) + target_include_directories(guest_formatting_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_formatting_tests PRIVATE libc) + configure_windows_unwind(guest_formatting_tests) + if(WIN32) + add_test(NAME guest_formatting COMMAND guest_formatting_tests) + set_tests_properties(guest_formatting PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_executable(guest_memory_tests tests/GuestMemory.cpp) target_include_directories(guest_memory_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libSceAgc/Cb/src/Registers.cpp b/core/libs/prx/libSceAgc/Cb/src/Registers.cpp index 62857c1c2..29b68fe01 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Registers.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Registers.cpp @@ -17,15 +17,11 @@ std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, st } uint32_t APS5_VABI sceAgcCbSetShRegisterRangeDirectGetSize(uint32_t num_values) { - (void)num_values; - NotImplemented_nid_no_patch(__func__); - return 0; + return (num_values + 2u) * 4u; } std::uint32_t APS5_VABI sceAgcCbSetUcRegisterRangeDirectGetSize(std::uint32_t numValues) { - (void)numValues; - NotImplemented_nid_no_patch(__func__); - return 0; + return (numValues + 2u) * 4u; } std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer* buf, const volatile ShaderRegister* regs, std::uint32_t numRegs) { diff --git a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp index 473c39dae..ad70ced12 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Sync.cpp @@ -15,9 +15,7 @@ std::uint32_t* APS5_VABI sceAgcCbNop_nid_postfix(CommandBuffer* buf, std::uint32 } uint32_t APS5_VABI sceAgcCbNopGetSize(uint32_t size_in_dwords) { - (void)size_in_dwords; - NotImplemented_nid_no_patch(__func__); - return 0; + return size_in_dwords * 4u; } std::uint32_t APS5_VABI sceAgcCbQueueEndOfPipeActionGetSize() { diff --git a/core/libs/prx/libSceAgc/Command/src/Control.cpp b/core/libs/prx/libSceAgc/Command/src/Control.cpp index 88f8d637d..e203a62d1 100644 --- a/core/libs/prx/libSceAgc/Command/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Control.cpp @@ -7,7 +7,7 @@ std::uint32_t* WriteJump(CommandBuffer* buffer, std::uint8_t chain, std::uint8_t CheckBits(chain, 1, function); CheckBits(cachePolicy, 3, function); CheckBits(sizeInDwords, 0xfffffu, function); - CheckAddress(guestAddress, 4, function); + Require((guestAddress & 3u) == 0, function, "misaligned jump target"); return Emit(buffer, 0x3fu, {static_cast(guestAddress) & ~3u, static_cast(guestAddress >> 32u), 0x0f200000u | (static_cast(cachePolicy) << 28u) | (static_cast(chain) << 20u) | sizeInDwords}, function); } diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp index 1578158dc..88f863661 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/DrawIndexed.cpp @@ -32,8 +32,7 @@ std::uint32_t* APS5_VABI sceAgcDcbDrawIndexOffset(CommandBuffer* buf, std::uint3 } uint32_t APS5_VABI sceAgcDcbDrawIndexOffsetGetSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 20; } uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t data_offset_in_bytes, uint64_t modifier) { diff --git a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp index bdc1ac7c4..e93ad3279 100644 --- a/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp +++ b/core/libs/prx/libSceAgc/DcbDraw/src/IndexBuffer.cpp @@ -14,8 +14,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64 } std::uint32_t APS5_VABI sceAgcDcbSetIndexBufferGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 12; } std::uint32_t* APS5_VABI sceAgcDcbSetIndexCount(CommandBuffer* buf, std::uint32_t indexCount) { @@ -33,8 +32,15 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize(CommandBuffer* buf, std::uint8_t } std::uint32_t APS5_VABI sceAgcDcbSetIndexSizeGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 12; +} + +APS5_EXPORT("-KRzWekV120", sceAgcUnknown__MKRzWekV120); +std::uint32_t* APS5_VABI sceAgcUnknown__MKRzWekV120(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy, std::uint8_t perInstanceObjectId) { + Agc::Command::CheckBits(perInstanceObjectId, 1, __func__); + auto* packet = sceAgcDcbSetIndexSize(buf, indexSize, cachePolicy); + packet[2] |= static_cast(perInstanceObjectId) << 14u; + return packet; } std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) { diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index 9e63c69d3..e7271811c 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -42,9 +42,7 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev } std::uint64_t APS5_VABI sceAgcDcbEventWriteGetSize(std::uint8_t eventType) { - (void)eventType; - NotImplemented_nid_no_patch(__func__); - return 0; + return (eventType & 0xfeu) == 0x38u ? 16 : 8; } std::uint32_t* APS5_VABI sceAgcDcbStallCommandBufferParser(CommandBuffer* buf) { diff --git a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp index 519e720bd..de4997d45 100644 --- a/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp @@ -1,33 +1,114 @@ #include "prx/libSceAgc/Misc/include/ShaderFusion.hpp" +#include +#include +#include +#include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" struct SizeAlign { uint64_t m_size; size_t m_align; }; +namespace { + +using namespace ShaderRegs; + +constexpr int GRAPHICS5_ERROR_INVALID_SHADER_HALVES = static_cast(0x8a6c0008u); +constexpr std::uint32_t SPI_SHADER_PGM_CHKSUM_GS = 0x080u; +constexpr std::uint32_t SPI_SHADER_PGM_RSRC1_GS = 0x08Au; +constexpr std::uint32_t SPI_SHADER_PGM_RSRC2_GS = 0x08Bu; +constexpr std::uint32_t SPI_SHADER_PGM_CHKSUM_HS = 0x100u; +constexpr std::uint32_t SPI_SHADER_PGM_RSRC1_HS = 0x10Au; +constexpr std::uint32_t SPI_SHADER_PGM_RSRC2_HS = 0x10Bu; + +bool ValidHalves(const Shader* front, const Shader* back) { + const auto frontType = static_cast(front->type); + const auto backType = static_cast(back->type); + return (frontType == ShaderBinaryType::GsFront && backType == ShaderBinaryType::GsBack) || + (frontType == ShaderBinaryType::HsFront && backType == ShaderBinaryType::HsBack); +} + +ShaderRegister& FindRegister(ShaderRegister* regs, std::uint32_t count, std::uint32_t offset, std::uint32_t occurrence = 0) { + for (std::uint32_t i = 0; regs != nullptr && i < count; ++i) { + if (regs[i].offset == offset && occurrence-- == 0) return regs[i]; + } + throw std::runtime_error("sceAgcUnknownFuseShaderHalves: shader half lacks register " + std::to_string(offset)); +} + +void MergeMax(ShaderRegister& dst, const ShaderRegister& src, std::uint32_t shift, std::uint32_t mask) { + const auto field = std::max((dst.value >> shift) & mask, (src.value >> shift) & mask); + dst.value = (dst.value & ~(mask << shift)) | (field << shift); +} + +} + extern "C" { APS5_EXPORT("fd5Bp5tGTgo", sceAgcUnknownFuseShaderHalves); int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) { - (void)fused_result; - (void)front; - (void)back; - (void)scratch_mem; - NotImplemented_nid_no_patch(__func__); + if (fused_result == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX; + if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; + const bool isGs = static_cast(front->type) == ShaderBinaryType::GsFront; + const std::uint32_t stageBit = isGs ? (1u << 22u) : (1u << 21u); + if (((front->specials->vgt_shader_stages_en.value ^ back->specials->vgt_shader_stages_en.value) & stageBit) != 0) { + return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; + } + *fused_result = *back; + fused_result->type = static_cast(isGs ? ShaderBinaryType::Gs : ShaderBinaryType::Hs); + if (scratch_mem != nullptr) { + auto* regs = static_cast(scratch_mem); + std::memcpy(regs, back->sh_registers, std::size_t{back->num_sh_registers} * sizeof(ShaderRegister)); + fused_result->sh_registers = regs; + } + ShaderRegister* fused = fused_result->sh_registers; + const std::uint32_t fusedCount = fused_result->num_sh_registers; + const std::uint32_t frontCount = front->num_sh_registers; + const std::uint32_t checksum = isGs ? SPI_SHADER_PGM_CHKSUM_GS : SPI_SHADER_PGM_CHKSUM_HS; + for (std::uint32_t occurrence = 0; occurrence < 2; ++occurrence) { + FindRegister(fused, fusedCount, checksum, occurrence).value = FindRegister(front->sh_registers, frontCount, checksum, occurrence).value; + } + const auto& frontRsrc1 = FindRegister(front->sh_registers, frontCount, isGs ? SPI_SHADER_PGM_RSRC1_GS : SPI_SHADER_PGM_RSRC1_HS); + const auto& frontRsrc2 = FindRegister(front->sh_registers, frontCount, isGs ? SPI_SHADER_PGM_RSRC2_GS : SPI_SHADER_PGM_RSRC2_HS); + auto& fusedRsrc1 = FindRegister(fused, fusedCount, isGs ? SPI_SHADER_PGM_RSRC1_GS : SPI_SHADER_PGM_RSRC1_HS); + auto& fusedRsrc2 = FindRegister(fused, fusedCount, isGs ? SPI_SHADER_PGM_RSRC2_GS : SPI_SHADER_PGM_RSRC2_HS); + const std::uint32_t frontVgprs = ((frontRsrc1.value & 0x3fu) + 1u) * 4u; + const std::uint32_t backVgprs = ((fusedRsrc1.value & 0x3fu) + 1u) * 4u; + const std::uint32_t frontTotal = frontVgprs + (frontRsrc2.value >> 28u) * 8u; + const std::uint32_t backTotal = backVgprs + (fusedRsrc2.value >> 28u) * 8u; + const std::uint32_t maxTotal = std::max(frontTotal, backTotal); + const std::uint32_t shared = std::max(frontVgprs, backVgprs) >= maxTotal ? 0u : (maxTotal - std::min(frontTotal, backTotal) + 7u) / 64u; + fusedRsrc2.value = (fusedRsrc2.value & 0x0fffffffu) | ((shared & 0xfu) << 28u); + MergeMax(fusedRsrc1, frontRsrc1, 0, 0x3fu); + if (isGs) { + MergeMax(fusedRsrc1, frontRsrc1, 29, 0x3u); + MergeMax(fusedRsrc2, frontRsrc2, 16, 0x3u); + fusedRsrc2.value = (fusedRsrc2.value & 0xfffbffffu) | (frontRsrc2.value & 0x00040000u); + } else { + MergeMax(fusedRsrc1, frontRsrc1, 28, 0x3u); + } + fusedRsrc2.value = (fusedRsrc2.value & 0xf7ffffc1u) | (frontRsrc2.value & 0x0800003eu); + const std::uint32_t lo = isGs ? SPI_SHADER_PGM_LO_ES : SPI_SHADER_PGM_LO_LS; + auto& loRegister = FindRegister(fused, fusedCount, lo); + auto& hiRegister = FindRegister(fused, fusedCount, lo + 1u); + const auto address = reinterpret_cast(front->code); + loRegister.value = static_cast(address >> 8u); + hiRegister.value = (hiRegister.value & 0xffffff00u) | static_cast((address >> 40u) & 0xffu); + fused_result->user_data = nullptr; return 0; } APS5_EXPORT("dolOmWH+huQ", sceAgcUnknownGetFusedShaderSize); int APS5_VABI sceAgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front, const Shader* back) { - (void)dst; - (void)front; - (void)back; - NotImplemented_nid_no_patch(__func__); + if (dst == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX; + if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES; + dst->m_size = std::uint64_t{back->num_sh_registers} * sizeof(ShaderRegister); + dst->m_align = 4; return 0; } diff --git a/core/libs/prx/libSceAgc/Patch/src/Jump.cpp b/core/libs/prx/libSceAgc/Patch/src/Jump.cpp index da33b9147..e31894dd9 100644 --- a/core/libs/prx/libSceAgc/Patch/src/Jump.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/Jump.cpp @@ -7,14 +7,33 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +void PatchJump(std::uint32_t* cmd, const volatile std::uint32_t* target, std::uint32_t sizeInDwords, const char* function) { + Agc::Command::ValidatePacket(cmd, 0x3fu, 4, function); + const auto address = reinterpret_cast(target); + Agc::Command::CheckAddress(address, 4, function); + Agc::Command::CheckBits(sizeInDwords, 0xfffffu, function); + cmd[1] = static_cast(address); + cmd[2] = static_cast(address >> 32u); + cmd[3] = (cmd[3] & ~0xfffffu) | sizeInDwords; +} + +} + extern "C" { int APS5_VABI sceAgcJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* target, uint32_t size_in_dwords) { - (void)cmd; - (void)target; - (void)size_in_dwords; - NotImplemented_nid_no_patch(__func__); - return 0; + PatchJump(cmd, target, size_in_dwords, __func__); + return 0; +} + +APS5_EXPORT("Ikfdt-rIqCE", sceAgcUnknown_Ikfdt_MrIqCE); +int APS5_VABI sceAgcUnknown_Ikfdt_MrIqCE(uint32_t* cmd, uint64_t cache_policy, const volatile uint32_t* target, uint32_t size_in_dwords) { + Agc::Command::CheckBits(cache_policy, 3u, __func__); + PatchJump(cmd, target, size_in_dwords, __func__); + cmd[3] = (cmd[3] & ~(3u << 28u)) | (static_cast(cache_policy) << 28u); + return 0; } } diff --git a/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp b/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp index 53b2e91ed..7e42fe012 100644 --- a/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/QueueEndOfPipeAction.cpp @@ -30,26 +30,30 @@ int APS5_VABI sceAgcQueueEndOfPipeActionPatchAddress(std::uint32_t* cmd, const v return 0; } -int APS5_VABI sceAgcQueueEndOfPipeActionPatchData(uint32_t* cmd, uint32_t context_id, uint32_t data_sel, uint64_t data) { - (void)cmd; - (void)context_id; - (void)data_sel; - (void)data; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAgcQueueEndOfPipeActionPatchData(uint32_t* cmd, uint64_t data) { + Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__); + if ((cmd[2] >> 29u) == 1u) Agc::Command::CheckBits(data, 0xffffffffu, __func__); + cmd[5] = static_cast(data); + cmd[6] = static_cast(data >> 32u); + return 0; } int APS5_VABI sceAgcQueueEndOfPipeActionPatchGcrCntl(std::uint32_t* cmd, std::uint16_t gcrControl) { - (void)cmd; - (void)gcrControl; - NotImplemented_nid_no_patch(__func__); + Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__); + Agc::Command::CheckBits(gcrControl, 0xfffu, __func__); + std::uint32_t control = gcrControl; + if ((control & 0x300u) == 0x100u) { + control |= 0x200u; + } + cmd[1] = (cmd[1] & ~(0xfffu << 12u)) | (control << 12u); return 0; } int APS5_VABI sceAgcQueueEndOfPipeActionPatchType(std::uint32_t* cmd, std::uint8_t action) { - (void)cmd; - (void)action; - NotImplemented_nid_no_patch(__func__); + Agc::Command::ValidatePacket(cmd, 0x49u, 8, __func__); + Agc::Command::CheckBits(action, 0x3fu, __func__); + const std::uint32_t eventIndex = action >= 0x2fu ? 6u : 5u; + cmd[1] = (cmd[1] & ~0xf3fu) | action | (eventIndex << 8u); return 0; } diff --git a/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp b/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp index fed73340b..06dc2c9b9 100644 --- a/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/Rewind.cpp @@ -2,25 +2,31 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" +#include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -extern "C" { +namespace { -int APS5_VABI sceAgcRewindPatchSetRewindState(uint32_t* cmd, uint8_t state) { - (void)cmd; - (void)state; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceAgcAsyncRewindPatchSetRewindState(std::uint32_t* cmd, std::uint8_t state) { - (void)cmd; - (void)state; - NotImplemented_nid_no_patch(__func__); +int SetRewindState(std::uint32_t* cmd, std::uint8_t state, const char* function) { + Agc::Command::ValidatePacket(cmd, 0x59u, 2, function); + Agc::Command::CheckBits(state, 1, function); + std::atomic_ref(cmd[1]).store(static_cast(state) << 31u, std::memory_order_release); return 0; } } + +extern "C" { + +int APS5_VABI sceAgcRewindPatchSetRewindState(uint32_t* cmd, uint8_t state) { + return SetRewindState(cmd, state, __func__); +} + +int APS5_VABI sceAgcAsyncRewindPatchSetRewindState(std::uint32_t* cmd, std::uint8_t state) { + return SetRewindState(cmd, state, __func__); +} + +} diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index b4b94f6a4..e53a08cb3 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -84,19 +84,6 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory() { return 0; } -APS5_EXPORT("-KRzWekV120", sceAgcUnknown__MKRzWekV120); -// Unnamed DCB state toggle: games call it as (cb, enable, 0, 0) and ignore the result. It sits -// between SetBaseIndirectArgs and SetIndexBuffer in the import table; treated as a no-op. -std::uint32_t* APS5_VABI sceAgcUnknown__MKRzWekV120(void* buf, std::uint32_t enable, std::uint64_t a2, std::uint64_t a3) { - static bool logged = false; - if (!logged) { - logged = true; - std::fprintf(stderr, "[agc] -KRzWekV120 treated as no-op (enable=%u args=0x%llx,0x%llx)" "\n", enable, static_cast(a2), static_cast(a3)); - } - (void)buf; - return nullptr; -} - APS5_EXPORT("6nths4DHNrs", sceAgcUnknown_6nths4DHNrs); int APS5_VABI sceAgcUnknown_6nths4DHNrs() { NotImplemented_nid_no_patch(__func__); @@ -121,12 +108,6 @@ int APS5_VABI sceAgcUnknown_ICkECTBxrMw() { return 0; } -APS5_EXPORT("Ikfdt-rIqCE", sceAgcUnknown_Ikfdt_MrIqCE); -int APS5_VABI sceAgcUnknown_Ikfdt_MrIqCE() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - APS5_EXPORT("RTpj-tIlvZc", sceAgcUnknown_RTpj_MtIlvZc); int APS5_VABI sceAgcUnknown_RTpj_MtIlvZc() { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e97930867..9c712a10c 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -59,6 +59,7 @@ add_library(libSceAgcDriver SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp @@ -157,6 +158,16 @@ target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driv set_target_properties(agc_shader_memory_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_memory_tests) +add_executable(agc_structurizer_tests EXCLUDE_FROM_ALL tests/Structurizer.cpp) +target_link_libraries(agc_structurizer_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_structurizer_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_structurizer_tests) + +add_executable(agc_dpp_write_tests EXCLUDE_FROM_ALL tests/DppWrite.cpp) +target_link_libraries(agc_dpp_write_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_dpp_write_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_dpp_write_tests) + add_executable(agc_driver_flip_tests EXCLUDE_FROM_ALL tests/Flip.cpp ) @@ -219,6 +230,7 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/GuestSamplerResource.cpp tests/TextureDetiler.cpp Graphics/src/Draw.cpp + Graphics/src/DepthSurface.cpp Graphics/src/Pipeline.cpp Graphics/src/State.cpp Graphics/src/ColorTargetLayout.cpp @@ -239,8 +251,11 @@ add_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL Graphics/src/Texture.cpp Graphics/src/UnitShadow.cpp Graphics/src/Sampler.cpp + Graphics/src/Recorder.cpp + Graphics/src/ShaderInputState.cpp ${agcTextureDetileShaderHeader} Execution/src/GuestMemory.cpp + Execution/src/Pm4.cpp ) target_include_directories(agc_driver_graphics_tests PRIVATE @@ -282,7 +297,7 @@ set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) if(BUILD_TESTING) - foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_driver_flip_tests agc_driver_pm4_tests) + foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_dpp_write_tests agc_driver_flip_tests agc_driver_pm4_tests) string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME ${agcTestName} COMMAND ${agcTest}) @@ -327,6 +342,7 @@ if(AGC_BUILD_VISUAL_TEST) Execution/src/GuestMemory.cpp Execution/src/BdaFeatures.cpp Graphics/src/Draw.cpp + Graphics/src/DepthSurface.cpp Graphics/src/ColorTargetLayout.cpp Graphics/src/ColorTargetTransfer.cpp Graphics/src/Pipeline.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp index 79f96d8c1..e26b9c657 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VideoOutput.hpp @@ -40,6 +40,7 @@ class IVideoOutput { public: virtual ~IVideoOutput() = default; virtual std::shared_ptr Reserve(const FlipInfo& info) = 0; + virtual void WaitForFlipRoom() {} virtual std::shared_ptr CaptureRenderingWait(std::uint32_t index) { throw std::runtime_error("VideoOut: rendering waits are unsupported by this output"); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp index c34a57bfa..de0db5a3b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver.cpp @@ -203,6 +203,8 @@ struct Submission { // submission trusts only labels the recorder noted with a newer stamp (see Recorder::NoteLabel). std::uint64_t received = 0; std::chrono::steady_clock::time_point enqueuedAt{}; + const std::uint32_t* rewindTail = nullptr; + std::size_t rewindWords = 0; }; std::uint32_t readRegister(const Registers& registers, std::uint32_t offset) { @@ -524,6 +526,106 @@ private: public: + static void copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words) { + submission.commands.clear(); + std::size_t budget = std::size_t{1} << 26u; + copySegment(submission, guest, words, budget); + } + + static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget) { + require(words <= budget, "command buffer jumps exceed the copy limit (a jump loop?)"); + budget -= words; + for (std::size_t cursor = 0; cursor < words;) { + const auto header = guest[cursor]; + if (Pm4::FillerPacket(header)) { submission.commands.push_back(header); ++cursor; continue; } + const auto count = (header & 0xc0000000u) == 0xc0000000u ? Pm4::PacketWords(header) : words - cursor; + if ((header & 0xc0000000u) != 0xc0000000u || count > words - cursor) { + submission.commands.insert(submission.commands.end(), guest + cursor, guest + words); + return false; + } + const auto opcode = (header >> 8u) & 0xffu; + if (opcode == 0x3fu) { + require(count == 4, "invalid INDIRECT_BUFFER size"); + const auto* target = reinterpret_cast(static_cast(guest[cursor + 1] & ~3u) | (static_cast(guest[cursor + 2] & 0xffffu) << 32u)); + const std::size_t targetWords = guest[cursor + 3] & 0xfffffu; + const bool chain = (guest[cursor + 3] & (1u << 20u)) != 0; + GuestMemory::CheckRange(target, targetWords * sizeof(std::uint32_t), alignof(std::uint32_t)); + if (copySegment(submission, target, targetWords, budget)) return true; + if (chain) return false; + cursor += count; + continue; + } + submission.commands.insert(submission.commands.end(), guest + cursor, guest + cursor + count); + cursor += count; + if (opcode == 0x59u) { + submission.rewindTail = guest + cursor; + submission.rewindWords = words - cursor; + return true; + } + } + return false; + } + + void waitForFlipRoom(const Submission& submission) { + for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) { + if (submission.commands[cursor] != FlipPacketHeader) continue; + std::shared_ptr output; + { + std::lock_guard lock(mutex); + const auto found = outputs.find(submission.commands[cursor + 1]); + require(found != outputs.end(), "flip references an unregistered video output"); + output = found->second; + } + output->WaitForFlipRoom(); + } + } + + void reserveOutputs(Submission& submission) { + for (std::size_t cursor = 0; cursor < submission.commands.size();) { + const auto* words = submission.commands.data() + cursor; + if (words[0] == RenderingWaitPacketHeader) { + const auto output = outputs.find(words[1]); + require(output != outputs.end(), "rendering wait references an unregistered video output"); + auto wait = output->second->CaptureRenderingWait(words[2]); + require(wait != nullptr, "video output returned a null rendering wait"); + submission.renderingWaits.emplace(cursor, std::move(wait)); + } + if (words[0] == FlipPacketHeader) { + const auto output = outputs.find(words[1]); + require(output != outputs.end(), "flip references an unregistered video output"); + const FlipInfo info{words[1], std::bit_cast(words[2]), words[3], std::bit_cast(static_cast(words[4]) | (static_cast(words[5]) << 32u))}; + auto request = output->second->Reserve(info); + require(request != nullptr, "video output returned a null flip reservation"); + submission.flips.emplace(cursor, std::move(request)); + } + cursor += Pm4::PacketWords(words[0]); + } + } + + void executeRewindTail(const Submission& stalled) { + std::atomic_ref control(*const_cast(stalled.rewindTail - 1)); + while ((control.load(std::memory_order_acquire) & 0x80000000u) == 0) { + CheckFailure(); + checkStopping(); + PollSleep(); + } + Submission tail{}; + tail.queue = stalled.queue; + copyCommands(tail, stalled.rewindTail, stalled.rewindWords); + validate(tail.commands, tail.queue, stalled.rewindTail); + waitForFlipRoom(tail); + { + std::lock_guard lock(mutex); + rethrowFailure(); + checkStopping(); + reserveOutputs(tail); + tail.shaders = shaders; + tail.serial = stalled.serial; + tail.received = ++eventSerial; + } + execute(tail); + } + void Submit(const Packet* packet, std::uint32_t queue) { CheckFailure(); require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue"); @@ -538,10 +640,11 @@ public: if (descriptor.dw_num != 0) { require(descriptor.dw_num <= std::numeric_limits::max() / sizeof(std::uint32_t), "command size overflow"); GuestMemory::CheckRange(descriptor.addr, static_cast(descriptor.dw_num) * sizeof(std::uint32_t), alignof(std::uint32_t)); - submission.commands.assign(descriptor.addr, descriptor.addr + descriptor.dw_num); + copyCommands(submission, descriptor.addr, descriptor.dw_num); } const auto copied = profile ? std::chrono::steady_clock::now() : start; validate(submission.commands, queue, descriptor.addr); + waitForFlipRoom(submission); static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr; if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast(descriptor.addr)); const auto validated = profile ? std::chrono::steady_clock::now() : start; @@ -550,25 +653,7 @@ public: rethrowFailure(); checkStopping(); require(accepted != std::numeric_limits::max(), "submission serial overflow"); - for (std::size_t cursor = 0; cursor < submission.commands.size();) { - const auto* words = submission.commands.data() + cursor; - if (words[0] == RenderingWaitPacketHeader) { - const auto output = outputs.find(words[1]); - require(output != outputs.end(), "rendering wait references an unregistered video output"); - auto wait = output->second->CaptureRenderingWait(words[2]); - require(wait != nullptr, "video output returned a null rendering wait"); - submission.renderingWaits.emplace(cursor, std::move(wait)); - } - if (words[0] == FlipPacketHeader) { - const auto output = outputs.find(words[1]); - require(output != outputs.end(), "flip references an unregistered video output"); - const FlipInfo info{words[1], std::bit_cast(words[2]), words[3], std::bit_cast(static_cast(words[4]) | (static_cast(words[5]) << 32u))}; - auto request = output->second->Reserve(info); - require(request != nullptr, "video output returned a null flip reservation"); - submission.flips.emplace(cursor, std::move(request)); - } - cursor += Pm4::PacketWords(words[0]); - } + reserveOutputs(submission); submission.shaders = shaders; submission.serial = accepted + 1; // Every CPU store the game made before this call precedes the stamp; labels recorded @@ -3119,7 +3204,7 @@ private: for (std::uint32_t i = 0; i < userCount; ++i) { userData.push_back(readRegister(queue.shader, 0x240 + i)); } - const auto compute = Graphics::DecodeComputeStageInfo(queue.shader); + auto compute = Graphics::DecodeComputeStageInfo(queue.shader); const std::array memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}}; // The GpuMutex is only taken to create the device: taking it just to copy the pointer made // every dispatch wait behind another queue's whole device phase before its lock-free prologue. @@ -3152,6 +3237,12 @@ private: pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; return; } + if (indirectArguments == 0 && (packet[4] & 0x20u) != 0) { + const std::array threads{packet[1], packet[2], packet[3]}; + for (std::uint32_t axis = 0; axis < 3; ++axis) { + if (threads[axis] % compute.numThreads[axis] != 0) compute.partialThreads = threads; + } + } ShaderRecompiler::RecompileRequest request{ {ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, {(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory}, @@ -3216,6 +3307,7 @@ private: }; mix(address); mix(packet[4] & 0x8000u); + for (const auto threads : compute.partialThreads) mix(threads); for (const auto word : userData) mix(word); // Only the shader registers the request reads (thread counts and RSRC1/2; the program // address is `address`): on queue 0 the bank also holds the graphics stages' registers, @@ -3889,13 +3981,16 @@ private: if (!drawParameters.indirect && !drawParameters.indexed && (drawParameters.indexCount == 0 || drawParameters.instanceCount == 0)) return DrawVerdict::Nothing; static const bool traceIndirect = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr; if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast(drawParameters.indirect->arguments), drawParameters.indirect->count); - // Depth/stencil-only passes (no color writes, no pixel shader) have no effect without depth - // targets, which are not emulated. { const auto targetMask = queue.context.find(0x8e); const auto shaderMask = queue.context.find(0x8f); const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && (targetMask->second & shaderMask->second) != 0; - if (!colorWrites && !queue.shader.contains(0x8)) return DrawVerdict::Nothing; + if (!colorWrites && !queue.shader.contains(0x8)) { + const auto word = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; }; + if ((word(0x200) & 3u) == 0 || ((word(0x010) & 3u) == 0 && (word(0x011) & 1u) == 0)) return DrawVerdict::Nothing; + rejected = "AGC graphics: depth/stencil-only draws without a pixel shader are not implemented"; + return DrawVerdict::Rejected; + } } if (drawParameters.indexed) { const auto restart = queue.userConfig.find(0x24b); @@ -4057,7 +4152,7 @@ private: } append(0x008, 1, Stage::Fragment, 0x00b, 0x00c, Role::Fragment); programs.back().firstUserSgpr = 0; - product->pixel = Graphics::DecodePixelStageInfo(queue.context, graphics.hasColorTarget, graphics.color.componentMapping); + product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(graphics)); return product; }; if (decode == nullptr || VerifyDrawRecipe()) { @@ -5042,6 +5137,23 @@ private: return true; } + void dumpSampleCounters(std::uint64_t address) { + std::uint64_t samples = 0; + { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + auto* recorder = Graphics::Recorder::Active(); + require(recorder != nullptr, "occlusion counters without the command recorder are not implemented"); + recorder->CountSamples(); + samples = Graphics::Recorder::SamplesPassed(); + } + constexpr std::uint64_t ready = 1ull << 63u; + for (std::uint64_t db = 0; db < 16; ++db) { + const std::uint64_t value = ready | (db == 0 ? samples : 0u); + GuestMemory::Write(address + db * 16u, std::as_bytes(std::span(&value, 1)), 8); + } + } + // Before a packet's CPU read of guest memory outside any lock (the group counts of a // DISPATCH_INDIRECT resolved on the CPU): a label this queue still has queued may write those // bytes and the flush hook only knows recorded stores, so the queued labels are recorded first. @@ -5886,11 +5998,12 @@ private: // draw's preparation reads goes through the flush hook. Debug aid: APS5_DRAW_DRAIN=1 restores. static const bool drawDrain = std::getenv("APS5_DRAW_DRAIN") != nullptr; const bool drawPacket = Pm4::DrawOpcode(opcode); + const bool sampleDump = opcode == 0x46 && (packet[1] & 0x3fu) == 0x39u; // Flips no longer drain either: the frame's batches are submitted and the presenter's blit // follows them on the same queue (see Driver::Present). Debug aid: APS5_SYNC_FLIP=1 restores. static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader) - : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x50 || opcode == 0x83 || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); + : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); if (drains) { // The GPU wait happens without the mutex: the batches are submitted under it, the // timeline value is waited for outside (the other workers keep recording), then the @@ -6071,6 +6184,8 @@ private: } }); }); finishDrawPacket(drawn); + } else if (sampleDump) { + dumpSampleCounters(packet[2] | (static_cast(packet[3]) << 32u)); } else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) { if (!wroteOnGpu) Pm4::Execute(packet, queue); } @@ -6090,7 +6205,7 @@ private: static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr; if (!deferredLabels().labels.empty() || Graphics::Recorder::PendingLabelSince().has_value() || Graphics::Recorder::RecordedWorkSinceSubmit() != 0) { auto& costs = submissionCosts(submission.queue); - if (!submitAtEnd && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) { + if (!submitAtEnd && submission.rewindTail == nullptr && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) { ++costs.endSkipped; return; } @@ -6102,6 +6217,7 @@ private: recordDeferredLabels(localDevice.get(), submission.queue); if (localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0); } + if (submission.rewindTail != nullptr) executeRewindTail(submission); } // The calling worker's pending count (QueueWorker::queued), set by run(); null off a worker. diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 561ef3b50..354e53311 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -506,7 +506,9 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) { const auto base = reinterpret_cast(memory.BaseAddress); if (memory.RegionSize > std::numeric_limits::max() - base || base + memory.RegionSize <= cursor) return false; const auto regionEnd = base + memory.RegionSize; - const auto protection = memory.Protect & 0xffu; + std::uint32_t logicalProtection = memory.Protect; + GuestArena::GuestArenaProtection_nid_postfix(cursor, &logicalProtection); + const auto protection = logicalProtection & 0xffu; const bool committed = memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0; const bool readable = committed && readableProtection(protection); const bool writable = readable && writableProtection(protection); @@ -762,9 +764,9 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr; bool dirty = false; while (cursor < stop) { - ULONG_PTR count = tracker.pages.size(); - DWORD granularity = 0; - if (GetWriteWatch(singlePass ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(cursor), static_cast(stop - cursor), tracker.pages.data(), &count, &granularity) != 0) { + std::size_t count = tracker.pages.size(); + DWORD granularity = 4096; + if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, singlePass)) { // Uncommitted pages inside the range make the call fail; such ranges are compared instead. return 0; } @@ -782,9 +784,9 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi // The resetting pass reports pages again, so a write racing the first pass is stamped too. cursor = address & ~(page - 1); while (cursor < stop) { - ULONG_PTR count = tracker.pages.size(); - DWORD granularity = 0; - if (GetWriteWatch(WRITE_WATCH_FLAG_RESET, reinterpret_cast(cursor), static_cast(stop - cursor), tracker.pages.data(), &count, &granularity) != 0) return 0; + std::size_t count = tracker.pages.size(); + DWORD granularity = 4096; + if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast(stop - cursor), tracker.pages.data(), &count, true)) return 0; for (ULONG_PTR i = 0; i < count; ++i) { const auto block = (reinterpret_cast(tracker.pages[i]) - tracker.base) / WriteBlockBytes; tracker.blocks[block] = tracker.generation; @@ -1365,9 +1367,16 @@ void WriteChanged(std::uint64_t address, std::span current, std std::size_t lastChanged = 0; for (std::size_t at = 0; at < size; at += block) { if (!differs(at)) continue; - std::memcpy(destination + at, current.data() + at, std::min(block, size - at)); - firstChanged = std::min(firstChanged, at); - lastChanged = std::min(at + block, size); + const auto blockEnd = std::min(at + block, size); + for (std::size_t run = at; run < blockEnd;) { + if (current[run] == original[run]) { ++run; continue; } + auto runEnd = run + 1; + while (runEnd < blockEnd && current[runEnd] != original[runEnd]) ++runEnd; + std::memcpy(destination + run, current.data() + run, runEnd - run); + firstChanged = std::min(firstChanged, run); + lastChanged = std::max(lastChanged, runEnd); + run = runEnd; + } } // Stamped like a GPU write: a collect memoized for this packet would not see the page fault. if (firstChanged < lastChanged) MarkWritten(address + firstChanged, lastChanged - firstChanged); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index ff230342a..a914189c2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -30,7 +30,11 @@ std::uint64_t address(std::uint32_t low, std::uint32_t high) { std::uint32_t registerOffset(std::uint32_t value) { require(value != 0xffffffffu, "indirect register sentinel semantics are not implemented"); const auto offset = value & ~0x70000000u; - require(offset <= 0xffffu, "extended register semantics are not implemented"); + if (offset > 0xffffu) { + char what[80]; + std::snprintf(what, sizeof(what), "extended register semantics are not implemented (offset dword 0x%08x)", value); + throw std::runtime_error(what); + } return offset; } @@ -175,8 +179,9 @@ std::string_view UnsupportedReason(std::uint32_t header) { case 0x22: return "conditional command execution and conditional flip reservation are not implemented"; case 0x33: case 0x3f: return "nested command buffers, branching and nested flip reservation are not implemented"; case 0x3c: case 0x93: return {}; - case 0x39: case 0x59: + case 0x39: return "cooperative command-queue waits are not implemented"; + case 0x59: return {}; case 0x84: case 0x85: case 0x86: case 0x88: return "separate CE/DE execution and counter synchronization are not implemented"; case 0x49: return {}; @@ -307,6 +312,12 @@ void Validate(std::span packet, std::uint32_t queue) { size(2); require(eventIndex == 0 || eventIndex == 7, "invalid cache-flush event index"); break; + case 0x39: + graphics(); + size(4); + require(eventIndex == 1, "invalid occlusion counter dump event index"); + require(address(packet[2], packet[3]) != 0 && (packet[2] & 7u) == 0, "null or misaligned occlusion counter dump address"); + break; default: throw std::runtime_error("EVENT_WRITE event type " + std::to_string(eventType) + " is not implemented"); } break; @@ -341,6 +352,10 @@ void Validate(std::span packet, std::uint32_t queue) { require(packet.size() - 2 <= 0x10000u - offset, "register range overflow"); break; } + case 0x59: + size(2); + require((packet[1] & 0x7fffffffu) == 0, "unsupported REWIND payload bits"); + break; case 0x3c: case 0x93: { size(opcode == 0x3c ? 7 : 9); require((packet[1] & 0x10u) != 0, "register-space WAIT_REG_MEM is not implemented"); @@ -688,6 +703,7 @@ void Execute(std::span packet, QueueState& queue) { } return; } + case 0x59: break; case 0x3c: case 0x93: { // The waited-on value is written by the CPU or another queue; poll it like the CP would. const auto start = std::chrono::steady_clock::now(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 83bd0b373..b77359bef 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -187,6 +188,7 @@ struct VulkanDevice::State { bool descriptorIndexing = false; bool depthClipControl = false; bool depthClamp = false; + bool occlusionQueryPrecise = false; VkDeviceSize hostImportAlignment = 0; bool depthRangeUnrestricted = false; bool samplerAnisotropy = false; @@ -216,6 +218,7 @@ struct VulkanDevice::State { std::map> computePipelines; std::unique_ptr colorTransfer; std::shared_ptr bufferPool; + std::unique_ptr emptyBuffer; std::unique_ptr textureCache; std::unique_ptr pipelineCache; std::unique_ptr descriptorCache; @@ -489,6 +492,7 @@ struct VulkanDevice::State { // Cached graphics pipelines (with their framebuffers, modules, render passes and layouts) // belong to this device and must be destroyed while it lives. Graphics::ClearCachedPipelines(device); + Graphics::ClearDepthSurfaces(device); { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); @@ -498,6 +502,7 @@ struct VulkanDevice::State { resourceCache.Clear(); Graphics::ClearCachedTextures(device); descriptorCache.reset(); + emptyBuffer.reset(); samplerCache.reset(); textureCache.reset(); detiler.reset(); @@ -773,12 +778,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u // PA_CL_CLIP_CNTL near/far clip disable maps to depth clamping. enabled.depthClamp = available.depthClamp; state->depthClamp = enabled.depthClamp == VK_TRUE; + enabled.occlusionQueryPrecise = available.occlusionQueryPrecise; + state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE; // Recompiled storage-image access declares no format (the guest descriptor decides it). enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat; enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat; // Gathers with non-constant offsets (ImageGatherExtended). enabled.shaderImageGatherExtended = available.shaderImageGatherExtended; if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended); + enabled.shaderResourceMinLod = available.shaderResourceMinLod; + if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod); if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat); if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat); // Bindless image tables index an image array with a wave-uniform runtime slot. @@ -859,6 +868,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u poolInfo.queueFamilyIndex = family; check(state->DeviceFunction("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool"); state->bufferPool = std::make_shared(graphicsContext()); + state->emptyBuffer = std::make_unique(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); state->pipelineCache = std::make_unique(graphicsContext()); state->detiler = std::make_unique(graphicsContext()); state->textureCache = std::make_unique(graphicsContext()); @@ -1691,14 +1701,19 @@ bool NoResidentPresent() { struct FrameDumps { int limit; bool cpu; + int every = 1; int dumped = 0; + std::uint64_t presents = 0; }; FrameDumps& Dumps() { static FrameDumps dumps{[] { const char* value = std::getenv("APS5_DUMP_FRAMES"); return value ? std::atoi(value) : 0; - }(), std::getenv("APS5_NO_GPU_DUMP") != nullptr}; + }(), std::getenv("APS5_NO_GPU_DUMP") != nullptr, [] { + const char* value = std::getenv("APS5_DUMP_FRAMES_EVERY"); + return value ? std::max(std::atoi(value), 1) : 1; + }()}; return dumps; } @@ -1742,7 +1757,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { } auto& dumps = Dumps(); bool dumpFrame = false; - if (dumps.dumped < dumps.limit) { + if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu) { const auto full = ReadDisplayBuffer(buffer); WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); @@ -2149,6 +2164,8 @@ Graphics::Context VulkanDevice::buildContext() const { context.drawIndirectFirstInstance = state->drawIndirectFirstInstance; context.multiDrawIndirect = state->multiDrawIndirect; context.drawIndirectCount = state->drawIndirectCount; + context.occlusionQueryPrecise = state->occlusionQueryPrecise; + context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE; context.copiedWriters = state->copiedWriters.get(); context.functions = state->functionsReady ? &state->deviceFunctions : nullptr; context.descriptorIndexing = state->descriptorIndexing; @@ -2286,6 +2303,68 @@ bool TraceDispatchIo() { return traceIo; } +void WatchMemory(std::uint64_t programAddress) { + struct Range { + std::uint64_t address = 0; + std::uint64_t bytes = 0; + }; + static const std::vector ranges = [] { + std::vector parsed; + const char* text = std::getenv("APS5_WATCH_MEMORY"); + if (text == nullptr) return parsed; + while (true) { + char* end = nullptr; + const auto address = std::strtoull(text, &end, 16); + const auto bytes = *end == ':' ? std::strtoull(end + 1, &end, 16) : 0ull; + if (address == 0 || bytes == 0 || (*end != ',' && *end != '\0')) throw std::runtime_error("APS5_WATCH_MEMORY takes :[,...]"); + parsed.push_back({address, bytes}); + if (*end == '\0') break; + text = end + 1; + } + if (!SyncEachDispatch()) throw std::runtime_error("APS5_WATCH_MEMORY needs APS5_SYNC_DISPATCH=1"); + return parsed; + }(); + if (ranges.empty()) return; + static std::mutex mutex; + static std::deque>> history; + static bool written = false; + std::lock_guard lock(mutex); + if (written) return; + static std::vector addresses; + if (addresses.empty()) { + std::vector found; + for (const auto& range : ranges) { + std::uint64_t address = 0; + for (std::uint64_t high = 0; high < 16 && address == 0; ++high) { + const auto candidate = (range.address & 0xfffffffffull) | (high << 36u); + if (GuestMemory::Accessible(reinterpret_cast(candidate), static_cast(range.bytes))) address = candidate; + } + if (address == 0) return; + found.push_back(address); + } + addresses = std::move(found); + for (std::size_t i = 0; i < ranges.size(); ++i) std::fprintf(stderr, "[watch] watching 0x%llx+0x%llx\n", static_cast(addresses[i]), static_cast(ranges[i].bytes)); + } + std::vector contents; + for (std::size_t i = 0; i < ranges.size(); ++i) { + const auto* bytes = reinterpret_cast(addresses[i]); + if (!GuestMemory::Accessible(bytes, static_cast(ranges[i].bytes))) return; + contents.insert(contents.end(), bytes, bytes + ranges[i].bytes); + } + if (history.empty() || history.back().second != contents) { + history.emplace_back(programAddress, std::move(contents)); + if (history.size() > 64) history.pop_front(); + } + if (!Graphics::LoopGuardTripped()) return; + written = true; + for (std::size_t i = 0; i < history.size(); ++i) { + char name[64]; + std::snprintf(name, sizeof(name), "watch_%02zu_%llx.bin", i, static_cast(history[i].first)); + std::ofstream(name, std::ios::binary).write(reinterpret_cast(history[i].second.data()), static_cast(history[i].second.size())); + } + std::fprintf(stderr, "[watch] loop guard tripped: %zu contents written (watch_*.bin, oldest first)\n", history.size()); +} + // The [dispatch] phases for indirect dispatches alone, every 10 s on an [indirect] line: what the // 'indirect' GpuMutex hold ([lock] line) spends its time on inside the device call (the label // record before it is timed in the driver, [labels] line), with the longest hold seen and the @@ -2858,7 +2937,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { Graphics::Recorder::CountMerged(CommandClass::DispatchLeading); } else { const auto timing = recorder.BeginGpuTiming(CommandClass::DispatchLeading); - Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT); Graphics::Recorder::CountBarriers(CommandClass::DispatchLeading); recorder.EndGpuTiming(timing); ++d.preBarriersRecorded; @@ -2914,7 +2993,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (pushStages != 0 && state->properties.limits.maxPushConstantsSize < Graphics::PipelinePushConstantBytes) { throw std::runtime_error("Vulkan dispatch: compute push constant range exceeds device limit"); } - const auto pushBytes = Graphics::AssemblePushConstants(shaders); + auto pushBytes = Graphics::AssemblePushConstants(shaders); const auto context = graphicsContext(); const auto* limit = state->properties.limits.maxComputeWorkGroupCount; if (arguments == 0 && (x > limit[0] || y > limit[1] || z > limit[2])) { @@ -3044,6 +3123,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec if (resources->Reusable()) insert(); } } + resources->PatchPushConstants(pushBytes); if (profile) timer.add(PhaseResourcesHookWaits, Graphics::Recorder::ThreadWaitedMs() - waitedBefore); // The lookup outcomes of this call: rows with counts on the [indirect] line, and a summary for // the longest call. @@ -3154,6 +3234,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec Graphics::CountLeaseOutcome(false, recorder.Submissions() + 1); } if (TraceDispatchIo()) std::fprintf(stderr, "[dispatch-io] %s:%s\n", groupsText, resources->Describe().c_str()); + WatchMemory(programAddress); // The recipe for the caller's dispatch-cache variant (design_cpu_final M4, rule R3): only an // object the resource cache serves under this content key (reusable: no lease, no copied // writes, every direct region import- or mirror-served), so a hit's proof is the template's @@ -3290,6 +3371,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu timer.phase(PhaseSync); } if (TraceDispatchIo()) std::fprintf(stderr, "[dispatch-io] %s:%s\n", groupsText, hit->resources->Describe().c_str()); + WatchMemory(programAddress); timer.finish(lookupsBefore, groupsText, indirect); if (profile) reportRecipes(); return RecipeOutcome::Recorded; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 32028ea79..301201afc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -5,6 +5,8 @@ namespace AgcDriver::Graphics { +bool LoopGuardTripped(); + class BdaResources { public: explicit BdaResources(const Context& context); @@ -38,6 +40,7 @@ private: // The page table is read-only to the shader, so consecutive builds mapping the same ranges to the // same device addresses share one buffer while one of them is alive (see the table cache in // BdaResources.cpp, which refers to it weakly). + void markWrittenPages() const; std::shared_ptr table; std::unique_ptr fault; std::size_t tableBytes = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index db6ae2af5..155caaeb4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -74,6 +74,8 @@ inline std::uint64_t& DeviceProcLookups() { return count; } +inline constexpr std::size_t EmptyBufferBytes = 16; + struct Context { VkDevice device; VkPhysicalDevice physical; @@ -116,6 +118,8 @@ struct Context { bool drawIndirectFirstInstance = false; bool multiDrawIndirect = false; bool drawIndirectCount = false; + bool occlusionQueryPrecise = false; + VkBuffer emptyBuffer = VK_NULL_HANDLE; // The device's list of recorded dispatches whose copied written buffers await a CPU write-back // (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect // draw whose records one of them produces reads them on the CPU. Null in tests. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp new file mode 100644 index 000000000..7dbb70b89 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -0,0 +1,21 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include + +namespace AgcDriver::Graphics { + +class Texture; + +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +void ClearDepthSurfaces(VkDevice device); +bool DepthSurfaceAt(std::uint64_t address); +std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index aca08e13e..1256823fe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -6,6 +6,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include #include +#include #include #include @@ -123,6 +124,23 @@ struct AddressSpaceStats { }; AddressSpaceStats AddressSpaceCounters(); +struct MirrorStats { + std::uint64_t heapMirrors = 0; + std::uint64_t heapBytes = 0; + std::uint64_t rebuilds = 0; + std::uint64_t blocksCopied = 0; + std::uint64_t heapRefills = 0; +}; +MirrorStats MirrorCounters(); + +struct AddressCopy { + std::uint64_t begin; + std::uint64_t end; + std::uint64_t committed; + const char* reason; +}; +std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit); + class GuestBufferMemory { public: explicit GuestBufferMemory(const Context& context); @@ -163,7 +181,7 @@ public: void Upload(bool addressable); void UploadPrepare(bool addressable); void UploadFinish(bool addressable); - VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes) const; + VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const; std::vector AddressRanges() const; // The BDA table of the cached address space when it serves this upload alone (an address-based // build with no region outside it): its ranges, immutable while the space lives, and the @@ -298,6 +316,7 @@ private: // Records the import-to-buffer copies of the given gpuCopy regions into the open batch, with // the barriers that order them after earlier recorded writes and before the shaders reading them. void recordGpuCopies(std::span copies, bool addressable); + void takeHeapReferences(); Context context; bool stagingAllowed = false; GuestAllocations::Lease lease; @@ -312,6 +331,7 @@ private: // regions follow the registry's order), so AddSnapshot can search instead of scanning. bool regionsSorted = false; std::vector> writes; + std::vector>> heapReferences; // UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran. bool prepared = false; bool uploaded = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index efa0a9514..aebce3e9b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -40,17 +40,12 @@ public: Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - // The framebuffer of these attachment views. A resident target's view (`owners[i]` set) is stable - // while its StorageTexture lives, so framebuffers made of resident views are kept in the pipeline - // and reused when every owner is still the same object; any other set is built per call. std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent); // Begins the render pass on the framebuffer, binds the pipeline and sets viewport and scissor. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const VkViewport& viewport, const VkRect2D& scissor) const; // The same inside a render pass another pipeline of the same attachments began (compatible by // construction: the attachment formats alone decide). void Continue(VkCommandBuffer commands, const VkViewport& viewport, const VkRect2D& scissor) const; - void PushConstants(VkCommandBuffer commands, std::span shaders) const; - // The same from an assembled block (a draw recipe's, made by AssemblePushConstants once). void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const; // Forgets the Vulkan objects without destroying them: for entries of a device that is already gone. void Abandon() noexcept; @@ -69,6 +64,7 @@ private: VkRenderPass renderPass = VK_NULL_HANDLE; VkPipeline pipeline = VK_NULL_HANDLE; std::size_t attachments = 0; + std::size_t colorAttachments = 0; std::vector framebuffers; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 80ea18a88..02503c83c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -165,6 +165,8 @@ public: void FinishUpTo(std::uint64_t serial); // Submits and waits for every batch, running completions in order. void Sync(); + void CountSamples(); + static std::uint64_t SamplesPassed(); // Waits only for the batches up to the newest one that writes the range (submitting the open // batch when it is that one); later batches stay in flight. Fences of one queue signal in // submission order, so completions still run in order. Debug aid: APS5_NO_SYNC_THROUGH=1 syncs all. @@ -486,6 +488,7 @@ private: std::uint32_t queue = 0xffffffffu; std::chrono::steady_clock::time_point submittedAt{}; VkQueryPool queries = VK_NULL_HANDLE; + VkQueryPool samples = VK_NULL_HANDLE; std::vector timedKeys; std::vector timedBytes; // The whole-batch timed range (BatchTimingKey) and its stamps once read (see Completed). @@ -582,6 +585,9 @@ private: bool behindCompletion = false; }; void readGpuTiming(Batch& batch); + void beginSamples(Batch& batch); + void readSamples(Batch& batch); + bool countingSamples = false; // BeginGpuTiming on the open batch without Commands() (RecordStore times its own run, which // Commands() would close). std::uint32_t beginTiming(std::uint64_t key); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 540d73af7..df16329a9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -10,7 +10,7 @@ namespace AgcDriver::Graphics { -ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, bool hasColorTarget, std::uint8_t colorComponentMapping); +ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings); ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader); // A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read: // the draw cache validates them by value with the stage's capture (design_cpu_final rule RD). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 2092a2b68..eaf43678a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -185,6 +185,9 @@ public: // compiled shader's descriptors. A recipe hit refreshes the template iff the two differ. std::uint64_t DataWordsHash() const { return dataWordsHash; } static std::uint64_t DataWordsHash(const CompiledShader& shader); + void PatchPushConstants(std::span bytes) const { + for (const auto& [position, adjustment] : pushPatches) bytes[position] = static_cast(adjustment); + } // Whether RefreshData would record anything for `shader` (the per-word compare; verification). bool DataWordsDiffer(const CompiledShader& shader) const; // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] @@ -237,6 +240,8 @@ private: bool written = true; // Data buffers: the words the buffer holds once the recorded work ran (see RefreshData). std::vector dataWords; + std::uint32_t adjustment = 0; + std::int32_t pushByte = -1; }; struct Binding { @@ -308,7 +313,7 @@ private: void forgetDeferredInputs(); void release() noexcept; void prepareAddressBindings(std::span shaders, std::span snapshots); - VkDescriptorBufferInfo descriptor(const Allocation& allocation) const; + VkDescriptorBufferInfo descriptor(Allocation& allocation); void noteReusable(); void reportDescriptorCaches() const; // What a sampled texture was proved current against when the build (or the last full Revalidate) @@ -400,6 +405,7 @@ private: // FNV-1a offset basis: the hash of no data buffers (DataWordsHash). std::uint64_t dataWordsHash = 14695981039346656037ull; void rehashDataWords(); + std::vector> pushPatches; BuildTiming timing; // Build state carried from stage A to stage B: the bindings in plan order, the image bindings // still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader), diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index fd90957c3..94c91a071 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -40,10 +40,31 @@ struct ColorTarget { // DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp). std::uint64_t dccAddress = 0; bool dccAlphaOnMsb = false; + std::uint64_t surfaceAddress = 0; + VkExtent2D surfaceExtent{}; + std::uint32_t mipCount = 1; + std::uint32_t mip = 0; + bool mipTail = false; +}; + +struct DepthTarget { + std::uint64_t address; + std::uint64_t stencilAddress; + VkExtent2D extent; + VkFormat format; + float clearDepth; + std::uint8_t clearStencil; }; struct State { ShaderStages stages; + std::optional depth; + bool depthTest = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + bool stencilTest = false; + VkStencilOpState stencilFront{}; + VkStencilOpState stencilBack{}; // MRT slot 0; `colors`/`blends` hold every written slot, attachment i being slot i. ColorTarget color; std::vector colors; @@ -64,6 +85,7 @@ struct State { ShaderStages DecodeShaderStages(const QueueState& queue); State DecodeState(const QueueState& queue); +std::array ExportMappings(const State& state); // The message DecodeState (or the pixel stage decode after it) would throw for the register rules // this precheck covers, evaluated without exceptions before the draw is decoded; empty when they // pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict. @@ -99,11 +121,9 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ - // PA_SC_SCREEN_SCISSOR, the window offset/scissor and clip rect, the edge rule, the hardware - // screen offset, CB_TARGET_MASK/CB_SHADER_MASK, the generic and viewport 0 scissors, the - // viewport 0 depth clamp, the blend constants, the viewport 0 transform. - {RegisterBank::Context, 0x00c, 2}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10f, 6}, +inline constexpr std::array DrawKeyRegisters{{ + {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x007, 1}, {RegisterBank::Context, 0x00a, 4}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 4}, + {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 3e42c0c42..6fc5ea467 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -57,6 +57,7 @@ public: // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components); static bool CanCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); ~Texture(); Texture(const Texture&) = delete; @@ -111,7 +112,7 @@ public: // Render targets live in the same images: draws attach mip 0 through a view of the color // buffer's format and mark the image dirty like a storage write. bool Attachable() const { return attachable; } - VkImageView AttachmentView(VkFormat format); + VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0); void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); @@ -403,7 +404,7 @@ private: std::uint32_t defaultMip = 0; std::map extraViews; bool attachable = false; - std::map attachmentViews; + std::map, VkImageView> attachmentViews; VkFormat storageFormat = VK_FORMAT_UNDEFINED; // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 1b8b03525..22bc67a98 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -1,5 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include +#include #include #include #include @@ -120,8 +122,8 @@ std::uint64_t hashRanges(const std::vector& ran BdaResources::BdaResources(const Context& context) { static_assert(std::endian::native == std::endian::little); - Require(sizeof(ShaderRecompiler::BdaAbi::Fault) <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); - fault = std::make_unique(context, sizeof(ShaderRecompiler::BdaAbi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); + fault = std::make_unique(context, ShaderRecompiler::BdaAbi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memset(fault->Bytes().data(), 0, fault->Bytes().size()); } @@ -134,7 +136,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo Require(ranges.size() <= std::numeric_limits::max(), "BDA table range count overflow"); Require(ranges.size() <= (std::numeric_limits::max() - sizeof(ShaderRecompiler::BdaAbi::Header)) / sizeof(ShaderRecompiler::BdaAbi::Range), "BDA table size overflow"); tableBytes = sizeof(ShaderRecompiler::BdaAbi::Header) + ranges.size() * sizeof(ShaderRecompiler::BdaAbi::Range); - Require(tableBytes <= context.limits.maxStorageBufferRange && sizeof(ShaderRecompiler::BdaAbi::Fault) <= context.limits.maxStorageBufferRange, "BDA descriptors exceed storage buffer range limit"); + Require(tableBytes <= context.limits.maxStorageBufferRange && ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA descriptors exceed storage buffer range limit"); auto& cache = Tables(); std::uint64_t hash = 0; if (tableCacheEnabled()) { @@ -222,10 +224,19 @@ VkDescriptorBufferInfo BdaResources::Table() const { } VkDescriptorBufferInfo BdaResources::Fault() const { - return {fault->Handle(), 0, sizeof(ShaderRecompiler::BdaAbi::Fault)}; + return {fault->Handle(), 0, ShaderRecompiler::BdaAbi::FaultBufferBytes}; +} + +namespace { +std::atomic loopGuardTripped{false}; +} + +bool LoopGuardTripped() { + return loopGuardTripped.load(std::memory_order_relaxed); } void BdaResources::CheckFault() const { + markWrittenPages(); ShaderRecompiler::BdaAbi::Fault report{}; std::memcpy(&report, fault->Bytes().data(), sizeof(report)); if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) { @@ -236,13 +247,44 @@ void BdaResources::CheckFault() const { Require(report.reason != ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, "rect-list requires finite nondegenerate axis-aligned positions with equal positive W"); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::LoopLimit) { // APS5_LOOP_GUARD: the shader left a loop that ran past the guard; the dispatch result is kept. - std::fprintf(stderr, "[gpu] loop guard: the loop exit at pc 0x%x ran past %u evaluations\n", report.instruction, report.bytes); + loopGuardTripped.store(true, std::memory_order_relaxed); + std::fprintf(stderr, "[gpu] loop guard: the loop exit at pc 0x%x of shader 0x%llx ran past %u evaluations\n", report.instruction, static_cast(report.address), report.bytes); std::memset(fault->Bytes().data(), 0, fault->Bytes().size()); return; } std::ostringstream message; message << "BDA access failed: address=0x" << std::hex << report.address << " instruction=0x" << report.instruction << std::dec << " bytes=" << report.bytes << " stage=" << report.stage << " reason=" << static_cast(report.reason); + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Permission && table != nullptr) { + const auto bytes = table->Bytes(); + ShaderRecompiler::BdaAbi::Header header{}; + std::memcpy(&header, bytes.data(), sizeof(header)); + for (std::uint32_t index = 0; index < header.count; ++index) { + ShaderRecompiler::BdaAbi::Range range{}; + std::memcpy(&range, bytes.data() + sizeof(header) + index * sizeof(range), sizeof(range)); + if (report.address < range.begin || report.address >= range.end) continue; + message << std::hex << "; the store hit 0x" << range.begin << "+0x" << range.end - range.begin << ", read-only in the BDA table: stores through GPU-selected descriptors reach only writable ranges imported in place, not ones served by a mirror or a copy (past APS5_HOST_IMPORT_MIB, or refused by the driver)"; + break; + } + } throw std::runtime_error(message.str()); } } + +namespace AgcDriver::Graphics { + +void BdaResources::markWrittenPages() const { + namespace Abi = ShaderRecompiler::BdaAbi; + auto* words = reinterpret_cast(fault->Bytes().data()); + Require(words[Abi::WrittenOverflowWord] == 0, "more than " + std::to_string(Abi::WrittenPageSlots) + " pages stored to through GPU-selected buffer descriptors in one use are not implemented"); + bool any = false; + for (std::uint32_t slot = 0; slot < Abi::WrittenPageSlots; ++slot) { + const auto page = words[Abi::WrittenSlotsWord + slot]; + if (page == 0) continue; + GuestMemory::MarkWritten(static_cast(page - 1u) << Abi::WrittenPageShift, std::size_t{1} << Abi::WrittenPageShift); + any = true; + } + if (any) std::memset(words + Abi::WrittenSlotsWord, 0, Abi::WrittenPageSlots * sizeof(std::uint32_t)); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp index f9d4c41e9..4c74d01bd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp @@ -32,9 +32,11 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height, require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "AGC graphics: unsupported color element size"); std::uint32_t paddedHeight = height; switch (mode) { - case ColorTileMode::Linear: - require(width * bytesPerElement % 256u == 0, "AGC graphics: linear surface pitch requires a width aligned to 256 bytes"); + case ColorTileMode::Linear: { + const auto pitchAlignment = 256u / bytesPerElement; + pitch = (width + pitchAlignment - 1u) / pitchAlignment * pitchAlignment; break; + } case ColorTileMode::RenderTarget: { // SW_64KB_R_X: 64 KiB blocks of 2^(16 - log2(bpe)) elements, wider than tall for odd powers. const auto log2Elements = 16u - static_cast(std::countr_zero(bytesPerElement)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp index 9d9d7d346..36690adc4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetTransfer.cpp @@ -9,7 +9,7 @@ void ReadColorTarget(const ColorTarget& target, std::span destination PerformanceTimer timing("ColorTarget.Read"); const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes); Require(target.bytes == layout.Bytes() && destination.size() == layout.LinearBytes(), "color target transfer size mismatch"); - if (target.tileMode == ColorTileMode::Linear) { + if (target.tileMode == ColorTileMode::Linear && layout.Bytes() == layout.LinearBytes()) { GuestMemory::Read(target.address, destination, layout.Alignment()); return; } @@ -25,7 +25,7 @@ void WriteColorTarget(const ColorTarget& target, std::span sour PerformanceTimer timing("ColorTarget.Write"); const ColorTargetLayout layout(target.extent.width, target.extent.height, target.tileMode, target.elementBytes); Require(target.bytes == layout.Bytes() && source.size() == layout.LinearBytes(), "color target transfer size mismatch"); - if (target.tileMode == ColorTileMode::Linear) { + if (target.tileMode == ColorTileMode::Linear && layout.Bytes() == layout.LinearBytes()) { GuestMemory::Write(target.address, source, layout.Alignment()); return; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp new file mode 100644 index 000000000..f8ae28b76 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -0,0 +1,172 @@ +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +class DepthSurface { +public: + DepthSurface(const Context& context, const DepthTarget& target) : context(context), target(target) { + this->context.bufferPool.reset(); + VkFormatProperties properties{}; + context.formatProperties(context.physical, target.format, &properties); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be an attachment on this device"); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0, "depth/stencil format " + std::to_string(target.format) + " cannot be sampled on this device"); + Require(target.extent.width <= context.limits.maxFramebufferWidth && target.extent.height <= context.limits.maxFramebufferHeight, "depth target exceeds framebuffer limits"); + const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = target.format; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth target"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = target.format; + viewInfo.subresourceRange = {aspects, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth"); + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = image; + toGeneral.subresourceRange = viewInfo.subresourceRange; + const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); + barrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + const VkClearDepthStencilValue clear{target.clearDepth, target.clearStencil}; + context.Function("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &toGeneral.subresourceRange); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + if (batch) batch->SubmitAndWait(); + else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + } catch (...) { + release(); + throw; + } + } + ~DepthSurface() { release(); } + DepthSurface(const DepthSurface&) = delete; + DepthSurface& operator=(const DepthSurface&) = delete; + + std::shared_ptr Sampled(std::span words, const GuestTextureResource& resource, VkComponentMapping components) { + std::array key{}; + std::copy_n(words.begin(), std::min(words.size(), 8), key.begin()); + key[8] = components.r; + key[9] = components.g; + key[10] = components.b; + key[11] = components.a; + if (const auto found = textures.find(key); found != textures.end()) return found->second; + const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; + const auto format = ResolveTextureFormat(resource.format); + const bool depthBits = !stencil && !d16 && words.size() >= 4 && ShaderRecompiler::IsDepthBitsTexture(words[1], words[3]); + if ((format != expected && !depthBits) || resource.dimension != TextureDimension::k2D || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0) { + char text[448]; + std::snprintf(text, sizeof(text), "AGC graphics: sampling the %s plane of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), tile mode %u, dimension %d, levels %u-%u, slice %u is not implemented (T# %08x %08x %08x %08x %08x %08x %08x %08x)", + stencil ? "stencil" : "depth", static_cast(target.address), target.extent.width, target.extent.height, static_cast(target.format), resource.width, resource.height, resource.format, static_cast(format), + static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseLevel, resource.lastLevel, resource.baseArray, key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7]); + throw std::runtime_error(text); + } + auto texture = std::make_shared(context, image, target.format, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components); + textures.emplace(key, texture); + return texture; + } + + const Context context; + const DepthTarget target; + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + +private: + std::map, std::shared_ptr> textures; + + void release() noexcept { + textures.clear(); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + view = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; + } +}; + +bool sameSurface(const DepthTarget& a, const DepthTarget& b) { + return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format; +} + +std::mutex& surfacesMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& surfaces() { + static auto* list = new std::vector>(); + return *list; +} + +} + +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { + std::lock_guard lock(surfacesMutex()); + for (const auto& surface : surfaces()) { + if (surface->context.device == context.device && sameSurface(surface->target, target)) return surface->view; + } + surfaces().push_back(std::make_unique(context, target)); + return surfaces().back()->view; +} + +void ClearDepthSurfaces(VkDevice device) { + std::lock_guard lock(surfacesMutex()); + std::erase_if(surfaces(), [&](const auto& surface) { return surface->context.device == device; }); +} + +std::shared_ptr DepthSurfaceTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(surfacesMutex()); + const auto& list = surfaces(); + const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { + return surface->context.device == context.device && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress)); + }); + return found == list.rend() ? nullptr : (*found)->Sampled(words, resource, components); +} + +bool DepthSurfaceAt(std::uint64_t address) { + std::lock_guard lock(surfacesMutex()); + return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->target.address == address || surface->target.stencilAddress == address; }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 73e9dfa33..8b46b0812 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" @@ -41,15 +42,16 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) { // The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X). GuestTextureResource SurfaceForTarget(const ColorTarget& color) { Require(color.tileMode == ColorTileMode::RenderTarget, "only 64 KiB tiled color targets are resident"); + const bool chain = color.mipCount > 1; GuestTextureResource surface{}; - surface.baseAddress = color.address; - surface.width = color.extent.width; - surface.height = color.extent.height; + surface.baseAddress = chain ? color.surfaceAddress : color.address; + surface.width = chain ? color.surfaceExtent.width : color.extent.width; + surface.height = chain ? color.surfaceExtent.height : color.extent.height; surface.depthOrLastArray = 0; surface.baseArray = 0; - surface.mipCount = 1; + surface.mipCount = color.mipCount; surface.baseLevel = 0; - surface.lastLevel = 0; + surface.lastLevel = color.mipCount - 1; surface.tileMode = TextureTileMode::kR64KBX; surface.dimension = TextureDimension::k2D; surface.format = GuestFormatFor(color.format, color.elementBytes); @@ -500,6 +502,7 @@ void CheckBufferAliases(std::span shaders, const ColorTarg const ShaderRecompiler::ShaderBufferResource descriptor{{words[offset], words[offset + 1], words[offset + 2], words[offset + 3]}}; const auto address = descriptor.Base48(); const auto size = descriptor.GetSize(); + if (size == 0 || address == 0) continue; Require(!overlap(address, size, target.address, target.bytes), "shader buffer aliases the render target"); Require(!overlap(address, size, indexAddress, indexBytes), "writable shader buffer aliases the index buffer"); } @@ -1135,8 +1138,8 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (continued) { record.pipeline->Continue(commands, state.viewport, state.scissor); } else { - const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); + const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr); countBarrier(1); APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, true, *record.indirect, scratch, argumentBuffer, argumentOffset, countBarrier); @@ -1154,7 +1157,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); if (record.pushBytes != nullptr) record.pipeline->PushConstants(commands, record.pushStages, *record.pushBytes); - else record.pipeline->PushConstants(commands, shaders); + else { + auto bytes = AssemblePushConstants(shaders); + resources.PatchPushConstants(bytes); + record.pipeline->PushConstants(commands, PushConstantStages(shaders), bytes); + } APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset); if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten); @@ -1281,15 +1288,16 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.resident = refreshResidentTarget(state, color, outcome, profile, [&] { auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); Require(resident->Attachable(), "storage format cannot be a color attachment"); - Require(resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); + Require(color.mipCount > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); return resident; }); } if (binding.resident != nullptr) { timer.phase(PhaseReadTarget); - targetViews.push_back(binding.resident->AttachmentView(color.format)); + targetViews.push_back(binding.resident->AttachmentView(color.format, color.mip)); continue; } + Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); if (binding.gpuTiling) { binding.mip = ColorTargetMip(color, colorLayout); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); @@ -1319,6 +1327,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends[index].blendEnable != 0); targetViews.push_back(binding.target->View()); } + if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth)); timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); @@ -1463,7 +1472,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable()) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -1479,7 +1488,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& recipe->passKey = passKey; recipe->vertexInput = inputs.vertexInput; recipe->pushStages = PushConstantStages(shaders); - if (recipe->pushStages != 0) recipe->pushBytes = AssemblePushConstants(shaders); + if (recipe->pushStages != 0) { + recipe->pushBytes = AssemblePushConstants(shaders); + resources->PatchPushConstants(recipe->pushBytes); + } recipe->masked = masked; recipe->fragmentOutputs = inputs.fragmentOutputs; recipe->shaderStages = inputs.shaderStages; @@ -1536,6 +1548,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& upload.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &upload, 0, nullptr, 0, nullptr); countBarrier(); + if (state.depth) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT); + countBarrier(); + } APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded"); if (gpuIndirect) rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier); for (auto& binding : targets) { @@ -1564,7 +1580,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); resources->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->Layout()); APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); - pipeline->PushConstants(commands, shaders); + auto pushBytes = AssemblePushConstants(shaders); + resources->PatchPushConstants(pushBytes); + pipeline->PushConstants(commands, PushConstantStages(shaders), pushBytes); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset); if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 7213c9085..8b8846c77 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -40,6 +40,8 @@ struct ImageMirror { std::uint64_t base = 0; std::uint64_t bytes = 0; bool writable = false; + bool heap = false; + std::vector generations; std::weak_ptr range; std::shared_ptr buffer; // Writable ranges only: the guest bytes the mirror holds, compared on refresh and at write-back. @@ -306,8 +308,11 @@ struct ImageMirrors { VkDevice device = VK_NULL_HANDLE; std::map> entries; std::set failed; + std::uint64_t heapBytes = 0; // APS5_PROFILE_DRAW counters, reported every 10 s as [buffers] image mirrors. std::uint64_t builds = 0; + std::uint64_t heapRefills = 0; + std::uint64_t heapUnwatched = 0; std::uint64_t subranges = 0; std::uint64_t rebuilds = 0; std::uint64_t refreshes = 0; @@ -361,6 +366,20 @@ bool mirrorsEnabled() { return !disabled; } +std::uint64_t heapMirrorBudget() { + static const std::uint64_t bytes = [] { + const char* value = std::getenv("APS5_HEAP_MIRROR_MIB"); + return (value ? std::strtoull(value, nullptr, 10) : 24576ull) << 20u; + }(); + return bytes; +} + +[[noreturn]] void heapMirrorFatal(const GuestAllocations::Range& range, std::uint64_t held, const char* reason) { + std::fprintf(stderr, "FATAL: heap mirror of 0x%llx+0x%llx: %s; heap mirrors hold %llu MiB of %llu MiB (APS5_HEAP_MIRROR_MIB)\n", static_cast(range.address), static_cast(range.bytes), reason, static_cast(held >> 20u), static_cast(heapMirrorBudget() >> 20u)); + std::fflush(stderr); + std::abort(); +} + bool sameRange(const std::weak_ptr& mirrored, const std::shared_ptr& range) { return !mirrored.owner_before(range) && !range.owner_before(mirrored); } @@ -370,13 +389,18 @@ struct RefreshBlock { ImageMirror* mirror; std::uint64_t address; std::size_t length; + std::uint64_t generation = 0; }; // Compares a block's guest bytes with the mirror's shadow and copies a changed block into the device -// buffer and the shadow. Blocks are distinct, so blocks of one refresh can be compared in parallel. bool compareBlock(const RefreshBlock& block) { const auto offset = static_cast(block.address - block.mirror->base); const auto* guest = reinterpret_cast(block.address); + if (block.mirror->heap) { + std::memcpy(block.mirror->buffer->Bytes().data() + offset, guest, block.length); + block.mirror->generations[static_cast(block.address / 65536 - block.mirror->base / 65536)] = block.generation; + return true; + } if (std::memcmp(block.mirror->shadow.data() + offset, guest, block.length) == 0) return false; std::memcpy(block.mirror->buffer->Bytes().data() + offset, guest, block.length); std::memcpy(block.mirror->shadow.data() + offset, guest, block.length); @@ -480,9 +504,7 @@ private: // completes first, so its results reach the guest and the shadow before the compare and no batch // writes a block being replaced; GPU reads of the mirror by earlier recorded work (an address-based // dispatch whose lease is released at completion) see the newer bytes, as they would on hardware. -// Returns whether there is anything to compare. -bool prepareRefresh(ImageMirror& mirror, std::uint64_t address, std::uint64_t bytes) { - if (!mirror.writable || bytes == 0) return false; +void prepareRange(std::uint64_t address, std::uint64_t bytes) { // The compare reads the pages directly; a mirror is only reused while its Range exists, but a // lease of a build in flight can keep a replaced Range alive past a protection change. Require(GuestMemory::Accessible(reinterpret_cast(address), static_cast(bytes)), "image mirror range became inaccessible"); @@ -498,6 +520,11 @@ bool prepareRefresh(ImageMirror& mirror, std::uint64_t address, std::uint64_t by recorder->Sync(); } ++state.refreshes; +} + +bool prepareRefresh(ImageMirror& mirror, std::uint64_t address, std::uint64_t bytes) { + if ((!mirror.writable && !mirror.heap) || bytes == 0) return false; + prepareRange(address, bytes); return true; } @@ -528,11 +555,41 @@ void refreshMirror(ImageMirror& mirror, std::uint64_t address, std::uint64_t byt compareBlocks(blocks); } +void refreshHeapMirrors(std::vector& mirrors, std::vector& blocks) { + constexpr std::uint64_t block = 65536; + std::sort(mirrors.begin(), mirrors.end(), [](const ImageMirror* left, const ImageMirror* right) { return left->base < right->base; }); + thread_local std::vector changed; + for (std::size_t first = 0; first < mirrors.size();) { + auto last = first + 1; + while (last < mirrors.size() && mirrors[last]->base == mirrors[last - 1]->base + mirrors[last - 1]->bytes) ++last; + const auto begin = mirrors[first]->base; + const auto bytes = mirrors[last - 1]->base + mirrors[last - 1]->bytes - begin; + prepareRange(begin, bytes); + const auto generation = GuestMemory::CollectWrites(begin, static_cast(bytes)); + Require(generation != 0, "a heap mirror's range is no longer write-watched"); + for (auto index = first; index < last; ++index) { + auto& mirror = *mirrors[index]; + changed.assign(mirror.generations.size(), 0); + GuestMemory::ChangedBlocks(mirror.base, static_cast(mirror.bytes), mirror.generations, changed); + const auto aligned = mirror.base / block * block; + const auto end = mirror.base + mirror.bytes; + bool refilled = false; + for (std::size_t at = 0; at < changed.size(); ++at) { + if (changed[at] == 0) continue; + const auto from = std::max(mirror.base, aligned + at * block); + blocks.push_back({&mirror, from, static_cast(std::min(end, aligned + (at + 1) * block) - from), generation}); + refilled = true; + } + if (refilled) ++Mirrors().heapRefills; + } + first = last; + } +} + // The mirror for a leased image range: the existing one, or a new one when none exists or the // registered Range object changed (a protection change). Null when the range cannot be mirrored. An // existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every -// mirror of the build (prepareRefresh ran); a new mirror is filled here. -std::shared_ptr acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks) { +std::shared_ptr acquireMirror(const Context& context, const std::shared_ptr& range, std::vector& blocks, bool heap) { auto& state = Mirrors(); std::shared_ptr mirror; { @@ -540,26 +597,42 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh if (state.device != context.device) { state.entries.clear(); state.failed.clear(); + state.heapBytes = 0; state.device = context.device; } if (state.failed.contains(range->address)) return nullptr; const auto found = state.entries.find(range->address); - if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->bytes == range->bytes && found->second->writable == range->writable) mirror = found->second; + if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->bytes == range->bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second; } if (mirror != nullptr) { - if (prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes); + if (!heap && prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes); return mirror; } - // The registered flags were taken from the pages; a range that disagrees is copied by this build. - if (!GuestMemory::Accessible(reinterpret_cast(range->address), range->bytes, range->writable)) return nullptr; + if (!GuestMemory::Accessible(reinterpret_cast(range->address), range->bytes, range->writable && !heap)) return nullptr; mirror = std::make_shared(); mirror->base = range->address; mirror->bytes = range->bytes; mirror->writable = range->writable; + mirror->heap = heap; mirror->range = range; + if (heap) { + prepareRefresh(*mirror, range->address, range->bytes); + const auto generation = GuestMemory::CollectWrites(range->address, range->bytes); + constexpr std::uint64_t block = 65536; + mirror->generations.assign(static_cast(((range->address + range->bytes + block - 1) / block) - range->address / block), generation); + std::lock_guard lock(state.mutex); + if (generation == 0) { + ++state.heapUnwatched; + return nullptr; + } + std::uint64_t held = state.heapBytes; + if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) held -= found->second->bytes; + if (held + range->bytes > heapMirrorBudget()) heapMirrorFatal(*range, held, "past the budget"); + } try { mirror->buffer = std::make_shared(context, range->bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); } catch (const std::runtime_error& error) { + if (heap) heapMirrorFatal(*range, state.heapBytes, error.what()); std::fprintf(stderr, "[gpu] image mirror of 0x%llx+0x%llx failed: %s; falling back to copies\n", static_cast(range->address), static_cast(range->bytes), error.what()); std::lock_guard lock(state.mutex); state.failed.insert(range->address); @@ -571,17 +644,17 @@ std::shared_ptr acquireMirror(const Context& context, const std::sh const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::MirrorRefresh); GuestMemory::Read(range->address, mirror->buffer->Bytes()); } - if (range->writable) mirror->shadow.assign(mirror->buffer->Bytes().begin(), mirror->buffer->Bytes().end()); + if (range->writable && !heap) mirror->shadow.assign(mirror->buffer->Bytes().begin(), mirror->buffer->Bytes().end()); std::lock_guard lock(state.mutex); ++state.rebuilds; mirror->serial = (1ull << 63u) | ++state.serials; // A replaced mirror lives on in the regions of recorded work that bound it. + if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) state.heapBytes -= found->second->bytes; + if (heap) state.heapBytes += mirror->bytes; state.entries[range->address] = mirror; return mirror; } -// The mirror containing [begin, end) whose registered range still exists, or null. A mirror whose -// Range was replaced is only rebuilt by the next address-based build; until then the range is copied. std::shared_ptr findMirror(const Context& context, std::uint64_t begin, std::uint64_t end) { auto& state = Mirrors(); std::lock_guard lock(state.mutex); @@ -590,7 +663,7 @@ std::shared_ptr findMirror(const Context& context, std::uint64_t be if (found == state.entries.begin()) return nullptr; --found; const auto& mirror = found->second; - if (begin < mirror->base || end > mirror->base + mirror->bytes || mirror->range.expired()) return nullptr; + if (mirror->heap || begin < mirror->base || end > mirror->base + mirror->bytes || mirror->range.expired()) return nullptr; return mirror; } @@ -598,7 +671,14 @@ std::shared_ptr findMirror(const Context& context, std::uint64_t be void sweepMirrors() { auto& state = Mirrors(); std::lock_guard lock(state.mutex); - for (auto it = state.entries.begin(); it != state.entries.end();) it = it->second->range.expired() ? state.entries.erase(it) : std::next(it); + for (auto it = state.entries.begin(); it != state.entries.end();) { + if (!it->second->range.expired()) { + ++it; + continue; + } + if (it->second->heap) state.heapBytes -= it->second->bytes; + it = state.entries.erase(it); + } } void reportMirrors() { @@ -610,16 +690,20 @@ void reportMirrors() { state.lastReport = now; std::size_t count = 0; std::size_t writable = 0; + std::size_t heaps = 0; std::uint64_t bytes = 0; + std::uint64_t heapBytes = 0; { std::lock_guard lock(state.mutex); for (const auto& [base, mirror] : state.entries) { ++count; bytes += mirror->bytes; if (mirror->writable) ++writable; + if (mirror->heap) ++heaps; } + heapBytes = state.heapBytes; } - std::fprintf(stderr, "[buffers] image mirrors: %zu ranges (%.1f MiB, %zu writable), %llu address-based builds served, %llu descriptor sub-ranges bound, %llu rebuilds, %llu refreshes: %llu blocks compared, %llu copied, %llu refresh syncs\n", count, bytes / 1048576.0, writable, static_cast(state.builds), static_cast(state.subranges), static_cast(state.rebuilds), static_cast(state.refreshes), static_cast(state.blocksCompared), static_cast(state.blocksCopied), static_cast(state.syncs)); + std::fprintf(stderr, "[buffers] image mirrors: %zu ranges (%.1f MiB, %zu writable, %zu heap %.1f MiB), %llu address-based builds served, %llu descriptor sub-ranges bound, %llu rebuilds, %llu refreshes: %llu blocks compared, %llu copied, %llu refresh syncs; heap refills %llu, unwatched heaps %llu\n", count, bytes / 1048576.0, writable, heaps, heapBytes / 1048576.0, static_cast(state.builds), static_cast(state.subranges), static_cast(state.rebuilds), static_cast(state.refreshes), static_cast(state.blocksCompared), static_cast(state.blocksCopied), static_cast(state.syncs), static_cast(state.heapRefills), static_cast(state.heapUnwatched)); } // Deferred lease release. The lease an address-based build takes (AcquireRegistered) is dropped by its @@ -778,6 +862,30 @@ AddressSpaceStats AddressSpaceCounters() { return {addressSpaceCacheEnabled(), load(cache.hits), load(cache.rebuiltFirst), load(cache.rebuiltGeneration), load(cache.rebuiltEpoch), load(cache.rebuiltDevice), load(cache.rebuiltWaiterDrop), load(cache.unpublished), load(cache.dissolvedOverlap), load(cache.dissolvedImports), load(cache.waiterDrops)}; } +std::string AddressCopyOverflow(std::vector copies, std::uint64_t limit) { + std::uint64_t total = 0; + for (const auto& copy : copies) total += copy.committed; + if (total <= limit) return {}; + std::sort(copies.begin(), copies.end(), [](const AddressCopy& left, const AddressCopy& right) { return left.committed > right.committed; }); + char text[256]; + std::snprintf(text, sizeof(text), "an address-based build copies %llu MiB of %zu registered ranges, past %llu MiB (APS5_ADDRESS_COPY_MAX_MIB); the largest:", static_cast(total >> 20u), copies.size(), static_cast(limit >> 20u)); + std::string message = text; + for (std::size_t index = 0; index < copies.size() && index < 8; ++index) { + const auto& copy = copies[index]; + std::snprintf(text, sizeof(text), " 0x%llx+0x%llx (%.1f MiB committed, %s)", static_cast(copy.begin), static_cast(copy.end - copy.begin), copy.committed / 1048576.0, copy.reason); + message += text; + } + return message; +} + +MirrorStats MirrorCounters() { + auto& state = Mirrors(); + std::lock_guard lock(state.mutex); + MirrorStats stats{0, state.heapBytes, state.rebuilds, state.blocksCopied, state.heapRefills}; + for (const auto& [base, mirror] : state.entries) stats.heapMirrors += mirror->heap ? 1 : 0; + return stats; +} + const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes) { if (context.hostImportAlignment == 0 || bytes == 0 || bytes > std::numeric_limits::max() - address) return nullptr; auto& state = Imports(); @@ -859,6 +967,7 @@ void GuestBufferMemory::AcquireRegistered() { // APS5_NO_SORTED_SNAPSHOT_LOOKUP=1: AddSnapshot scans the regions as before instead of searching. static const bool sortedLookup = std::getenv("APS5_NO_SORTED_SNAPSHOT_LOOKUP") == nullptr; std::vector blocks; + std::vector heaps; bool mirrored = false; auto& spaces = Spaces(); // Read before the lease below is acquired: a mutation ending in between leaves the space @@ -890,7 +999,8 @@ void GuestBufferMemory::AcquireRegistered() { for (const auto& region : space->base) { if (region.mirror == nullptr) continue; mirrored = true; - if (region.mirror->writable && prepareRefresh(*region.mirror, region.begin, region.end - region.begin)) appendBlocks(blocks, *region.mirror, region.begin, region.end - region.begin); + if (region.mirror->heap) heaps.push_back(region.mirror.get()); + else if (region.mirror->writable && prepareRefresh(*region.mirror, region.begin, region.end - region.begin)) appendBlocks(blocks, *region.mirror, region.begin, region.end - region.begin); } lap(timing.mirrorsUs, at); } @@ -914,6 +1024,7 @@ void GuestBufferMemory::AcquireRegistered() { // The import lock is taken per range: a mirror's preparation may wait for recorded work, which // must not happen under it. regions.reserve(lease.size()); + std::vector copies; for (const auto& range : lease) { if (!range->readable) continue; validate(range->address, range->bytes); @@ -932,20 +1043,36 @@ void GuestBufferMemory::AcquireRegistered() { regions.push_back(std::move(region)); continue; } - if (!range->releasable && mirrorsEnabled()) { - // Main image: served by a persistent mirror (see ImageMirror); the writable ones are - // compared with guest memory below, all at once. - region.mirror = acquireMirror(context, range, blocks); + if (mirrorsEnabled()) { + region.mirror = acquireMirror(context, range, blocks, range->releasable); lap(timing.mirrorsUs, at); if (region.mirror != nullptr) { mirrored = true; + if (region.mirror->heap) heaps.push_back(region.mirror.get()); regions.push_back(std::move(region)); continue; } } addCopiedRange({range->address, range->address + range->bytes, range->writable}); + const auto& copied = regions.back(); + std::uint64_t committed = copied.end - copied.begin; + if (copied.sparse) { + committed = 0; + for (const auto& [first, last] : copied.backed) committed += last - first; + } + const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : "outside the write-watched arena"; + copies.push_back({copied.begin, copied.end, committed, reason}); } regionsSorted = sortedLookup; + static const std::uint64_t copyLimit = [] { + const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB"); + return (value ? std::strtoull(value, nullptr, 10) : 64ull) << 20u; + }(); + if (const auto overflow = AddressCopyOverflow(std::move(copies), copyLimit); !overflow.empty()) { + std::fprintf(stderr, "FATAL: %s\n", overflow.c_str()); + std::fflush(stderr); + std::abort(); + } lap(timing.importsUs, at); if (addressSpaceCacheEnabled()) { // Published only while the imports the regions point at stand: the table entries below @@ -991,6 +1118,8 @@ void GuestBufferMemory::AcquireRegistered() { } } } + refreshHeapMirrors(heaps, blocks); + lap(timing.mirrorsUs, at); const auto copiedBefore = Mirrors().blocksCopied; compareBlocks(blocks); lap(timing.compareUs, at); @@ -1018,7 +1147,7 @@ void GuestBufferMemory::addCopiedRange(const CopiedRange& range) { // cannot write them, so that equals a snapshot taken now without the extra copy. Reserved heaps // are registered whole while the guest commits pages on demand: committed pages only. region.hostBacked = !range.writable; - auto committed = GuestMemory::DescribeCommitted(range.begin, range.end - range.begin, range.writable); + auto committed = GuestMemory::DescribeCommitted(range.begin, range.end - range.begin); if (!committed.whole) { region.sparse = true; region.backed = std::move(committed.ranges); @@ -1094,11 +1223,9 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b // the shader stores to them; what is written back is decided by Writes() alone. Region region{address, address + bytes, true, {}, nullptr}; region.atomic = atomic; - auto committed = GuestMemory::DescribeCommitted(address, bytes, true); + auto committed = GuestMemory::DescribeCommitted(address, bytes); if (!committed.whole) { // A GPU heap bound whole while the guest commits its pages on demand, or a descriptor left - // pointing at unmapped memory: only committed pages are copied in and stored back. Shaders - // must not touch the rest, which reads as zeros. region.sparse = true; region.backed = std::move(committed.ranges); } @@ -1670,6 +1797,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { if (profile) copyUs += microsecondsSince(copyStart); } if (!gpuCopies.empty()) recordGpuCopies(gpuCopies, addressable); + takeHeapReferences(); if (!profile) return; const auto loopUs = microsecondsSince(loopStart); readUs.fetch_add(refreshUs, std::memory_order_relaxed); @@ -1927,7 +2055,7 @@ void GuestBufferMemory::RecordCopyBacks(Recorder& recorder) { recorder.EndGpuTiming(timing, copiedBytes); } -VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std::size_t bytes) const { +VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const { Require(uploaded && !committed, "guest GPU memory is not available"); Require(bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest buffer view"); const auto* found = owner(address); @@ -1936,17 +2064,20 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std: Require(address >= region.begin && address + bytes <= region.end && (region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr), "guest buffer view exceeds its GPU owner"); const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin; const auto offset = address - base; - Require(context.limits.minStorageBufferOffsetAlignment != 0 && offset % context.limits.minStorageBufferOffsetAlignment == 0, "guest buffer view violates storage buffer offset alignment"); - Require(bytes <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit"); + Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment"); + adjustment = static_cast(offset % context.limits.minStorageBufferOffsetAlignment); + Require(adjustment % 4 == 0, "guest buffer view off the storage buffer offset alignment is not DWORD aligned"); + Require(bytes + adjustment <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit"); const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? region.mirror->buffer->Handle() : region.buffer->Handle(); - return {handle, offset, bytes}; + return {handle, offset - adjustment, bytes + adjustment}; } ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) { Require(region.buffer != nullptr || region.direct != nullptr || region.mirror != nullptr, "incomplete guest GPU upload"); const auto address = region.direct != nullptr ? region.direct->address + (region.begin - region.direct->base) : region.mirror != nullptr ? region.mirror->buffer->DeviceAddress() + (region.begin - region.mirror->base) : region.buffer->DeviceAddress(); Require(region.end - region.begin <= std::numeric_limits::max() - address, "GPU address range overflow"); - return {region.begin, region.end, address, ShaderRecompiler::BdaAbi::Read, 0}; + const auto permissions = ShaderRecompiler::BdaAbi::Read | (region.direct != nullptr && region.writable ? ShaderRecompiler::BdaAbi::Write : 0u); + return {region.begin, region.end, address, permissions, 0}; } std::vector GuestBufferMemory::AddressRanges() const { @@ -1996,14 +2127,59 @@ void GuestBufferMemory::MarkDirectWrites() const { } } -void GuestBufferMemory::WriteBack() { - Require(uploaded && !committed, "guest memory cannot be committed twice or before upload"); +namespace { + +void storeChanged(std::uint64_t address, std::span current, std::span original) { + const auto writable = GuestMemory::DescribeCommitted(address, current.size(), true); + if (writable.whole) { + GuestMemory::WriteChanged(address, current, original); + return; + } + const auto unchanged = [&](std::uint64_t from, std::uint64_t to) { + const auto at = static_cast(from - address); + if (std::memcmp(current.data() + at, original.data() + at, static_cast(to - from)) == 0) return; + char message[192]; + std::snprintf(message, sizeof(message), "the GPU changed guest memory 0x%llx+0x%llx that is not writable (a direct memory page shared by several views; aliased writes are not implemented)", static_cast(from), static_cast(to - from)); + throw std::runtime_error(message); + }; + auto cursor = address; + for (const auto& [first, last] : writable.ranges) { + if (cursor < first) unchanged(cursor, first); + const auto at = static_cast(first - address); + const auto length = static_cast(last - first); + GuestMemory::WriteChanged(first, current.subspan(at, length), original.subspan(at, length)); + cursor = last; + } + if (cursor < address + current.size()) unchanged(cursor, address + current.size()); +} + +std::vector> mergeWrites(std::vector> writes) { std::sort(writes.begin(), writes.end()); std::vector> merged; for (const auto& range : writes) { if (!merged.empty() && range.first < merged.back().second) merged.back().second = std::max(merged.back().second, range.second); else merged.push_back(range); } + return merged; +} + +} + +void GuestBufferMemory::takeHeapReferences() { + heapReferences.clear(); + for (const auto& [begin, end] : mergeWrites(writes)) { + const auto* found = owner(begin); + if (found == nullptr || found->mirror == nullptr || !found->mirror->heap || !found->mirror->writable) continue; + Require(end <= found->end, "written range exceeds its heap mirror"); + const auto& mirror = *found->mirror; + const auto bytes = mirror.buffer->Bytes().subspan(static_cast(begin - mirror.base), static_cast(end - begin)); + heapReferences.emplace_back(begin, std::vector(bytes.begin(), bytes.end())); + } +} + +void GuestBufferMemory::WriteBack() { + Require(uploaded && !committed, "guest memory cannot be committed twice or before upload"); + const auto merged = mergeWrites(writes); // The CPU keeps running while the GPU works, so storing whole ranges would roll back its writes to // bytes the shader never touched. Only runs that differ from the uploaded copy are stored. for (const auto& [begin, end] : merged) { @@ -2026,6 +2202,12 @@ void GuestBufferMemory::WriteBack() { const auto offset = static_cast(begin - mirror.base); const auto length = static_cast(end - begin); const auto current = mirror.buffer->Bytes().subspan(offset, length); + if (mirror.heap) { + const auto reference = std::find_if(heapReferences.begin(), heapReferences.end(), [&](const auto& entry) { return entry.first == begin; }); + Require(reference != heapReferences.end() && reference->second.size() == length, "heap mirror write-back has no reference"); + storeChanged(begin, current, reference->second); + continue; + } GuestMemory::WriteChanged(begin, current, std::span(mirror.shadow).subspan(offset, length)); std::memcpy(mirror.shadow.data() + offset, current.data(), length); continue; @@ -2058,7 +2240,7 @@ void GuestBufferMemory::WriteBack() { if (!region.sparse) { const auto first = static_cast(begin - region.begin); const auto length = static_cast(end - begin); - GuestMemory::WriteChanged(begin, region.buffer->Bytes().subspan(first, length), std::span(region.uploaded).subspan(first, length)); + storeChanged(begin, region.buffer->Bytes().subspan(first, length), std::span(region.uploaded).subspan(first, length)); continue; } std::size_t packed = 0; @@ -2067,7 +2249,7 @@ void GuestBufferMemory::WriteBack() { const auto to = std::min(last, end); if (from < to) { const auto length = static_cast(to - from); - GuestMemory::WriteChanged(from, region.buffer->Bytes().subspan(static_cast(from - region.begin), length), std::span(region.uploaded).subspan(packed + static_cast(from - first), length)); + storeChanged(from, region.buffer->Bytes().subspan(static_cast(from - region.begin), length), std::span(region.uploaded).subspan(packed + static_cast(from - first), length)); } packed += static_cast(last - first); } @@ -2075,6 +2257,7 @@ void GuestBufferMemory::WriteBack() { committed = true; lease.clear(); space.reset(); + heapReferences.clear(); } std::optional>> GuestBufferMemory::DirectRegions() const { @@ -2082,16 +2265,15 @@ std::optional>> GuestBufferM std::vector> result; if (space != nullptr) { for (const auto& region : space->base) { - if (region.direct == nullptr && (region.mirror == nullptr || region.mirror->writable)) return std::nullopt; + if (region.direct == nullptr && (region.mirror == nullptr || region.mirror->writable || region.mirror->heap)) return std::nullopt; result.emplace_back(region.begin, region.end); } } for (const auto& region : regions) { // A read-only mirror is as fixed as an import: its bytes and VkBuffer never change while its - // Range exists (see ImageMirrorSerial). A writable one is refreshed and written back per build. // A staged region is keyed by its import like one bound in place: the import is the source // of its copy-in and the destination of its copy-back, both recorded per use. - const bool fixedMirror = region.mirror != nullptr && !region.mirror->writable; + const bool fixedMirror = region.mirror != nullptr && !region.mirror->writable && !region.mirror->heap; const bool staged = region.gpuCopy && region.deviceLocal; if (region.direct == nullptr && !fixedMirror && !staged) return std::nullopt; if (staged) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3cd364eae..a18b2f49a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -103,7 +103,6 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(!prtDefColor, "guest texture descriptor uses a partially resident default color which is not implemented"); Require(arrayPitch == 0, "guest texture descriptor uses a nonzero array pitch which is not implemented"); Require(!msaaDepth, "guest texture descriptor uses MSAA which is not implemented"); - // DCC block sizes only apply with metadata compression, which is rejected above. static_cast(maxUncompBlkSize); static_cast(maxCompBlkSize); // Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index af75014dd..efca469c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -41,7 +41,7 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::min(viewport.y, viewport.y + viewport.height) >= context.limits.viewportBoundsRange[0] && std::max(viewport.y, viewport.y + viewport.height) <= context.limits.viewportBoundsRange[1], "viewport Y exceeds device bounds"); } -Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size()) { +Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()) { // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep // the buffer pool, which is reset with the device, alive past it. this->context.bufferPool.reset(); @@ -105,6 +105,20 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = static_cast(references.size()); subpass.pColorAttachments = references.empty() ? nullptr : references.data(); + const VkAttachmentReference depthReference{static_cast(colors.size()), VK_IMAGE_LAYOUT_GENERAL}; + if (state.depth) { + VkAttachmentDescription depth{}; + depth.format = state.depth->format; + depth.samples = VK_SAMPLE_COUNT_1_BIT; + depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + depth.stencilStoreOp = VK_ATTACHMENT_STORE_OP_STORE; + depth.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + depth.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + colors.push_back(depth); + subpass.pDepthStencilAttachment = &depthReference; + } VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; passInfo.attachmentCount = static_cast(colors.size()); passInfo.pAttachments = colors.empty() ? nullptr : colors.data(); @@ -138,6 +152,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput raster.lineWidth = 1; VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencil.depthTestEnable = state.depthTest; + depthStencil.depthWriteEnable = state.depthWrite; + depthStencil.depthCompareOp = state.depthCompare; + depthStencil.stencilTestEnable = state.stencilTest; + depthStencil.front = state.stencilFront; + depthStencil.back = state.stencilBack; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; blend.attachmentCount = static_cast(state.blends.size()); blend.pAttachments = state.blends.empty() ? nullptr : state.blends.data(); @@ -155,6 +176,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput pipelineInfo.pViewportState = &viewports; pipelineInfo.pRasterizationState = &raster; pipelineInfo.pMultisampleState = &samples; + pipelineInfo.pDepthStencilState = state.depth ? &depthStencil : nullptr; pipelineInfo.pColorBlendState = &blend; pipelineInfo.pDynamicState = &dynamic; pipelineInfo.layout = layout; @@ -202,7 +224,7 @@ VkPipelineLayout Pipeline::Layout() const { } std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent) { - Require(targets.size() == attachments && targets.size() == owners.size(), "render targets do not match the pipeline's color attachments"); + Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments"); const bool resident = std::all_of(owners.begin(), owners.end(), [](const auto& owner) { return owner != nullptr; }); if (resident) { // Entries whose views are gone can never match again and go as soon as no recorded draw holds @@ -254,13 +276,6 @@ void Pipeline::Continue(VkCommandBuffer commands, const VkViewport& viewport, co context.Resolved(&DeviceFunctions::cmdSetScissor, "vkCmdSetScissor")(commands, 0, 1, &scissor); } -void Pipeline::PushConstants(VkCommandBuffer commands, std::span shaders) const { - const auto stages = PushConstantStages(shaders); - if (stages == 0) return; - const auto bytes = AssemblePushConstants(shaders); - context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data()); -} - void Pipeline::PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span bytes) const { if (stages == 0) return; context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data()); @@ -322,6 +337,16 @@ std::vector pipelineKey(const Context& context, const State& state, c for (const auto value : state.blendConstants) append(key, value); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); + append(key, state.depth.has_value()); + if (state.depth) { + append(key, state.depth->format); + append(key, state.depthTest); + append(key, state.depthWrite); + append(key, state.depthCompare); + append(key, state.stencilTest); + append(key, state.stencilFront); + append(key, state.stencilBack); + } append(key, state.stages.mesh.has_value()); if (state.stages.mesh) { const auto& mesh = *state.stages.mesh; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index f796220fd..df4ac9cee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -166,6 +166,7 @@ std::atomic& WaitersOf(std::uint64_t id) { return unlockedWaiters[id % Wait std::mutex liveRecordersMutex; std::vector liveRecorders; std::atomic nextRecorderId{1}; +std::atomic samplesPassed{0}; bool RecorderAlive(std::uint64_t id) { std::lock_guard lock(liveRecordersMutex); @@ -1304,7 +1305,7 @@ void Recorder::endOpenRenderPass() { context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(open->commands); // The pass's attachment and shader writes are visible to everything recorded after it (the // host sees them at the batch's fence). - recordBarrier(open->commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + recordBarrier(open->commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); CountBarriers(CommandClass::Draw); EndGpuTiming(pass.timing); open->coveredAccess = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; @@ -1405,6 +1406,7 @@ void Recorder::ensureOpen() { throw; } batch->queue = GuestMemory::GpuLockThreadTag(); + if (countingSamples) beginSamples(*batch); open = std::move(batch); // The whole-batch range: its start stamp waits for the previous batches like any // bottom-of-pipe stamp, so it marks when this batch's execution began. @@ -1788,6 +1790,34 @@ void Recorder::EndGpuTiming(std::uint32_t index, std::uint64_t bytes) { context.Function("vkCmdWriteTimestamp")(open->commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, open->queries, index * 2 + 1); } +void Recorder::CountSamples() { + GuestMemory::AssertGpuLockHeld("Recorder::CountSamples"); + countingSamples = true; + if (open == nullptr || open->samples != VK_NULL_HANDLE) return; + if (open->renderPass.open) endOpenRenderPass(); + beginSamples(*open); +} + +std::uint64_t Recorder::SamplesPassed() { + return samplesPassed.load(std::memory_order_acquire); +} + +void Recorder::beginSamples(Batch& batch) { + VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + info.queryType = VK_QUERY_TYPE_OCCLUSION; + info.queryCount = 1; + Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &batch.samples), "vkCreateQueryPool occlusion"); + context.Function("vkCmdResetQueryPool")(batch.commands, batch.samples, 0, 1); + context.Function("vkCmdBeginQuery")(batch.commands, batch.samples, 0, context.occlusionQueryPrecise ? VK_QUERY_CONTROL_PRECISE_BIT : 0u); +} + +void Recorder::readSamples(Batch& batch) { + if (batch.samples == VK_NULL_HANDLE || !batch.submitted) return; + std::uint64_t samples = 0; + Check(context.Function("vkGetQueryPoolResults")(context.device, batch.samples, 0, 1, sizeof(samples), &samples, sizeof(samples), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults occlusion"); + samplesPassed.fetch_add(samples, std::memory_order_acq_rel); +} + void Recorder::readGpuTiming(Batch& batch) { if (batch.queries == VK_NULL_HANDLE || batch.timedKeys.empty()) return; std::vector stamps(batch.timedKeys.size() * 2); @@ -2327,6 +2357,7 @@ void Recorder::Submit() { GuestMemory::AssertGpuLockHeld("Recorder::Submit"); if (!open->keyStores.empty()) recordKeyStores(false); if (open->renderPass.open) endOpenRenderPass(); + if (open->samples != VK_NULL_HANDLE) context.Function("vkCmdEndQuery")(open->commands, open->samples, 0); const bool hostReadCovered = (open->run.open || !open->run.queued.empty()) && closeStoreRun(true); if (BarrierValidate()) validateBatchEnds.fetch_add(1, std::memory_order_relaxed); if (open->hostReadOwed && !hostReadCovered) { @@ -2819,6 +2850,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source) { } } readGpuTiming(*batch); + readSamples(*batch); if (batch->serial != 0) { std::lock_guard ringLock(completedMutex); auto& entry = completed[batch->serial % completed.size()]; @@ -2893,6 +2925,8 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source) { void Recorder::release(Batch& batch) noexcept { if (batch.queries != VK_NULL_HANDLE) context.Function("vkDestroyQueryPool")(context.device, batch.queries, nullptr); batch.queries = VK_NULL_HANDLE; + if (batch.samples != VK_NULL_HANDLE) context.Function("vkDestroyQueryPool")(context.device, batch.samples, nullptr); + batch.samples = VK_NULL_HANDLE; // A completed (or never submitted) batch's objects are kept for reuse: the fence is signaled or // untouched, so resetting it cannot block, and the command buffer is no longer pending. if (batch.commands != VK_NULL_HANDLE && batch.fence != VK_NULL_HANDLE && spare.size() < 64 && function(resetFences, "vkResetFences")(context.device, 1, &batch.fence) == VK_SUCCESS) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index f39491ce0..cbbd53b0c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -81,6 +81,7 @@ std::span Buffer::Bytes() { } void Buffer::Invalidate() { + Require(mapping != nullptr, "cannot invalidate an unmapped GPU buffer"); VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE}; range.memory = memory; range.size = VK_WHOLE_SIZE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 8077e8882..895fa359d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -32,8 +32,12 @@ void ShaderResources::prepareAddressBindings(std::span sha } } -VkDescriptorBufferInfo ShaderResources::descriptor(const Allocation& allocation) const { - if (allocation.guest) return guestMemory.Descriptor(allocation.address, allocation.size); +VkDescriptorBufferInfo ShaderResources::descriptor(Allocation& allocation) { + if (allocation.guest) return guestMemory.Descriptor(allocation.address, allocation.size, allocation.adjustment); + if (allocation.role == ShaderRecompiler::DescriptorRole::GuestBuffers) { + Require(context.emptyBuffer != VK_NULL_HANDLE, "no placeholder buffer for a null V#"); + return {context.emptyBuffer, 0, allocation.size}; + } if (allocation.role == ShaderRecompiler::DescriptorRole::BdaPagetable || allocation.role == ShaderRecompiler::DescriptorRole::FaultBuffer) { Require(bda != nullptr, "BDA descriptors have no memory owner"); return allocation.role == ShaderRecompiler::DescriptorRole::BdaPagetable ? bda->Table() : bda->Fault(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index e69fe0fb8..a45d6ee4b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -84,7 +84,7 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& }; } -ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, bool hasColorTarget, std::uint8_t colorComponentMapping) { +ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings) { const auto inControl = read(context, spiPsInControl, RegisterBank::Context); const auto inputNum = inControl & 0x3Fu; if (inputNum > 32u) { @@ -117,11 +117,6 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; const auto zOrder = (shaderControl >> 4u) & 0x3u; - std::array targetExportMapping{}; - targetExportMapping.fill(0xe4u); - if (hasColorTarget) { - targetExportMapping[0] = colorComponentMapping; - } return ShaderRecompiler::ShaderPixelStageInfo{ inputNum, interpolatorSettings, @@ -142,7 +137,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, ((shaderControl >> 10u) & 0x1u) != 0, targetOutputMode, - targetExportMapping + exportMappings }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d84bf94de..95e471b7a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include @@ -15,6 +16,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libc/include/General.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" +#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include #include @@ -262,6 +264,12 @@ std::shared_ptr sampledStorageSource(const Context& context, con // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0) { CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); + if (auto depth = DepthSurfaceTexture(context, words, resource, components)) return depth; + if (words.size() >= 4 && ShaderRecompiler::IsDepthBitsTexture(words[1], words[3])) { + char text[160]; + std::snprintf(text, sizeof(text), "AGC graphics: 32-bit integer read of the depth-layout texture 0x%llx, which is no depth surface drawn with, is not implemented", static_cast(resource.baseAddress)); + throw std::runtime_error(text); + } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; const bool profile = LookupOutcomes::Profiled(); const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; @@ -474,6 +482,11 @@ std::array SurfaceKey(const Context& context, const GuestTextu // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes) { + if (DepthSurfaceAt(resource.baseAddress)) { + char text[112]; + std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(resource.baseAddress)); + throw std::runtime_error(text); + } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; if (disabled) return std::make_shared(context, *context.detiler, resource, mip); static_cast(words); @@ -812,7 +825,14 @@ void ShaderResources::buildPrepare(std::span shaders, cons // vector) counts as written, like imageWritten below. const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element]; const bool atomic = element < binding.bufferAtomic.size() && binding.bufferAtomic[element]; - item.allocations.push_back(addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic)); + const auto index = addGuestBuffer(std::span(binding.guestDescriptor).subspan(static_cast(element) * 4, 4), target, indexAddress, indexBytes, written, atomic); + const auto& push = shader.program->pushConstants; + if (!push.empty()) { + const auto position = shader.program->memoryOffsetDword * 4u + element; + Require(position < push.size(), "guest buffer offset lies outside the shader's push constants"); + allocations[index].pushByte = static_cast(shader.pushConstantOffset + position); + } + item.allocations.push_back(index); } } else if (addressRole) { item.allocations.push_back(allocations.size()); @@ -950,6 +970,11 @@ void ShaderResources::buildComplete() { } context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")(context.device, static_cast(writes.size()), writes.data(), 0, nullptr); } + for (const auto& allocation : allocations) { + if (allocation.adjustment == 0) continue; + Require(allocation.pushByte >= 0, "a guest buffer off the storage buffer offset alignment in a shader whose shader data is a buffer is not implemented"); + pushPatches.emplace_back(static_cast(allocation.pushByte), allocation.adjustment); + } timing.descriptorsMs += phase(BuildPhase::Descriptors); noteReusable(); completed = true; @@ -1999,8 +2024,10 @@ std::size_t ShaderResources::addGuestBuffer(std::span words Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type"); const auto address = descriptor.Base48(); const auto byteSize = descriptor.GetSize(); - Require(address != 0, "null shader buffer descriptor address"); - Require(byteSize != 0, "empty shader buffer descriptor"); + if (byteSize == 0 || address == 0) { + allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false}); + return allocations.size() - 1; + } Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit"); Require(byteSize <= std::numeric_limits::max(), "shader buffer size exceeds host address space"); const auto size = static_cast(byteSize); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index dcc5a608e..828f1e626 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -79,8 +79,100 @@ bool IgnoreDepthTest() { return ignore; } -// DB_DEPTH_CONTROL: depth and stencil tests whose function is ALWAYS and that write no depth cannot -// change color output, so they are accepted without a depth target (stencil writes are dropped). +bool depthSurfaceBound(const Registers& cx) { + const auto z = find(cx, 0x010); + const auto stencil = find(cx, 0x011); + return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0); +} + +VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint32_t refMask, bool readOnly) { + VkStencilOpState face{}; + face.compareOp = static_cast(compare); + face.compareMask = (refMask >> 8u) & 0xffu; + face.writeMask = readOnly ? 0u : (refMask >> 16u) & 0xffu; + const auto test = refMask & 0xffu; + const auto opValue = refMask >> 24u; + auto reference = test; + auto fixed = compare == 0 || compare == 7 ? 0u : face.compareMask; + const auto convert = [&](std::uint32_t op) { + if (face.writeMask == 0) return VK_STENCIL_OP_KEEP; + char detail[96]; + switch (op) { + case 0: return VK_STENCIL_OP_KEEP; + case 1: return VK_STENCIL_OP_ZERO; + case 2: + case 3: + case 4: { + const auto value = op == 2 ? 0xffu : op == 3 ? test : opValue; + if (((reference ^ value) & fixed & face.writeMask) != 0) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil replacement 0x%02x against test value 0x%02x (masks 0x%02x/0x%02x) is unsupported", value, test, face.compareMask, face.writeMask); + throw std::runtime_error(detail); + } + reference = (reference & ~face.writeMask) | (value & face.writeMask); + fixed |= face.writeMask; + return VK_STENCIL_OP_REPLACE; + } + case 5: + case 6: + case 8: + case 9: + if (opValue != 1) { + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil add/subtract of 0x%02x is unsupported", opValue); + throw std::runtime_error(detail); + } + return op == 5 ? VK_STENCIL_OP_INCREMENT_AND_CLAMP : op == 6 ? VK_STENCIL_OP_DECREMENT_AND_CLAMP : op == 8 ? VK_STENCIL_OP_INCREMENT_AND_WRAP : VK_STENCIL_OP_DECREMENT_AND_WRAP; + case 7: return VK_STENCIL_OP_INVERT; + default: + std::snprintf(detail, sizeof(detail), "AGC graphics: stencil operation %u is unsupported", op); + throw std::runtime_error(detail); + } + }; + face.failOp = convert(ops & 0xfu); + face.passOp = convert((ops >> 4u) & 0xfu); + face.depthFailOp = convert((ops >> 8u) & 0xfu); + face.reference = reference; + return face; +} + +void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) { + zero(cx, 0x000, 0x00001f9fu, "depth/stencil clear, copy, resummarize or decompress draws (DB_RENDER_CONTROL)"); + const auto view = read(cx, 0x002); + zero(cx, 0x002, ~0x03000000u, "depth array slices or mips (DB_DEPTH_VIEW)"); + zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); + const auto zFormat = read(cx, 0x010) & 3u; + const bool stencil = (read(cx, 0x011) & 1u) != 0; + Require(zFormat != 2, "Z_24 depth is unsupported"); + Require(zFormat != 0 || (depthControl & 2u) == 0, "depth test without a depth plane"); + Require(stencil || (depthControl & 1u) == 0, "stencil test without a stencil plane"); + const auto base = [&](std::uint32_t low, std::uint32_t highOffset) { + const auto high = find(cx, highOffset); + return (high == cx.end() ? 0ull : static_cast(high->second & 0xffu) << 40u) | (static_cast(read(cx, low)) << 8u); + }; + const bool depthReadOnly = (view & 0x01000000u) != 0; + const bool stencilReadOnly = (view & 0x02000000u) != 0; + DepthTarget depth{}; + depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; + depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0; + Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); + Require(!stencil || stencilReadOnly || base(0x015, 0x01d) == depth.stencilAddress, "stencil read and written at different addresses is unsupported"); + const auto size = read(cx, 0x007); + depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u}; + depth.format = zFormat == 1 ? (stencil ? VK_FORMAT_D16_UNORM_S8_UINT : VK_FORMAT_D16_UNORM) : (stencil ? VK_FORMAT_D32_SFLOAT_S8_UINT : VK_FORMAT_D32_SFLOAT); + depth.clearDepth = readFloat(cx, 0x00b); + depth.clearStencil = static_cast(read(cx, 0x00a) & 0xffu); + result.depth = depth; + result.depthTest = (depthControl & 2u) != 0; + result.depthWrite = result.depthTest && (depthControl & 4u) != 0 && !depthReadOnly; + result.depthCompare = static_cast((depthControl >> 4u) & 7u); + result.stencilTest = (depthControl & 1u) != 0; + if (result.stencilTest) { + const auto ops = read(cx, 0x10b); + result.stencilFront = stencilFace((depthControl >> 8u) & 7u, ops, read(cx, 0x10c), stencilReadOnly); + result.stencilBack = (depthControl & 0x80u) != 0 ? stencilFace((depthControl >> 20u) & 7u, ops >> 12u, read(cx, 0x10d), stencilReadOnly) : result.stencilFront; + } +} + bool depthPassThrough(std::uint32_t depthControl) { const bool stencil = (depthControl & 1u) != 0; const bool depth = (depthControl & 2u) != 0; @@ -129,44 +221,47 @@ VkBlendOp blendOp(std::uint32_t value) { struct DecodedColorFormat { VkFormat format; std::uint32_t elementBytes; + std::uint8_t componentMapping = 0xe4u; }; // CB_COLOR_INFO FORMAT / NUMBER_TYPE / COMP_SWAP to a Vulkan attachment format. AMD formats list // components from the least significant bits, as Vulkan's non-packed formats do. DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) { - constexpr std::uint32_t unorm = 0, sint = 5, srgb = 6, floating = 7; + constexpr std::uint32_t unorm = 0, snorm = 1, sint = 5, srgb = 6, floating = 7; constexpr std::uint32_t uint = 4; const auto fail = [&]() -> DecodedColorFormat { throw std::runtime_error("AGC graphics: unsupported color format " + std::to_string(format) + " number type " + std::to_string(number) + " component swap " + std::to_string(swap)); }; const bool alternate = swap == 1; - if (swap > 1) return fail(); + const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; + if (swap > 1 && format != 1 && format != 2 && format != 4) return fail(); switch (format) { case 1: - if (swap != 0) return fail(); - if (number == unorm) return {VK_FORMAT_R8_UNORM, 1}; - if (number == uint) return {VK_FORMAT_R8_UINT, 1}; + if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); + if (number == snorm) return single(VK_FORMAT_R8_SNORM, 1); + if (number == uint) return single(VK_FORMAT_R8_UINT, 1); return fail(); case 2: - if (swap != 0) return fail(); - if (number == unorm) return {VK_FORMAT_R16_UNORM, 2}; - if (number == floating) return {VK_FORMAT_R16_SFLOAT, 2}; - if (number == uint) return {VK_FORMAT_R16_UINT, 2}; + if (number == unorm) return single(VK_FORMAT_R16_UNORM, 2); + if (number == snorm) return single(VK_FORMAT_R16_SNORM, 2); + if (number == floating) return single(VK_FORMAT_R16_SFLOAT, 2); + if (number == uint) return single(VK_FORMAT_R16_UINT, 2); return fail(); case 3: if (swap != 0) return fail(); if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2}; + if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2}; return fail(); case 4: - if (swap != 0) return fail(); - if (number == floating) return {VK_FORMAT_R32_SFLOAT, 4}; - if (number == uint) return {VK_FORMAT_R32_UINT, 4}; - if (number == sint) return {VK_FORMAT_R32_SINT, 4}; + if (number == floating) return single(VK_FORMAT_R32_SFLOAT, 4); + if (number == uint) return single(VK_FORMAT_R32_UINT, 4); + if (number == sint) return single(VK_FORMAT_R32_SINT, 4); return fail(); case 5: if (swap != 0) return fail(); if (number == floating) return {VK_FORMAT_R16G16_SFLOAT, 4}; if (number == unorm) return {VK_FORMAT_R16G16_UNORM, 4}; + if (number == snorm) return {VK_FORMAT_R16G16_SNORM, 4}; if (number == uint) return {VK_FORMAT_R16G16_UINT, 4}; return fail(); case 6: @@ -179,6 +274,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, return {alternate ? VK_FORMAT_A2R10G10B10_UNORM_PACK32 : VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4}; case 10: if (number == unorm) return {alternate ? VK_FORMAT_B8G8R8A8_UNORM : VK_FORMAT_R8G8B8A8_UNORM, 4}; + if (number == snorm) return {alternate ? VK_FORMAT_B8G8R8A8_SNORM : VK_FORMAT_R8G8B8A8_SNORM, 4}; if (number == srgb) return {alternate ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_R8G8B8A8_SRGB, 4}; return fail(); case 11: @@ -190,6 +286,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, if (swap != 0) return fail(); if (number == floating) return {VK_FORMAT_R16G16B16A16_SFLOAT, 8}; if (number == unorm) return {VK_FORMAT_R16G16B16A16_UNORM, 8}; + if (number == snorm) return {VK_FORMAT_R16G16B16A16_SNORM, 8}; if (number == uint) return {VK_FORMAT_R16G16B16A16_UINT, 8}; return fail(); case 14: @@ -304,7 +401,9 @@ State DecodeState(const QueueState& queue) { zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); { const auto depthControl = read(cx, 0x200); - if (depthPassThrough(depthControl)) { + if ((depthControl & 3u) != 0 && depthSurfaceBound(cx)) { + decodeDepth(cx, depthControl, result); + } else if (depthPassThrough(depthControl)) { static bool reported = false; if (!reported) { reported = true; @@ -378,7 +477,7 @@ State DecodeState(const QueueState& queue) { for (std::uint32_t slot = 0; slot < slotCount; ++slot) { // Export formats only matter for the targets the draw writes. const auto slotExport = (exportFormat >> (4u * slot)) & 0xfu; - Require(slotExport == 4 || slotExport == 9, "only FP16_ABGR or 32_ABGR color export is supported"); + if (slotExport == 0 || slotExport == 7 || slotExport == 8 || slotExport > 9) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " is unsupported"); const auto stride = slot * 0xfu; ColorTarget color{}; const auto info = read(cx, 0x31c + stride); @@ -388,13 +487,11 @@ State DecodeState(const QueueState& queue) { const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - // uncompressed and only its fast-clear keys matter (see DccMetadata.hpp). - Require((info & ~(0x00029f7cu | 0x00040000u | 0x10000000u)) == 0, "color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported"); - Require((info & 0x8000u) != 0 || number == 7, "unclamped normalized color is unsupported"); - // CB_COLOR_VIEW: MIP_LEVEL (bits 24-27) selects the rendered mip; array slices are not modeled. + if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); + Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); const auto view = read(cx, 0x31b + stride); - Require((view & ~0x0f000000u) == 0, "color array views are unsupported"); - const auto viewMip = (view >> 24u) & 0xfu; + Require((view & ~0x3c000000u) == 0, "color array views are unsupported"); + const auto viewMip = (view >> 26u) & 0xfu; zero(cx, 0x31d + stride, ~0u, "color samples, fragments or destination alpha override"); const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; @@ -404,22 +501,26 @@ State DecodeState(const QueueState& queue) { color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u}; color.elementBytes = decoded.elementBytes; std::uint64_t mipOffset = 0; + color.surfaceExtent = color.extent; + color.mipCount = maxMip + 1u; + color.mip = viewMip; if (maxMip != 0) { // A mipmapped surface is addressed like a texture; the view renders into one mip of it. const auto mips = ComputeElementMipLayout(color.tileMode == ColorTileMode::Linear ? TextureTileMode::kLinear : TextureTileMode::kR64KBX, color.elementBytes, color.extent.width, color.extent.height, maxMip + 1u); const auto& mip = mips.at(viewMip); - Require(!mip.tail, "rendering into a packed mip tail is unsupported"); mipOffset = mip.tiledOffset; + color.mipTail = mip.tail; color.extent = {mip.width, mip.height}; } const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); const auto high = read(cx, 0x390 + slot); Require((high & ~0xffu) == 0, "invalid color address extension"); - color.address = ((static_cast(high) << 40u) | (static_cast(read(cx, 0x318 + stride)) << 8u)) + mipOffset; + color.surfaceAddress = (static_cast(high) << 40u) | (static_cast(read(cx, 0x318 + stride)) << 8u); + color.address = color.surfaceAddress + mipOffset; color.bytes = colorLayout.Bytes(); GuestMemory::CheckRange(reinterpret_cast(color.address), color.bytes, colorLayout.Alignment(), true); color.format = decoded.format; - color.componentMapping = 0xe4u; + color.componentMapping = decoded.componentMapping; if ((info & 0x10000000u) != 0) { if (maxMip == 0) { const auto dccHigh = find(cx, 0x3a8 + slot); @@ -443,6 +544,9 @@ State DecodeState(const QueueState& queue) { } if (result.hasColorTarget) { result.color = result.colors.front(); + if (result.depth) result.renderExtent = {std::min(result.renderExtent.width, result.depth->extent.width), std::min(result.renderExtent.height, result.depth->extent.height)}; + } else if (result.depth) { + result.renderExtent = result.depth->extent; } else { const auto screenBottomRight = read(cx, 0xd); APS5_LOG_OUT_DEBUG("No color target, screen BR register=0x%x", screenBottomRight); @@ -478,7 +582,11 @@ State DecodeState(const QueueState& queue) { APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend); Require((blend & 0x0000e000u) == 0, "reserved blend control bits"); VkPipelineColorBlendAttachmentState state{}; - state.colorWriteMask = (targetMask >> (4u * slot)) & 0xfu; + const auto mapping = result.colors[slot].componentMapping; + const auto exportedMask = (targetMask >> (4u * slot)) & 0xfu; + for (std::uint32_t component = 0; component < 4; ++component) { + if (((exportedMask >> ((mapping >> (2u * component)) & 3u)) & 1u) != 0) state.colorWriteMask |= 1u << component; + } state.blendEnable = (blend >> 30u) & 1u; if (state.blendEnable) { Require((read(cx, 0x31c + slot * 0xfu) & 0x10000u) == 0, "blend bypass conflicts with enabled blending"); @@ -489,6 +597,11 @@ State DecodeState(const QueueState& queue) { state.srcAlphaBlendFactor = blendFactor(alpha & 0x1fu); state.dstAlphaBlendFactor = blendFactor((alpha >> 8u) & 0x1fu); state.alphaBlendOp = blendOp((alpha >> 5u) & 7u); + if ((mapping & 3u) == 3u) { + state.srcColorBlendFactor = state.srcAlphaBlendFactor; + state.dstColorBlendFactor = state.dstAlphaBlendFactor; + state.colorBlendOp = state.alphaBlendOp; + } for (std::uint32_t i = 0; i < 4; ++i) result.blendConstants[i] = readFloat(cx, 0x105 + i); } result.blends.push_back(state); @@ -498,6 +611,13 @@ State DecodeState(const QueueState& queue) { return result; } +std::array ExportMappings(const State& state) { + std::array mappings{}; + mappings.fill(0xe4u); + for (std::size_t slot = 0; slot < state.colors.size() && slot < mappings.size(); ++slot) mappings[slot] = state.colors[slot].componentMapping; + return mappings; +} + std::string DrawRejection(const QueueState& queue, bool indexed) { const auto& cx = queue.context; // A register a rule needs that is absent gives no verdict here: DecodeState reports it. @@ -517,7 +637,8 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (auto reason = indexed ? nonzero(queue.userConfig, 0x24b, ~0u, "primitive restart (GE_MULTI_PRIM_IB_RESET_EN)") : std::string(); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); !reason.empty()) return reason; if (value(cx, 0x200, word)) { - if (!depthPassThrough(word) && !IgnoreDepthTest() && (word & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); + const bool surface = (word & 3u) != 0 && depthSurfaceBound(cx); + if (!surface && !depthPassThrough(word) && !IgnoreDepthTest() && (word & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); if (auto reason = require((word & 0xc0000008u) == 0, "depth bounds or depth-conditional color writes are unsupported"); !reason.empty()) return reason; } if (auto reason = nonzero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 74f55c1a8..f5cd4eccb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -457,6 +457,17 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components) : context(context) { + layout = VK_IMAGE_LAYOUT_GENERAL; + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = depthImage; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = depthFormat; + viewInfo.components = components; + viewInfo.subresourceRange = {aspect, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); +} + Texture::~Texture() { release(); } @@ -866,9 +877,10 @@ VkImageView StorageTexture::createView(std::uint32_t mip) const { return created; } -VkImageView StorageTexture::AttachmentView(VkFormat format) { +VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip) { Require(attachable, "storage image cannot be a color attachment"); - const auto found = attachmentViews.find(format); + Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); + const auto found = attachmentViews.find({format, mip}); if (found != attachmentViews.end()) return found->second; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; @@ -878,10 +890,10 @@ VkImageView StorageTexture::AttachmentView(VkFormat format) { viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = format; viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; - viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1u, descriptor.baseArray, 1u}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, descriptor.baseArray, 1u}; VkImageView created = VK_NULL_HANDLE; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment"); - attachmentViews.emplace(format, created); + attachmentViews.emplace(std::pair{format, mip}, created); return created; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 49af94ea1..656239486 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include #include #include #include @@ -245,7 +246,7 @@ std::array ThickBlockExtent(TextureTileMode tileMode, std::uin switch (tileMode) { case TextureTileMode::kStandard4KB: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]}; case TextureTileMode::kStandard64KB: return {1u << thick64KB[index][0], 1u << thick64KB[index][1], 1u << thick64KB[index][2]}; - default: throw std::runtime_error("AGC graphics: 3D textures are only supported linear or in SW_4KB_S / SW_64KB_S"); + default: throw std::runtime_error("AGC graphics: 3D textures are only supported linear or in SW_4KB_S / SW_64KB_S, not in XOR swizzle " + std::to_string(XorSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element"); } } @@ -289,6 +290,17 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; + if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kD64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { + Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); + const auto depth = descriptor.depthOrLastArray + 1u; + geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, 1u); + geometry.layers = depth; + geometry.imageDepth = depth; + geometry.guestBytes = ComputeSurfaceSize(geometry.mips, depth); + geometry.layerBytes = geometry.guestBytes / depth; + for (const auto& mip : geometry.mips) geometry.sliceLinearBytes = std::max(geometry.sliceLinearBytes, mip.linearOffset + mip.linearSize); + return geometry; + } if (descriptor.dimension == TextureDimension::k3D) { Require(descriptor.mipCount == 1, "mipmapped 3D textures are not implemented"); const auto depth = descriptor.depthOrLastArray + 1u; diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp index 8ff5e49b4..78757ea71 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Lifecycle.cpp @@ -5,16 +5,17 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr uint32_t AGC_EINVAL = 0x80020065u; + extern "C" { uint32_t APS5_VABI sceAgcDriverInitResourceRegistration(void) { - NotImplemented_nid_no_patch(__func__); return 0; } uint32_t APS5_VABI sceAgcDriverQueryResourceRegistrationUserMemoryRequirements(uint64_t* size_in_bytes) { - (void)size_in_bytes; - NotImplemented_nid_no_patch(__func__); + if (!size_in_bytes) return AGC_EINVAL; + *size_in_bytes = 0x4000ULL; return 0; } diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index 4de64c2dd..7aec5262e 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -30,7 +30,6 @@ int APS5_VABI sceAgcDriverRegisterResource(uint32_t* resource_handle, uint32_t o int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) { (void)stream_id; (void)stream; - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp b/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp index fa1ae944e..134e81a64 100644 --- a/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp +++ b/core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -11,14 +12,11 @@ int APS5_VABI sceAgcDriverSetHsOffchipParam(uint64_t value0, uint64_t value1, ui (void)value0; (void)value1; (void)value2; - NotImplemented_nid_no_patch(__func__); return 0; } int APS5_VABI sceAgcDriverSetTFRing(const volatile void* base, uint32_t size) { - (void)base; - (void)size; - NotImplemented_nid_no_patch(__func__); + if (base == nullptr || size == 0) throw std::invalid_argument("sceAgcDriverSetTFRing: empty ring"); return 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index 14c6348d8..3af9d345d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -38,7 +38,11 @@ void RunBdaContractTests() { auto& block = program.CreateBlock(); block.AppendInstruction(&program.CreateValue(IrOpcode::Barrier, IrType::Void)); program.BlockOrder().push_back(&block); - reject([&] { ValidateBdaTarget(program, target); }, "workgroup barrier"); + ValidateBdaTarget(program, target); + AgcDriver::Graphics::Require(!BdaInvocationsMayStop(program), "a BDA program with barriers must keep its invocations running"); + program.Resources().stage = IrShaderStage::TessellationControl; + reject([&] { ValidateBdaTarget(program, target); }, "barrier-safe"); + program.Resources().stage = IrShaderStage::Compute; RecompileRequest request{}; request.target.bdaAbiVersion = BdaAbi::Version; request.target.supportedCapabilities = capabilities; @@ -50,5 +54,6 @@ void RunBdaContractTests() { AgcDriver::Graphics::Require(decoded.request.target.bdaAbiVersion == BdaAbi::Version && decoded.request.target.supportedCapabilities.size() == capabilities.size() && decoded.request.target.supportedExtensions[1] == extensions[1], "BDA request serialization changed target contract"); auto invalid = encoded; invalid[0] = invalid[0] == 'A' ? 'B' : 'A'; - reject([&] { static_cast(serializer.Deserialize(invalid)); }, "serialization version"); + reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); + reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 1a578bb94..484d60a7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -2,12 +2,19 @@ #include "ColorTransferTests.hpp" #include #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include #include #include #include +namespace AgcDriver::Graphics { +Recorder* Recorder::Active() { return nullptr; } +void Recorder::Submit() { Require(false, "the device tests have no recorder"); } +bool Recorder::Reap() { Require(false, "the device tests have no recorder"); return false; } +} + namespace { using namespace AgcDriver::Graphics; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index d5c87f22e..82d4cbb8e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -2,10 +2,19 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif #include #include #include +#include #include +#include namespace { @@ -26,6 +35,190 @@ ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) { return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false}; } +void heapMirrorTests(const Context& context, const BdaTestAccess& access) { + Require(GuestArena::GuestArenaAvailable_nid_postfix() && GuestArena::GuestArenaWriteWatched_nid_postfix(), "the heap mirror tests need the write-watched guest arena"); + constexpr std::size_t bytes = 2 * 65536; + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536); +#ifdef _WIN32 + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#endif + auto* guest = static_cast(block); + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(block); + const auto registry = [&](bool add, bool writable) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, writable); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto build = [&](std::uint64_t written, const std::function& gpu) { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + if (written != 0) leased.AddWritable(written, 32); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == address && range.end == address + bytes; }); + Require(found != ranges.end() && found->permissions == ShaderRecompiler::BdaAbi::Read, "the heap range is missing from the BDA table"); + const auto device = found->deviceAddress; + if (gpu) gpu(leased); + leased.WriteBack(); + return device; + }; + const auto sweep = [&] { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + leased.WriteBack(); + }; + sweep(); + registry(true, false); + const auto before = MirrorCounters(); + const auto first = build(0, {}); + const auto made = MirrorCounters(); + Require(made.heapMirrors == before.heapMirrors + 1 && made.heapBytes == before.heapBytes + bytes, "a read-only heap range was not mirrored"); + Require(access.addressBytes(first)[65536 + 3] == std::byte{0x11}, "the heap mirror was not filled"); + Require(build(0, {}) == first && MirrorCounters().blocksCopied == made.blocksCopied && MirrorCounters().heapRefills == made.heapRefills, "an unchanged heap range was read into its mirror again"); + guest[65536 + 3] = 0x22; + Require(build(0, {}) == first && MirrorCounters().heapRefills == made.heapRefills + 1 && MirrorCounters().blocksCopied == made.blocksCopied + 1, "a written heap block was not read again alone"); + Require(access.addressBytes(first)[65536 + 3] == std::byte{0x22}, "the heap mirror missed the CPU write"); + const auto one = MirrorCounters(); + guest[5] = 0x33; + guest[65536 + 7] = 0x44; + Require(build(0, {}) == first && MirrorCounters().blocksCopied == one.blocksCopied + 2, "two written heap blocks were not read again"); + Require(access.addressBytes(first)[5] == std::byte{0x33} && access.addressBytes(first)[65536 + 7] == std::byte{0x44}, "the heap mirror missed the CPU writes"); + registry(false, false); + sweep(); + const auto swept = MirrorCounters(); + Require(swept.heapMirrors == before.heapMirrors && swept.heapBytes == before.heapBytes, "the heap mirror outlived its range"); + + registry(true, true); + const auto device = build(address + 16, [&](GuestBufferMemory& leased) { + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(address + 16, 32, adjustment); + access.bytes(view.buffer)[view.offset + adjustment] = std::byte{0x77}; + guest[16 + 8] = 0x55; + }); + Require(guest[16] == 0x77 && guest[16 + 8] == 0x55, "a writable heap mirror's write-back lost the GPU's store or rolled back the CPU's"); + Require(build(0, {}) == device && access.addressBytes(device)[16] == std::byte{0x77} && access.addressBytes(device)[16 + 8] == std::byte{0x55}, "the writable heap mirror missed the stores"); + registry(false, true); + sweep(); + Require(MirrorCounters().heapMirrors == before.heapMirrors, "the writable heap mirror outlived its range"); + +#ifdef _WIN32 + { + constexpr std::size_t half = 2 * 65536; + void* pair = GuestArena::GuestArenaAllocate_nid_postfix(2 * half, 65536); + GuestArena::GuestArenaCommit_nid_postfix(pair, 2 * half, PAGE_READWRITE, 2 * half); + std::memset(pair, 0x11, 2 * half); + auto* first = static_cast(pair); + auto* second = first + half; + const auto registerPair = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + for (auto* range : {first, second}) { + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, range, half, true, false); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, range); + } + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + const auto firstDevice = [&] { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == reinterpret_cast(first); }); + Require(found != ranges.end(), "the first heap range is missing from the BDA table"); + const auto device = found->deviceAddress; + leased.WriteBack(); + return device; + }; + registerPair(true); + const auto device = firstDevice(); + first[5] = 0x66; + DWORD previous = 0; + Require(VirtualProtect(second, 65536, PAGE_NOACCESS, &previous) != 0, "cannot protect the second heap range"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(second), 65536); + bool threw = false; + try { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(VirtualProtect(second, 65536, PAGE_READWRITE, &previous) != 0, "cannot unprotect the second heap range"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(second), 65536); + Require(threw, "a build over an inaccessible heap mirror range did not fail"); + Require(firstDevice() == device && access.addressBytes(device)[5] == std::byte{0x66}, "an interrupted build left a changed heap block marked current"); + registerPair(false); + sweep(); + GuestArena::GuestArenaReset_nid_postfix(pair, 2 * half); + GuestArena::GuestArenaRelease_nid_postfix(pair, 2 * half); + } +#endif +#ifdef _WIN32 + { + constexpr std::size_t size = 2 * 65536; + void* raw = GuestArena::GuestArenaAllocate_nid_postfix(size, 65536); + GuestArena::GuestArenaCommit_nid_postfix(raw, size, PAGE_READWRITE, size); + auto* bytes8 = static_cast(raw); + std::memset(raw, 0x11, size); + const auto base = reinterpret_cast(raw); + const auto page = base + 65536; + DWORD previous = 0; + Require(VirtualProtect(reinterpret_cast(page), 4096, PAGE_READONLY, &previous) != 0, "cannot make the aliased page read-only"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(page, 4096); + const auto registerRange = [&](bool add) { + auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix(); + if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, raw, size, true, true); + else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, raw); + GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation); + }; + registerRange(true); + const auto storeAt = [&](std::uint64_t at) { + GuestBufferMemory leased(context); + leased.AcquireRegistered(); + leased.AddWritable(page - 16, 32); + leased.Upload(true); + const auto ranges = leased.AddressRanges(); + const auto found = std::find_if(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == base; }); + Require(found != ranges.end() && access.addressBytes(found->deviceAddress)[65536 + 8] == std::byte{0x11}, "a writable range with a read-only page was not mirrored with its bytes"); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(page - 16, 32, adjustment); + access.bytes(view.buffer)[view.offset + adjustment + static_cast(at - (page - 16))] = std::byte{0x77}; + leased.WriteBack(); + }; + const auto made = MirrorCounters().heapMirrors; + storeAt(page - 8); + Require(MirrorCounters().heapMirrors == made + 1 && bytes8[65536 - 8] == 0x77, "a store next to a read-only page was not written back"); + bool refused = false; + try { + storeAt(page + 4); + } catch (const std::runtime_error& error) { + refused = std::string(error.what()).find("aliased writes are not implemented") != std::string::npos; + } + Require(refused && bytes8[65536 + 4] == 0x11, "a GPU change of a read-only page was not refused"); + { + GuestBufferMemory plain(context); + plain.AddReadable(page, 16); + plain.Upload(false); + std::uint32_t adjustment = 0; + const auto view = plain.Descriptor(page, 16, adjustment); + Require(access.bytes(view.buffer)[view.offset + adjustment] == std::byte{0x11}, "a descriptor over a read-only page of a writable range read zeros"); + plain.WriteBack(); + } + registerRange(false); + sweep(); + Require(VirtualProtect(reinterpret_cast(page), 4096, PAGE_READWRITE, &previous) != 0, "cannot restore the aliased page"); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(page, 4096); + GuestArena::GuestArenaReset_nid_postfix(raw, size); + GuestArena::GuestArenaRelease_nid_postfix(raw, size); + } +#endif +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, bytes); +#endif + GuestArena::GuestArenaRelease_nid_postfix(block, bytes); +} + } void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { @@ -36,9 +229,11 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { memory.AddWritable(address, sizeof(guest)); memory.AddWritable(address + 16, 16); memory.Upload(true); - const auto first = memory.Descriptor(address, sizeof(guest)); - const auto alias = memory.Descriptor(address + 16, 16); - Require(first.buffer == alias.buffer && alias.offset == 16, "aliased guest buffers have different owners"); + std::uint32_t adjustment = 0; + const auto first = memory.Descriptor(address, sizeof(guest), adjustment); + Require(adjustment == 0, "a view at its owner's start is bound off it"); + const auto alias = memory.Descriptor(address + 16, 16, adjustment); + Require(first.buffer == alias.buffer && alias.offset + adjustment == 16 && alias.range == 16 + adjustment, "aliased guest buffers have different owners"); const auto ranges = memory.AddressRanges(); Require(ranges.size() == 1 && ranges[0].begin == address && ranges[0].end == address + sizeof(guest), "incorrect BDA range bounds"); Require(ranges[0].deviceAddress != 0 && ranges[0].permissions == ShaderRecompiler::BdaAbi::Read, "incorrect BDA address or permissions"); @@ -94,6 +289,10 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { Require(range.begin == snapshots[0].address && range.end == range.begin + source.size(), "64-bit guest address was truncated"); const auto fault = access.bytes(access.descriptor(5).buffer); for (const auto byte : fault) Require(byte == std::byte{}, "fault buffer was not initialized"); + const ShaderRecompiler::BdaAbi::Fault denied{ShaderRecompiler::BdaAbi::FaultState::Ready, ShaderRecompiler::BdaAbi::FaultReason::Permission, snapshots[0].address + 4, 4, 0, 0x88, 0}; + std::memcpy(fault.data(), &denied, sizeof(denied)); + reject([&] { resources.WriteBack(); }, "read-only in the BDA table"); + std::memset(fault.data(), 0, fault.size()); resources.WriteBack(); } { @@ -118,8 +317,10 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { GuestBufferMemory unaligned(aligned); unaligned.AddWritable(address, sizeof(guest)); unaligned.Upload(true); - reject([&] { unaligned.Descriptor(address + 4, 4); }, "offset alignment"); - reject([&] { unaligned.Descriptor(address + sizeof(guest), 4); }, "exceeds its GPU owner"); + std::uint32_t adjustment = 0; + const auto view = unaligned.Descriptor(address + 4, 4, adjustment); + Require(adjustment == 4 && view.offset == 0 && view.range == 8, "a view off the offset alignment binds from below it"); + reject([&] { unaligned.Descriptor(address + sizeof(guest), 4, adjustment); }, "exceeds its GPU owner"); } { // The cached address space: a second build in an unchanged registry takes the first one's @@ -139,8 +340,9 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { leased.AcquireRegistered(); Require(leased.HoldsLease(), "address-based build holds no lease"); leased.Upload(true); - const auto view = leased.Descriptor(blockAddress, 64); - Require(view.range == 64, "leased block has no descriptor"); + std::uint32_t adjustment = 0; + const auto view = leased.Descriptor(blockAddress, 64, adjustment); + Require(view.range == 64 + adjustment, "leased block has no descriptor"); const auto ranges = leased.AddressRanges(); Require(std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return range.begin == blockAddress && range.end == blockAddress + 64; }), "leased block is missing from the BDA table"); leased.WriteBack(); @@ -153,4 +355,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { const auto dropped = AddressSpaceCounters(); if (dropped.enabled) Require(dropped.waiterDrops == after.waiterDrops + 1 && LeaseCounters().cacheDrops == dropped.waiterDrops, "the free did not drop the cached address space"); } + heapMirrorTests(context, access); + Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); + const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); + Require(!copies.empty() && copies.find("0x10000+0x20000 (0.1 MiB committed, uncommitted pages)") < copies.find("0x1000+0x1000"), "the copy limit does not name the largest copy first"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp index 0ebb477a9..45d1d8f25 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp @@ -8,6 +8,7 @@ struct BdaTestAccess { std::function(VkBuffer)> bytes; std::function descriptor; + std::function(VkDeviceAddress)> addressBytes; }; void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access); diff --git a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp index 1c96fbee9..f95863fdc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ColorTargetLayout.cpp @@ -1,7 +1,10 @@ #include "BdaTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" +#include #include +#include #include +#include #include namespace { @@ -25,7 +28,8 @@ void RunColorTargetLayoutTests() { reject([] { DecodeColorTileMode(0xcdc6c000); }); reject([] { DecodeColorTileMode(0x09014000); }); reject([] { ColorTargetLayout(0, 1, ColorTileMode::RenderTarget); }); - reject([] { ColorTargetLayout(63, 1, ColorTileMode::Linear); }); + const ColorTargetLayout padded(63, 2, ColorTileMode::Linear); + Require(padded.Bytes() == 512 && padded.LinearBytes() == 504 && padded.Offset(0, 1) == 256, "linear rows are not padded to 256 bytes"); const ColorTargetLayout screen(3840, 2160, ColorTileMode::RenderTarget); Require(screen.Bytes() == 33423360 && screen.LinearBytes() == 33177600 && screen.Alignment() == 65536, "4K color backing layout is incorrect"); const ColorTargetLayout layout(257, 129, ColorTileMode::RenderTarget); @@ -55,8 +59,9 @@ void RunColorTargetLayoutTests() { } reject([&] { layout.Detile(std::span(tiled).first(4), restored); }); reject([&] { layout.Tile(std::span(linear).first(4), tiled); }); - alignas(65536) static std::array guest{}; - guest.fill(std::byte{0x6b}); + static std::vector storage(2 * 65536); + const std::span guest(reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}), 65536); + std::fill(guest.begin(), guest.end(), std::byte{0x6b}); ColorTarget target{reinterpret_cast(guest.data()), {2, 2}, VK_FORMAT_R8G8B8A8_UNORM, guest.size(), 0xe4, ColorTileMode::RenderTarget}; std::array pixels{}; pixels.fill(std::byte{0x32}); diff --git a/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp new file mode 100644 index 000000000..0c5fccca7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DppWrite.cpp @@ -0,0 +1,37 @@ +#include "ControlFlow/GraphBuilder.hpp" +#include "ControlFlow/Structurizer.hpp" +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include "Translation/InstructionTranslator.hpp" +#include "Translation/ShaderInputInfoBuilder.hpp" +#include +#include +#include + +using namespace ShaderRecompiler; + +int main() { + const std::vector code{0x4a0202fau, 0xaf00e400u, 0xbf810000u}; + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + auto cfg = GraphBuilder{}.Build(decoded); + Structurizer{}.Structurize(cfg); + GuestContext context{}; + context.waveSize = 32; + context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + TranslateOptions options{}; + options.stage = ShaderStageKind::Compute; + options.waveSize = 32; + options.userDataCount = 0; + options.inputInfo = BuildShaderStageInputInfo(ShaderStageKind::Compute, context, 32); + const auto program = InstructionTranslator{}.Translate(decoded, cfg, options); + for (const auto& block : program.Blocks()) { + for (const auto* inst : block->Instructions()) { + if (inst->Opcode() == IrOpcode::DppUpdateU32 && inst->Flags().rowMask == 0xa) return 0; + } + } + std::fprintf(stderr, "the DPP add writes v1 without its row mask\n"); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + } + return 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index dacf921c2..0b172c699 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -100,7 +100,8 @@ void stateTests() { state = AgcDriver::Graphics::DecodeState(queue); Require(state.scissor.offset.x == 3 && state.scissor.offset.y == 1 && state.scissor.extent.width == 29 && state.scissor.extent.height == 2, "scissor intersection changed"); queue.context[0x31c] |= 0x10000000; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC"); + queue.context[0x325] = 0x1234; + Require(AgcDriver::Graphics::DecodeState(queue).color.dccAddress == 0x123400, "DCC key address decode changed"); queue = makeState(); queue.context.erase(0x3b8); expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); @@ -109,10 +110,11 @@ void stateTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color tile mode"); queue = makeState(); queue.context[0x3b0] = (62u << 14u) | 3u; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "pitch"); + Require(AgcDriver::Graphics::DecodeState(queue).color.bytes == 64u * 4u * 4u, "padded linear pitch changed"); queue = makeState(); queue.context[0x8e] = 0xff; - expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "target zero"); + queue.context[0x8f] = 0xff; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 0"); queue = makeState(); queue.context[0x200] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth"); @@ -128,7 +130,7 @@ void stateTests() { queue.context[0x1b4] = 2; state = AgcDriver::Graphics::DecodeState(queue); Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "precheck rejected the reference state"); - static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, state.hasColorTarget, state.color.componentMapping)); + static_cast(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, AgcDriver::Graphics::ExportMappings(state))); AgcDriver::Graphics::RegisterReadLog() = nullptr; Require(!log.empty(), "the register facade recorded nothing"); for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register the decoders read: " + std::string(AgcDriver::Graphics::RegisterBankName(read.bank)) + " " + std::to_string(read.offset)); @@ -233,6 +235,66 @@ void DisabledColorTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void DepthStencilTests() { + auto queue = makeState(); + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (1u << 16u) | 3u; + queue.context[0x00a] = 7; + queue.context[0x00b] = 0; + queue.context[0x010] = 0x22900983; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x00050050; + queue.context[0x10c] = 0x01ffff01; + queue.context[0x10d] = 0x01000001; + queue.context[0x200] = 0x00700711; + std::vector log; + AgcDriver::Graphics::RegisterReadLog() = &log; + auto state = AgcDriver::Graphics::DecodeState(queue); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a depth register the decoder reads: " + std::to_string(read.offset)); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "precheck rejected a stencil draw with a surface: " + rejection); + Require(state.depth && state.depth->address == 0x10000 && state.depth->stencilAddress == 0x20000 && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.depth->clearStencil == 7, "depth surface decode changed"); + Require(state.renderExtent.width == 4 && state.renderExtent.height == 2, "render extent ignores the depth surface"); + Require(!state.depthTest && !state.depthWrite && state.stencilTest, "depth/stencil enables changed"); + const auto& front = state.stencilFront; + Require(front.compareOp == VK_COMPARE_OP_ALWAYS && front.passOp == VK_STENCIL_OP_INCREMENT_AND_CLAMP && front.failOp == VK_STENCIL_OP_KEEP && front.reference == 1 && front.writeMask == 0xff, "stencil mask pass changed"); + Require(std::memcmp(&state.stencilBack, &front, sizeof(front)) == 0, "back faces without BACKFACE_ENABLE must use the front state"); + queue.context[0x10b] = 0; + queue.context[0x10c] = 0x01ffff02; + queue.context[0x200] = 0x00200211; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.compareOp == VK_COMPARE_OP_EQUAL && state.stencilFront.reference == 2 && state.stencilFront.passOp == VK_STENCIL_OP_KEEP, "stencil content pass changed"); + queue.context[0x10b] = 0x00030030; + queue.context[0x10c] = 0x01ffff00; + queue.context[0x200] = 0x00700771; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.reference == 0 && std::memcmp(&state.stencilBack, &state.stencilFront, sizeof(state.stencilFront)) == 0, "stencil clear pass changed"); + queue.context[0x200] = 0x007007f1; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.stencilBack.compareOp == VK_COMPARE_OP_ALWAYS && state.stencilBack.passOp == VK_STENCIL_OP_KEEP && state.stencilBack.writeMask == 0 && state.stencilBack.reference == 1, "back-face stencil state changed"); + queue.context[0x10b] = 0x40; + queue.context[0x10c] = 0x05ffff02; + queue.context[0x200] = 0x00200211; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil replacement"); + queue.context[0x10b] = 0x50; + queue.context[0x200] = 0x00700711; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "stencil add/subtract"); + queue.context[0x10b] = 0; + queue.context[0x000] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DB_RENDER_CONTROL"); + queue = makeState(); + queue.context[0x31b] = 1u << 26u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "mip exceeds"); + queue.context[0x31b] = 1u << 13u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "array views"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -261,7 +323,7 @@ void DepthClipTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "inverted viewport depth clamp"); queue.context[0xb4] = 0; for (std::uint32_t bit = 0; bit < 32; ++bit) { - if (bit == 19) continue; + if (bit == 19 || bit == 26 || bit == 27) continue; queue.context[0x204] = 1u << bit; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "PA_CL_CLIP_CNTL"); } @@ -592,6 +654,17 @@ void expectSingleFailure(const ShaderRecompiler::DescriptorBinding& binding, std expectResourceFailure(vertex, fragment, reason); } +void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, std::string_view what) { + ShaderRecompiler::RecompileResult vertex; + ShaderRecompiler::RecompileResult fragment; + vertex.bindings.push_back(binding); + mock = MockVulkan{}; + const auto context = mockContext(); + const auto color = AgcDriver::Graphics::DecodeState(makeState()).color; + { AgcDriver::Graphics::ShaderResources resources(context, vertex, fragment, color, 0, 0); } + Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); +} + void pushConstantTests() { ShaderRecompiler::RecompileResult vertex; ShaderRecompiler::RecompileResult fragment; @@ -716,9 +789,9 @@ void resourceTests() { mutate(binding); return binding; }; - expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::SampledImage; }), "sampled and storage image resources are not implemented"); - expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::StorageImage; }), "sampled and storage image resources are not implemented"); - expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestSamplers; binding.kind = Kind::Sampler; }), "sampled and storage image resources are not implemented"); + expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::SampledImage; }), "guest texture descriptor must contain 8 dwords"); + expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestImages; binding.kind = Kind::StorageImage; }), "guest storage image descriptors must contain 8 dwords"); + expectSingleFailure(changed([](auto& binding) { binding.role = Role::GuestSamplers; binding.kind = Kind::Sampler; }), "shader sampler descriptors exceed per-stage limits"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::Gds; binding.guestDescriptor.clear(); }), "unsupported descriptor role Gds"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::BdaPagetable; binding.guestDescriptor.clear(); }), "BDA table and fault descriptors"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::FaultBuffer; binding.guestDescriptor.clear(); }), "BDA table and fault descriptors"); @@ -733,15 +806,13 @@ void resourceTests() { expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.count = 2; binding.guestDescriptor = {1, 2}; }), "must not be arrays"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::ShaderData; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); expectSingleFailure(changed([](auto& binding) { binding.role = Role::FlattenedSrt; binding.guestDescriptor.clear(); }), "empty shader data descriptor"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[0] = 0; binding.guestDescriptor[1] = 0; }), "null shader buffer descriptor address"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 0; }), "empty shader buffer descriptor"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x40000000u; }), "reserved bits"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[3] |= 0x40000000u; }), "unsupported type"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[1] |= 0x3fffu << 16u; binding.guestDescriptor[2] = 0xffffffffu; }), "descriptor range limit"); expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor[2] = 8192; }), "descriptor range limit"); - expectSingleFailure(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "not readable"); + expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(0x1000), 8); }), "an unmapped V#"); expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast(state.color.address), 64); }), "aliases the render target"); - expectSingleFailure(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "not readable"); + expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(state.color.address), 64)); }), "aliases the render target"); expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits"); { @@ -963,7 +1034,8 @@ void rectListTests() { expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "per-vertex interpolation"); fragment.fragmentParameters[0].perVertex = false; vertex.parameterExports.clear(); - expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "no vertex export"); + auto unexported = BuildRectListShaders(vertex, fragment, target); + Require(!unexported.control.spirv.empty() && !unexported.evaluation.spirv.empty(), "rect-list shaders with an unexported parameter are empty"); fragment.fragmentParameters.clear(); target.tessellation->maxPatchSize = 3; expectFailure([&] { static_cast(BuildRectListShaders(vertex, fragment, target)); }, "device limits"); @@ -1143,6 +1215,7 @@ void validationTests() { } int main() { + _putenv_s("APS5_PIN_WAIT_MS", "200"); try { { const AgcDriver::Graphics::Context context{}; @@ -1168,6 +1241,7 @@ int main() { stateTests(); hardwareScreenOffsetTests(); DepthClipTests(); + DepthStencilTests(); DisabledColorTests(); ShaderStageTests(); InitialContextTests(); @@ -1178,6 +1252,7 @@ int main() { mock = MockVulkan{}; auto bdaContext = mockContext(); bdaContext.bufferDeviceAddress = true; + bdaContext.limits.maxStorageBufferRange = 1u << 27; RunBdaResourceTests(bdaContext, { [](VkBuffer buffer) -> std::span { return mock.memories.at(mock.bufferMemory.at(buffer)); }, [](std::uint32_t binding) { @@ -1185,6 +1260,11 @@ int main() { if (it->binding == binding) return it->buffers.at(0); } throw std::runtime_error("missing BDA test descriptor"); + }, + [](VkDeviceAddress address) { + const auto offset = address - 0x100000000000ULL; + const auto buffer = reinterpret_cast(offset / 0x10000); + return std::span(mock.memories.at(mock.bufferMemory.at(buffer))).subspan(offset % 0x10000); } }); Require(mock.live == 0, "BDA resources leaked Vulkan objects"); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index fe60064b0..4af9a844c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -24,7 +24,7 @@ struct Fields { std::uint32_t filterMode = 0; bool disableDegamma = false; std::uint32_t minLodRaw = 0; - std::uint32_t maxLodRaw = 0x3c00; + std::uint32_t maxLodRaw = 0xc00; std::uint32_t perfMip = 0; std::uint32_t perfZ = 0; std::uint32_t lodBiasRaw = 0; @@ -82,7 +82,7 @@ void RunGuestSamplerResourceTests() { Require(result.mipmapMode == VK_SAMPLER_MIPMAP_MODE_LINEAR, "linear mip filter must decode to linear mipmap mode"); Require(result.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "clamp mode 2 must decode to clamp-to-edge"); Require(!result.anisotropyEnable && result.maxAnisotropy == 1.0f, "non-anisotropic filter must leave anisotropy disabled"); - Require(nearlyEqual(result.maxLod, static_cast(0x3c00) / 256.0f), "max LOD decoded incorrectly"); + Require(nearlyEqual(result.maxLod, 12.0f), "max LOD decoded incorrectly"); Require(result.borderColor == VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, "border color type 0 must decode to transparent black"); Fields nearest = base; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index eedc6e271..64116f4a8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -114,7 +114,10 @@ void RunGuestTextureResourceTests() { tileModes.tileModeRaw = 0x09; Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::kStandard64KB, "tile mode 0x09 must decode to standard 64KB"); tileModes.tileModeRaw = 0x1b; + rejectFields(tileModes, "pipe/bank XOR base"); + tileModes.base40 = 0x120000ull; Require(DecodeTextureResource(pack(tileModes)).tileMode == TextureTileMode::RenderTarget64KB, "tile mode 0x1b must decode to render target 64KB"); + tileModes.base40 = base.base40; tileModes.tileModeRaw = 0x02; rejectFields(tileModes, "unsupported tile mode"); @@ -215,31 +218,22 @@ void RunGuestTextureResourceTests() { badMsaa.msaaDepth = true; rejectFields(badMsaa, "MSAA"); - Fields badBlockSize = base; - badBlockSize.maxUncompBlkSize = 1; - rejectFields(badBlockSize, "DCC block size overrides"); - badBlockSize = base; - badBlockSize.maxCompBlkSize = 1; - rejectFields(badBlockSize, "DCC block size overrides"); + Fields blockSize = base; + blockSize.maxUncompBlkSize = 1; + blockSize.maxCompBlkSize = 1; + Require(DecodeTextureResource(pack(blockSize)).baseAddress == DecodeTextureResource(pack(base)).baseAddress, "DCC block size overrides changed texture storage"); - Fields badMeta = base; - badMeta.metaPipeAligned = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.writeCompress = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.metaCompress = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.dccAlphaPos = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.dccColorTransf = true; - rejectFields(badMeta, "metadata compression"); - badMeta = base; - badMeta.metaAddr = 1; - rejectFields(badMeta, "metadata compression"); + Fields meta = base; + meta.metaPipeAligned = true; + meta.writeCompress = true; + meta.dccColorTransf = true; + meta.metaAddr = 1; + const auto uncompressed = DecodeTextureResource(pack(meta)); + Require(uncompressed.dccAddress == 0 && !uncompressed.dccAlphaOnMsb, "DCC metadata without compression was decoded"); + meta.metaCompress = true; + meta.dccAlphaPos = true; + const auto compressed = DecodeTextureResource(pack(meta)); + Require(compressed.dccAddress == 0x100 && compressed.dccAlphaOnMsb, "DCC metadata was decoded wrongly"); Fields badSwizzle = base; badSwizzle.bcSwizzle = 1; @@ -251,10 +245,11 @@ void RunGuestTextureResourceTests() { badLevels.maxMip = 1; rejectFields(badLevels, "base mip level past its last mip level"); - Fields badMaxMip = base; - badMaxMip.lastLevel = 1; - badMaxMip.maxMip = 2; - rejectFields(badMaxMip, "must expose every mip level"); + Fields partialMips = base; + partialMips.lastLevel = 1; + partialMips.maxMip = 2; + const auto partial = DecodeTextureResource(pack(partialMips)); + Require(partial.lastLevel == 1 && partial.mipCount == 3, "a view over part of the mip chain decoded wrongly"); std::array shortWords{}; reject([&] { DecodeTextureResource(shortWords); }, "8 dwords"); @@ -264,7 +259,7 @@ void RunGuestTextureResourceTests() { Require(MatchesGuestDimension(Shape::Image2D, TextureDimension::k2D), "2D shape must match 2D dimension"); Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DArray), "2D shape must not match 2D array dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::k2DArray), "2D array shape must match 2D array dimension"); - Require(!MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "2D array shape must not match cube dimension"); + Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "a cube must be readable as a 2D array of its faces"); Require(MatchesGuestDimension(Shape::ImageCube, TextureDimension::kCube), "cube shape must match cube dimension"); Require(!MatchesGuestDimension(Shape::ImageCube, TextureDimension::k1D), "cube shape must not match 1D dimension"); Require(!MatchesGuestDimension(Shape::Image3D, TextureDimension::k1D), "3D shape must never match a guest dimension"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 34198c47b..adaaafd60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -342,6 +342,16 @@ void testMemorySynchronization() { check(memory.destination == 42, "failed synchronization changed the destination"); } +void testWriteChangedKeepsUntouchedBytes() { + alignas(256) static std::uint8_t guest[256]; + std::memset(guest, 0, sizeof(guest)); + std::vector original(sizeof(guest)), current(sizeof(guest)); + current[3] = std::byte{7}; + guest[100] = 0x55; + AgcDriver::GuestMemory::WriteChanged(reinterpret_cast(guest), current, original); + check(guest[3] == 7 && guest[100] == 0x55, "write-back rolled back a byte the GPU did not change"); +} + void testEventWrite() { for (const auto eventType : {0x07u, 0x0fu, 0x10u}) { AgcDriver::Pm4::Validate(makePacket(0x46, {0x400u | eventType}), 0); @@ -364,12 +374,19 @@ void testEventWrite() { } expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue"); } + AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000, 0x2}), 0); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000, 0x2}), 0x20); }, "compute queue"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x039, 0x1000, 0x2}), 0); }, "counter dump event index"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1004, 0x2}), 0); }, "misaligned occlusion counter"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0, 0}), 0); }, "null or misaligned occlusion counter"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0x1000}), 0); }, "packet size"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x138, 0x1000, 0x2}), 0); }, "event type 56"); for (const auto bit : {0x40u, 0x80u, 0x800u, 0x80000000u}) { expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410u | bit}), 0); }, "reserved bits"); } expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 1), 0); }, "header flags"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410, 0, 0}), 0); }, "packet size"); - expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x139, 0, 0}), 0); }, "event type 57"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x13a, 0, 0}), 0); }, "event type 58"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x0d}), 0); }, "event type 13"); } @@ -438,7 +455,7 @@ void testDriverSubmission() { AgcDriverWaitIdle_nid_postfix(); check(destination[0] == 83, "worker did not execute PM4 memory operations"); auto rejectedCommands = commands; - const auto unsupportedEvent = makePacket(0x46, {0x139, 0, 0}); + const auto unsupportedEvent = makePacket(0x46, {0x0d}); rejectedCommands.insert(rejectedCommands.end(), unsupportedEvent.begin(), unsupportedEvent.end()); Packet rejectedPacket{rejectedCommands.data(), static_cast(rejectedCommands.size()), 0, {}}; destination[0] = 0; @@ -481,6 +498,7 @@ int main(int argc, char** argv) { return 0; } testCatalog(); + testWriteChangedKeepsUntouchedBytes(); testRegisters(); testContextAndBases(); testIndexedDraw(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 98eb4a645..8679cf508 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -343,7 +343,7 @@ void unchangedSinceTests() { constexpr std::size_t bytes = 65536; void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, bytes); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the arena block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif auto* words = static_cast(block); const auto address = reinterpret_cast(block); @@ -371,7 +371,7 @@ void unchangedSinceTests() { CollectWritesUncached(address, 4); Require(!UnchangedSinceCollected(address, 4, generation3), "(6) a CPU write collected by another caller is not seen"); #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } @@ -389,13 +389,13 @@ void keyProofTests(const Device& device, Recorder& recorder) { constexpr std::size_t bytes = 65536; void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, bytes); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the arena block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif struct Release { void* block; ~Release() { #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } @@ -476,7 +476,7 @@ void closeRaceTests(const Device& device, Recorder& recorder) { constexpr std::size_t bytes = 1u << 20; void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the arena block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif const auto base = reinterpret_cast(block); // Late in the range, so the walker is usually still ahead of the page when the store lands. @@ -510,7 +510,7 @@ void closeRaceTests(const Device& device, Recorder& recorder) { recorder.Sync(); Require(!Recorder::LookupLabel(label, 4, 5).has_value(), "(8) the entry outlived its batch"); #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } @@ -722,13 +722,13 @@ void unitShadowTests(const Device& device, Recorder& recorder) { void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536); Require(block != nullptr, "cannot allocate the shadow test block"); #ifdef _WIN32 - Require(VirtualAlloc(block, bytes, MEM_COMMIT, PAGE_READWRITE) != nullptr, "cannot commit the shadow test block"); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); #endif struct Release { void* block; ~Release() { #ifdef _WIN32 - VirtualFree(block, bytes, MEM_DECOMMIT); + GuestArena::GuestArenaReset_nid_postfix(block, bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(block, bytes); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp new file mode 100644 index 000000000..197224a25 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -0,0 +1,49 @@ +#include "ControlFlow/Structurizer.hpp" +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +int main() { + const std::vector> successors{{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}}; + ControlFlowGraph graph; + graph.entryBlock = 0; + for (std::uint32_t id = 0; id < successors.size(); ++id) { + BasicBlock block; + block.id = id; + block.startProgramCounter = id * 8; + block.endProgramCounter = id * 8 + 8; + block.instructionBegin = id * 2; + block.instructionEnd = id * 2 + 2; + block.successors = successors[id]; + auto& terminator = block.terminator; + if (successors[id].empty()) { + terminator.kind = TerminatorKind::Return; + } else if (successors[id].size() == 1) { + terminator.kind = TerminatorKind::Branch; + terminator.trueBlock = successors[id][0]; + } else { + terminator.kind = TerminatorKind::ConditionalBranch; + terminator.condition = BranchCondition::SccNonZero; + terminator.trueBlock = successors[id][0]; + terminator.falseBlock = successors[id][1]; + } + graph.blocks.push_back(std::move(block)); + } + for (const auto& block : graph.blocks) { + for (const auto successor : block.successors) graph.blocks[successor].predecessors.push_back(block.id); + } + try { + Structurizer{}.Structurize(graph); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } + if (graph.FindBlock(2).terminator.mergeBlock == graph.FindBlock(3).terminator.mergeBlock) { + std::fprintf(stderr, "the nested selections share merge block %u\n", graph.FindBlock(2).terminator.mergeBlock); + return 1; + } + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp index 43f4061e7..afda8cfc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp @@ -1,8 +1,11 @@ #include "GraphicsTests.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include #include #include #include +#include +#include namespace { @@ -85,27 +88,30 @@ void RunTextureTilingTests() { { const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, 56, 257, 129, 1); Require(mips[0].blocksPerRow == 3 && mips[0].tiledSize == 393216, "render target surfaces must pad to complete 128 by 128 blocks for 32-bit pixels"); - Require(mips[0].pitchBytes == 1028 && mips[0].linearSize == 132612, "detiled render target rows must use the actual texture width"); + Require(mips[0].pitchBytes == 1536 && mips[0].linearSize == 1536u * 129u, "detiled render target rows must span the padded block width"); Require(ComputeSurfaceSize(mips, 6) == 2359296, "render target cube faces must retain the padded guest slice stride"); } for (const auto format : std::array{1, 7, 56, 71, 77}) { const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, format, 1024, 513, 11); - std::uint64_t linearEnd = 0; + std::vector> ranges; bool tailSeen = false; for (const auto& mip : mips) { - Require(mip.linearOffset >= linearEnd && mip.linearOffset % 4 == 0, "detiled mip levels must occupy separate word-aligned ranges"); + Require(mip.linearOffset % 4 == 0, "detiled mip levels must start word-aligned"); Require(mip.linearSize >= static_cast(mip.pitchBytes) * mip.height, "detiled mip allocation must contain every row"); Require(mip.linearSize % 4 == 0, "detiled mip sizes must preserve word alignment between array layers"); - linearEnd = mip.linearOffset + mip.linearSize; + ranges.emplace_back(mip.linearOffset, mip.linearOffset + mip.linearSize); if (mip.tail) { tailSeen = true; Require(mip.tiledOffset == 0 && mip.tiledSize == 65536, "render target mip tails must share one guest 64KB block"); } } + std::sort(ranges.begin(), ranges.end()); + for (std::size_t index = 1; index < ranges.size(); ++index) Require(ranges[index].first >= ranges[index - 1].second, "detiled mip levels must occupy separate ranges"); Require(tailSeen && !mips.front().tail, "render target mip chains must cover both regular blocks and mip tails"); } - reject([] { ComputeMipLayout(TextureTileMode::RenderTarget64KB, 169, 64, 64, 1); }, "block compressed formats"); - reject([] { ComputeMipLayout(TextureTileMode::RenderTarget64KB, 132, 64, 64, 1); }, "does not support render target tiling"); + const auto compressed = ComputeMipLayout(TextureTileMode::RenderTarget64KB, 169, 64, 64, 1); + Require(compressed.size() == 1 && compressed[0].tiledSize == 65536 && compressed[0].linearSize != 0, "block compressed render target layout is wrong"); + Require(ComputeMipLayout(TextureTileMode::RenderTarget64KB, 132, 64, 64, 1).size() == 1, "format 132 render target layout is missing"); reject([] { ComputeMipLayout(TextureTileMode::RenderTarget64KB, 74, 64, 64, 1); }, "unsupported bytes per element"); reject([] { ComputeMipLayout(TextureTileMode::kLinear, 1, 0, 4, 1); }, "zero-sized texture"); diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index 4536f474b..dd73918e3 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -38,6 +38,7 @@ std::string FirstLine(const std::string& text) { bool g_disassemble = false; bool g_assembly = false; bool g_memory = false; +bool g_spirv = false; bool Replay(const char* path) { const auto request = ShaderRecompiler::RequestSerializer{}.Deserialize(ReadText(path)); @@ -45,6 +46,7 @@ bool Replay(const char* path) { if (request.request.context.compute.has_value()) { const auto& compute = *request.request.context.compute; std::printf(" compute: threads %ux%ux%u, lds %u dwords, group ids %d%d%d, tg size %d, thread id components %u\n", compute.numThreads[0], compute.numThreads[1], compute.numThreads[2], compute.ldsSizeDwords, compute.groupIdEnable[0], compute.groupIdEnable[1], compute.groupIdEnable[2], compute.tgSizeEnable, compute.threadIdComponentCount); + if (compute.PartialGroups()) std::printf(" partial groups: dispatch of %ux%ux%u threads\n", compute.partialThreads[0], compute.partialThreads[1], compute.partialThreads[2]); } if (g_memory) { // --mem: the captured inputs. User data words are printed; each memory region is written to @@ -77,6 +79,21 @@ bool Replay(const char* path) { try { const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); + if (g_spirv) { + std::string name(path); + name = name.substr(name.find_last_of("/\\") + 1) + ".spv"; + std::ofstream(name, std::ios::binary).write(reinterpret_cast(result.spirv.data()), static_cast(result.spirv.size() * sizeof(result.spirv[0]))); + } + if (g_memory) { + for (const auto& binding : result.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + for (std::size_t i = 0; i + 4 <= binding.guestDescriptor.size(); i += 4) { + const auto* v = binding.guestDescriptor.data() + i; + const bool written = i / 4 >= binding.bufferWritten.size() || binding.bufferWritten[i / 4]; + std::printf(" buffer %zu: V# %08x %08x %08x %08x%s\n", i / 4, v[0], v[1], v[2], v[3], written ? " written" : ""); + } + } + } #if ANYPS5_ENABLE_SPIRV_TOOLS if (g_disassemble) { spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_2); @@ -97,7 +114,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--mem] [--spv] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set\n"); return 2; } int failures = 0; @@ -114,6 +131,10 @@ int main(int argc, char** argv) { g_memory = true; continue; } + if (std::string(argv[i]) == "--spv") { + g_spirv = true; + continue; + } try { if (!Replay(argv[i])) ++failures; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 83bd7327e..6623b3914 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -27,5 +27,5 @@ target_include_directories(libSceAjm.native PRIVATE ${LIBS_INCLUDE_DIR} ${LIBATRAC9_DIR} ) -target_link_libraries(libSceAjm.native PRIVATE atrac9 libc libkernel) +target_link_libraries(libSceAjm.native PRIVATE atrac9 ffmpeg libc libkernel) configure_windows_unwind(libSceAjm.native) diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index c2150a64a..ff03fa3b4 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -33,18 +33,6 @@ int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint6 return 0; } -int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { - (void)info; - (void)instance; - (void)bitstream_input; - (void)bitstream_input_size; - (void)pcm_output; - (void)pcm_output_size; - (void)result; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { (void)info; (void)instance; @@ -114,20 +102,6 @@ int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instanc return 0; } -int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size) { - (void)info; - (void)instance; - (void)flags; - (void)data_input; - (void)data_input_size; - (void)data_output; - (void)data_output_size; - (void)sideband_output; - (void)sideband_output_size; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { (void)info; (void)instance; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 1bfc8f760..4d6207075 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -3,14 +3,21 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "libatrac9.h" +extern "C" { +#include +#include +} #include #include +#include #include #include #include #include #include #include +#include +#include #include // The batch buffer and its job records are private to this library: the guest only reserves the @@ -29,6 +36,7 @@ constexpr std::int32_t AJM_RESULT_PARTIAL_INPUT = 0x00000008; constexpr std::int32_t AJM_RESULT_NOT_ENOUGH_ROOM = 0x00000010; constexpr std::int32_t AJM_RESULT_CODEC_ERROR = 0x40000000; +constexpr std::uint32_t CODEC_MP3 = 0; constexpr std::uint32_t CODEC_AT9 = 1; // APS5_TRACE_AJM=1 logs every job the title submits and what the library writes back. @@ -89,9 +97,14 @@ struct Instance { std::uint64_t totalDecodedSamples = 0; SidebandGaplessDecode gapless{}; bool flagsReported = false; + AVCodecContext* mp3 = nullptr; + std::uint32_t mp3Channels = 0; + std::uint32_t mp3SampleRate = 0; + std::uint32_t mp3Bitrate = 0; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); + if (mp3) avcodec_free_context(&mp3); } }; @@ -223,21 +236,93 @@ bool consumedFirstRun(Instance& instance) { return true; } -// Decodes AT9 frames from the concatenated inputs into the concatenated outputs in the instance's PCM -// encoding. -void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { +std::vector JoinInputs(const JobHeader& job, const AjmBuffer* inputs) { std::vector input; for (std::uint32_t index = 0; index < job.inputCount; ++index) { const auto* data = static_cast(inputs[index].ptr); input.insert(input.end(), data, data + inputs[index].size); } - std::size_t outputCapacity = 0; - for (std::uint32_t index = 0; index < job.outputCount; ++index) outputCapacity += outputs[index].size; + return input; +} + +struct PcmOutputs { + const AjmBuffer* buffers; + std::uint32_t count; + std::size_t capacity = 0; + std::size_t produced = 0; + std::uint32_t index = 0; + std::size_t offset = 0; + + PcmOutputs(const AjmBuffer* buffers, std::uint32_t count) : buffers(buffers), count(count) { + for (std::uint32_t i = 0; i < count; ++i) capacity += buffers[i].size; + } + std::size_t Room() const { return capacity - produced; } + void Emit(const std::uint8_t* source, std::size_t bytes) { + while (bytes > 0 && index < count) { + const std::size_t chunk = std::min(buffers[index].size - offset, bytes); + std::memcpy(static_cast(buffers[index].ptr) + offset, source, chunk); + source += chunk; + bytes -= chunk; + produced += chunk; + offset += chunk; + if (offset == buffers[index].size) { + ++index; + offset = 0; + } + } + } +}; + +std::uint32_t PcmEncoding(const Instance& instance) { + return static_cast((instance.flags >> 7u) & 7u); +} + +void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, std::size_t samples, std::size_t channels, std::size_t sampleBytes) { + std::size_t first = 0; + std::size_t count = samples; + if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { + const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); + instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); + first += skip; + count -= skip; + } + if (instance.gapless.totalSamples != 0) { + const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; + count = static_cast(std::min(count, remaining)); + } + outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); + instance.totalDecodedSamples += count; +} + +void WriteRunSideband(const JobHeader& job, const Instance& instance, std::int32_t result, std::size_t consumed, std::size_t produced, std::uint32_t frames, const SidebandFormat& format) { + auto* sideband = static_cast(job.sideband); + std::size_t offset = 0; + const auto write = [&](const void* value, std::size_t size) { + if (!sideband || offset + size > job.sidebandSize) return; + std::memcpy(sideband + offset, value, size); + offset += size; + }; + const SidebandResult status{result, 0}; + write(&status, sizeof(status)); + if (job.flags & SIDEBAND_STREAM) { + const SidebandStream stream{static_cast(consumed), static_cast(produced), instance.totalDecodedSamples}; + write(&stream, sizeof(stream)); + } + if (job.flags & SIDEBAND_FORMAT) write(&format, sizeof(format)); + if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); + if (job.flags & RUN_MULTIPLE_FRAMES) { + const SidebandMultipleFrames multiple{frames, 0}; + write(&multiple, sizeof(multiple)); + } +} + +void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + const auto input = JoinInputs(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); const auto channels = static_cast(instance.info.channels); const auto frameSamples = static_cast(instance.info.frameSamples); - // Instance flags bits 7..9 select the PCM encoding: 0 16-bit, 1 32-bit integer, 2 float. - const auto encoding = static_cast((instance.flags >> 7u) & 7u); + const auto encoding = PcmEncoding(instance); const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); const std::size_t frameBytes = frameSamples * channels * sampleBytes; std::vector pcm(frameBytes); @@ -247,25 +332,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c std::int32_t result = 0; int decodeStatus = 0; std::size_t consumed = 0; - std::size_t produced = 0; std::uint32_t frames = 0; - std::uint32_t outputIndex = 0; - std::size_t outputOffset = 0; - const auto emit = [&](const std::uint8_t* source, std::size_t bytes) { - while (bytes > 0 && outputIndex < job.outputCount) { - const std::size_t room = outputs[outputIndex].size - outputOffset; - const std::size_t chunk = std::min(room, bytes); - std::memcpy(static_cast(outputs[outputIndex].ptr) + outputOffset, source, chunk); - source += chunk; - bytes -= chunk; - produced += chunk; - outputOffset += chunk; - if (outputOffset == outputs[outputIndex].size) { - ++outputIndex; - outputOffset = 0; - } - } - }; // A superframe is a fixed superframeSize bytes holding framesInSuperframe frames packed back to back, // each byte aligned but of its own size (the encoder moves bits between the frames of a superframe), @@ -282,7 +349,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (outputCapacity - produced < frameBytes) { + if (pcmOutputs.Room() < frameBytes) { if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } @@ -310,54 +377,132 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c } ++frames; - std::size_t first = 0; - std::size_t count = frameSamples; - if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { - const std::size_t skip = std::min(count, instance.gapless.skipSamples - instance.gapless.skippedSamples); - instance.gapless.skippedSamples = static_cast(instance.gapless.skippedSamples + skip); - first += skip; - count -= skip; - } - if (instance.gapless.totalSamples != 0) { - const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; - count = static_cast(std::min(count, remaining)); - } - emit(pcm.data() + first * channels * sampleBytes, count * channels * sampleBytes); - instance.totalDecodedSamples += count; + EmitFrame(instance, pcmOutputs, pcm.data(), frameSamples, channels, sampleBytes); if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } if (TraceEnabled()) { std::fprintf(stderr, "[ajm] instance %u run flags 0x%llx: %zu input bytes in %u buffers, %zu output bytes in %u buffers, sideband %llu bytes; superframe %d (%d frames), %d channels, %d samples/frame, %d Hz, config %02x %02x %02x %02x -> result 0x%x, decoder status 0x%x, %u frames, consumed %zu, produced %zu, total samples %llu, gapless total %u skip %u skipped %u, input", - job.instance, static_cast(job.flags), input.size(), job.inputCount, outputCapacity, job.outputCount, static_cast(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate, - instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast(result), static_cast(decodeStatus), frames, consumed, produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); + job.instance, static_cast(job.flags), input.size(), job.inputCount, pcmOutputs.capacity, job.outputCount, static_cast(job.sidebandSize), instance.info.superframeSize, instance.info.framesInSuperframe, instance.info.channels, instance.info.frameSamples, instance.info.samplingRate, + instance.info.configData[0], instance.info.configData[1], instance.info.configData[2], instance.info.configData[3], static_cast(result), static_cast(decodeStatus), frames, consumed, pcmOutputs.produced, static_cast(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); for (std::size_t i = 0; i < input.size() && i < 16; ++i) std::fprintf(stderr, " %02x", input[i]); std::fprintf(stderr, "\n"); } - auto* sideband = static_cast(job.sideband); - std::size_t offset = 0; - const auto write = [&](const void* value, std::size_t size) { - if (!sideband || offset + size > job.sidebandSize) return; - std::memcpy(sideband + offset, value, size); - offset += size; - }; - const SidebandResult status{result, 0}; - write(&status, sizeof(status)); - if (job.flags & SIDEBAND_STREAM) { - const SidebandStream stream{static_cast(consumed), static_cast(produced), instance.totalDecodedSamples}; - write(&stream, sizeof(stream)); - } - if (job.flags & SIDEBAND_FORMAT) { - const SidebandFormat format{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}; - write(&format, sizeof(format)); - } - if (job.flags & SIDEBAND_GAPLESS_DECODE) write(&instance.gapless, sizeof(instance.gapless)); - if (job.flags & RUN_MULTIPLE_FRAMES) { - const SidebandMultipleFrames multiple{frames, 0}; - write(&multiple, sizeof(multiple)); - } + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{static_cast(channels), ChannelMask(channels), static_cast(instance.info.samplingRate), encoding, 0, 0}); } +struct Mp3Frame { + std::size_t bytes; + std::uint32_t channels; + std::uint32_t samples; + std::uint32_t sampleRate; + std::uint32_t bitrate; +}; + +bool ParseMp3Frame(const std::uint8_t* data, std::size_t size, Mp3Frame& frame) { + if (size < 4) return false; + const std::uint32_t header = (std::uint32_t{data[0]} << 24u) | (std::uint32_t{data[1]} << 16u) | (std::uint32_t{data[2]} << 8u) | data[3]; + const auto version = (header >> 19u) & 3u; + const auto layer = (header >> 17u) & 3u; + const auto bitrateIndex = (header >> 12u) & 15u; + const auto rateIndex = (header >> 10u) & 3u; + if ((header & 0xFFE00000u) != 0xFFE00000u || version == 1 || layer != 1 || bitrateIndex == 0 || bitrateIndex == 15 || rateIndex == 3) return false; + static constexpr std::uint32_t rates[4][3] = {{11025, 12000, 8000}, {0, 0, 0}, {22050, 24000, 16000}, {44100, 48000, 32000}}; + static constexpr std::uint32_t mpeg1Kbps[16] = {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}; + static constexpr std::uint32_t mpeg2Kbps[16] = {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}; + const bool mpeg1 = version == 3; + frame.sampleRate = rates[version][rateIndex]; + frame.bitrate = (mpeg1 ? mpeg1Kbps : mpeg2Kbps)[bitrateIndex] * 1000u; + frame.samples = mpeg1 ? 1152u : 576u; + frame.channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; + frame.bytes = (mpeg1 ? 144u : 72u) * frame.bitrate / frame.sampleRate + ((header >> 9u) & 1u); + return true; +} + +void OpenMp3(Instance& instance) { + static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); + (void)quiet; + const AVCodec* codec = avcodec_find_decoder(AV_CODEC_ID_MP3); + if (instance.mp3) avcodec_free_context(&instance.mp3); + instance.mp3 = codec ? avcodec_alloc_context3(codec) : nullptr; + if (!instance.mp3 || avcodec_open2(instance.mp3, codec, nullptr) < 0) throw std::runtime_error("AJM: cannot open the FFmpeg MP3 decoder"); +} + +void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { + const auto input = JoinInputs(job, inputs); + PcmOutputs pcmOutputs(outputs, job.outputCount); + const auto encoding = PcmEncoding(instance); + if (encoding > 2) throw std::runtime_error("AJM MP3: PCM encoding " + std::to_string(encoding) + " is not implemented"); + const std::size_t sampleBytes = encoding == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + std::unique_ptr packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); }); + std::unique_ptr decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); }); + if (!packet || !decoded) throw std::bad_alloc(); + std::vector pcm; + + std::int32_t result = 0; + std::size_t consumed = 0; + std::uint32_t frames = 0; + while (consumed < input.size()) { + Mp3Frame frame{}; + if (!ParseMp3Frame(input.data() + consumed, input.size() - consumed, frame)) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + if (input.size() - consumed < frame.bytes) { + result |= AJM_RESULT_PARTIAL_INPUT; + break; + } + if (pcmOutputs.Room() < frame.samples * frame.channels * sampleBytes) { + if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + break; + } + packet->data = const_cast(input.data() + consumed); + packet->size = static_cast(frame.bytes); + if (avcodec_send_packet(instance.mp3, packet.get()) < 0) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + consumed += frame.bytes; + ++frames; + instance.mp3Channels = frame.channels; + instance.mp3SampleRate = frame.sampleRate; + instance.mp3Bitrate = frame.bitrate; + int status = 0; + while ((status = avcodec_receive_frame(instance.mp3, decoded.get())) == 0) { + if (decoded->format != AV_SAMPLE_FMT_FLTP) throw std::runtime_error("AJM MP3: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); + const auto channels = static_cast(decoded->ch_layout.nb_channels); + const auto samples = static_cast(decoded->nb_samples); + pcm.resize(samples * channels * sampleBytes); + for (std::size_t sample = 0; sample < samples; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) { + const float value = reinterpret_cast(decoded->extended_data[channel])[sample]; + auto* out = pcm.data() + (sample * channels + channel) * sampleBytes; + if (encoding == 0) { + const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); + std::memcpy(out, &converted, sizeof(converted)); + } else if (encoding == 1) { + const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); + std::memcpy(out, &converted, sizeof(converted)); + } else { + std::memcpy(out, &value, sizeof(value)); + } + } + } + EmitFrame(instance, pcmOutputs, pcm.data(), samples, channels, sampleBytes); + av_frame_unref(decoded.get()); + } + if (status != AVERROR(EAGAIN)) { + result |= AJM_RESULT_INVALID_DATA; + break; + } + if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; + } + if (consumed == input.size() && frames == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; + + AJM_TRACE("[ajm] instance %u mp3 run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, %u channels, %u Hz, %u bps, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, + instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast(instance.totalDecodedSamples)); + WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, SidebandFormat{instance.mp3Channels, ChannelMask(instance.mp3Channels), instance.mp3SampleRate, encoding, instance.mp3Bitrate, 0}); +} void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (job.kind == JobKind::GetStatistics) { AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast(job.sidebandSize)); @@ -383,6 +528,7 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out instance->totalDecodedSamples = 0; instance->gapless.skippedSamples = 0; if (instance->decoder && instance->initialized) ResetDecoder(*instance); + if (instance->mp3) avcodec_flush_buffers(instance->mp3); WriteResult(job.sideband, job.sidebandSize, 0); break; case JobKind::SetGaplessDecode: { @@ -401,9 +547,10 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_NOT_INITIALIZED); } else if (instance->codec == CODEC_AT9) { RunAt9(*instance, job, inputs, outputs); + } else if (instance->codec == CODEC_MP3) { + RunMp3(*instance, job, inputs, outputs); } else { - AJM_TRACE("[ajm] instance %u run on codec %u without a decoder\n", job.instance, instance->codec); - WriteResult(job.sideband, job.sidebandSize, AJM_RESULT_CODEC_ERROR); + throw std::runtime_error("AJM: decoding codec " + std::to_string(instance->codec) + " is not implemented"); } break; default: @@ -458,12 +605,13 @@ int APS5_VABI sceAjmMemoryUnregister(uint32_t context, void* ptr) { int APS5_VABI sceAjmInstanceCreate(uint32_t context, uint32_t codec, uint64_t flags, uint32_t* instance) { (void)context; if (!instance) return SCE_AJM_ERROR_INVALID_PARAMETER; - static std::atomic reportedCodecs{0}; - if (codec != CODEC_AT9 && codec < 32 && (reportedCodecs.fetch_or(1u << codec) & (1u << codec)) == 0) - std::fprintf(stderr, "[ajm] codec %u has no decoder; its jobs report codec errors\n", codec); auto created = std::make_unique(); created->codec = codec; created->flags = flags; + if (codec == CODEC_MP3) { + OpenMp3(*created); + created->initialized = true; + } const std::uint32_t id = (codec << 14) | (g_nextInstance.fetch_add(1, std::memory_order_relaxed) & 0x3FFF); std::lock_guard lock(g_lock); g_instances[id] = std::move(created); @@ -532,6 +680,16 @@ int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint return Append(info, header, input_buffers, output_buffers); } +int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* data_input, size_t data_input_size, void* data_output, size_t data_output_size, void* sideband_output, size_t sideband_output_size) { + const AjmBuffer input{const_cast(data_input), data_input_size}; + const AjmBuffer output{data_output, data_output_size}; + return sceAjmBatchJobRunSplit(info, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size); +} + +int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { + return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); +} + int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, void* result) { (void)interval; return Append(info, MakeHeader(JobKind::GetStatistics, 0, result, 24), nullptr, nullptr); diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index 9469c86d6..3e38e5ad2 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -2,12 +2,30 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include +#include +#include +#include static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast(0x80D90002); static constexpr uint32_t APPPARAM_ID_SKU_FLAG = 1; static constexpr int32_t SKU_FLAG_FULL = 3; +static constexpr char TEMPORARY_MOUNT_POINT[] = "/temp0"; +static constexpr char DOWNLOAD_MOUNT_POINT[] = "/download0"; +static constexpr uint32_t TEMPORARY_DATA_OPTION_FORMAT = 1; + +static std::filesystem::path TemporaryDirectory(const AppContentMountPoint* mount_point) { + if (!mount_point || std::strncmp(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + return ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); +} + +static void ClearDirectory(const std::filesystem::path& directory) { + for (const auto& entry : std::filesystem::directory_iterator(directory)) std::filesystem::remove_all(entry.path()); +} + extern "C" { int APS5_VABI sceAppContentAddcontMount(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentMountPoint* mount_point) { @@ -39,10 +57,19 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { } int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - (void)mount_point; - (void)available_space_kb; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; + const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u; + if (quotaKb == 0) throw std::logic_error(std::string(__func__) + ": the title declares no download data"); + const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT); + std::filesystem::create_directories(directory); + std::uint64_t usedKb = 0; + for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { + if (entry.is_regular_file()) usedKb += (entry.file_size() + 1023u) / 1024u; + } + const std::uint64_t hostKb = std::filesystem::space(directory).available / 1024u; + *available_space_kb = static_cast(std::min(quotaKb - std::min(quotaKb, usedKb), hostKb)); + return 0; } int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, AppContentBootParam* boot_param) { @@ -53,23 +80,25 @@ int APS5_VABI sceAppContentInitialize(const AppContentInitParam* init_param, App } int APS5_VABI sceAppContentTemporaryDataFormat(const AppContentMountPoint* mount_point) { - (void)mount_point; - NotImplemented_nid_no_patch(__func__); - return 0; + ClearDirectory(TemporaryDirectory(mount_point)); + return 0; } int APS5_VABI sceAppContentTemporaryDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) { - (void)mount_point; - (void)available_space_kb; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER; + *available_space_kb = static_cast(std::filesystem::space(TemporaryDirectory(mount_point)).available / 1024); + return 0; } int APS5_VABI sceAppContentTemporaryDataMount2(uint32_t option, AppContentMountPoint* mount_point) { - (void)option; - (void)mount_point; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (option > TEMPORARY_DATA_OPTION_FORMAT) throw std::invalid_argument(std::string(__func__) + ": unknown option " + std::to_string(option)); + std::memset(mount_point->data, 0, sizeof(mount_point->data)); + std::memcpy(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(TEMPORARY_MOUNT_POINT)); + const auto directory = TemporaryDirectory(mount_point); + std::filesystem::create_directories(directory); + if (option == TEMPORARY_DATA_OPTION_FORMAT) ClearDirectory(directory); + return 0; } } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 16d7901f5..04a0fc850 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -329,6 +331,8 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len if (type != PORT_TYPE_VIBRATION) { openDevice(port); } + static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; + if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); return i + 1; } } @@ -490,4 +494,9 @@ int APS5_VABI sceAudioOutGetPortState(int handle, AudioOutPortState* state) { return 0; } +int APS5_VABI sceAudioOutSetMixLevelPadSpk(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 611d3e726..0ee974536 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -46,13 +46,9 @@ struct AudioOut2Port { std::uint32_t dataFormat = 0; std::uint32_t samplingFreq = 0; std::uint32_t flags = 0; - // Channel count decoded from dataFormat; 0 when the format is not understood (the port is then - // not rendered). Samples are float. std::uint32_t channels = 0; - // The PCM buffer (one grain, float, interleaved) the title last handed over through the data - // attribute. It is guest memory the title rewrites every tick, so it is read when the context - // mixes, not when it is set. - const float* data = nullptr; + bool int16 = false; + const void* data = nullptr; float volume[AUDIO_OUT2_PORT_CHANNELS_MAX] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::uint64_t dataSets = 0; std::uint64_t attributeTraces = 0; diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index 546ac704e..2cc479fe1 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -1,7 +1,10 @@ #include #include #include +#include #include +#include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -19,11 +22,9 @@ static constexpr std::uint64_t ATTRIBUTE_TRACE_EVERY = 2000; static constexpr std::uint32_t ATTRIBUTE_DATA = 0; static constexpr std::uint32_t ATTRIBUTE_VOLUME = 1; -// data_format bits 8..11 carry the channel count: the title opens 0x100 (mono object ports), -// 0x200 (stereo) and 0x880 (7.1 bed). The buffers behind them are float, as their spacing shows -// (1024 bytes per 256-sample mono grain). The low byte's meaning is not known. static constexpr std::uint32_t FORMAT_CHANNELS_SHIFT = 8; static constexpr std::uint32_t FORMAT_CHANNELS_MASK = 0xFu; +static constexpr std::uint32_t FORMAT_TYPE_MASK = 0x7Fu; static std::mutex g_portsLock; // Grows on demand: the title opens its bed ports plus max_object_ports object ports at once. @@ -42,8 +43,12 @@ static constexpr float DOWNMIX_GAIN = 0.7071f; static void AccumulatePort(const AudioOut2Port& port, float* out, std::uint32_t frames) { const auto ch = port.channels; const float* volume = port.volume; + float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { - const float* in = port.data + static_cast(frame) * ch; + const auto first = static_cast(frame) * ch; + for (std::uint32_t c = 0; c < ch; c++) { + in[c] = port.int16 ? static_cast(port.data)[first + c] / 32768.0f : static_cast(port.data)[first + c]; + } float left = 0.0f; float right = 0.0f; if (ch == 1) { @@ -116,7 +121,12 @@ int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut2 entry.samplingFreq = params->sampling_freq; entry.flags = params->flags; entry.channels = (params->data_format >> FORMAT_CHANNELS_SHIFT) & FORMAT_CHANNELS_MASK; - if (entry.channels > AUDIO_OUT2_PORT_CHANNELS_MAX) entry.channels = 0; + const auto sampleType = params->data_format & FORMAT_TYPE_MASK; + if (entry.channels == 0 || entry.channels > AUDIO_OUT2_PORT_CHANNELS_MAX || sampleType > 1) { + entry = AudioOut2Port{}; + throw std::runtime_error("sceAudioOut2PortCreate: data format 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%x", params->data_format); return std::string(text); }() + " is not implemented"); + } + entry.int16 = sampleType == 1; *port = static_cast(index) + 1; AUDIOOUT2_TRACE("t=%.3f PortCreate ctx=%llx -> port %llu: type=0x%x data_format=0x%x (%u float ch) sampling_freq=%u flags=0x%x user=%llx\n", AudioOut2TraceSeconds(), static_cast(ctx), static_cast(*port), params->port_type, params->data_format, diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp index 5f9850726..9185d192e 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp @@ -1,30 +1,101 @@ +#include +#include #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +struct GuestVbapParams { + const AudioOut2Position* positions; + std::uint32_t numSpeakers; + std::uint8_t is3d; + void* memory; + std::size_t memorySize; + std::uint32_t reserved; +}; + +constexpr std::uint32_t MaxSpeakers = 32; +constexpr std::uint32_t AmbisonicsChannelFlag = 0x40; +constexpr std::uint32_t MaxAmbisonicsOrder = 5; + +struct SpeakerArray { + std::uint32_t magic; + std::uint32_t numSpeakers; + AudioOut2Position directions[MaxSpeakers]; +}; +constexpr std::uint32_t SpeakerArrayMagic = 0x53504b41; + +SpeakerArray& Array(AudioOut2SpeakerArrayHandle handle) { + auto* array = static_cast(handle); + if (array == nullptr || array->magic != SpeakerArrayMagic) throw std::invalid_argument("invalid speaker array handle"); + return *array; +} + +float Sn3d(std::uint32_t acn, const AudioOut2Position& direction) { + const int n = static_cast(std::sqrt(static_cast(acn))); + const int m = static_cast(acn) - n * n - n; + const int am = std::abs(m); + const double length = std::sqrt(double(direction.x) * direction.x + double(direction.y) * direction.y + double(direction.z) * direction.z); + const double sinEl = length > 0 ? direction.z / length : 0; + const double cosEl = std::sqrt(std::max(0.0, 1 - sinEl * sinEl)); + const double azimuth = std::atan2(direction.y, direction.x); + double legendre = 1; + for (int i = 1; i <= am; ++i) legendre *= (2 * i - 1) * cosEl; + if (n > am) { + double previous = legendre; + double current = sinEl * (2 * am + 1) * legendre; + for (int l = am + 2; l <= n; ++l) { + const double next = (sinEl * (2 * l - 1) * current - (l + am - 1) * previous) / (l - am); + previous = current; + current = next; + } + legendre = current; + } + double ratio = 1; + for (int i = n - am + 1; i <= n + am; ++i) ratio /= i; + const double norm = std::sqrt((m == 0 ? 1.0 : 2.0) * ratio); + return static_cast(norm * legendre * (m < 0 ? std::sin(am * azimuth) : std::cos(am * azimuth))); +} + +} + extern "C" { int APS5_VABI sceAudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle, const void* vbap_params, const void* ambi_params) { - (void)handle; - (void)vbap_params; - (void)ambi_params; - NotImplemented_nid_no_patch(__func__); + const auto* params = static_cast(vbap_params); + if (handle == nullptr || params == nullptr || ambi_params == nullptr || params->positions == nullptr || + params->numSpeakers == 0 || params->numSpeakers > MaxSpeakers || params->memory == nullptr || + params->memorySize < sizeof(SpeakerArray) || reinterpret_cast(params->memory) % alignof(SpeakerArray) != 0) { + APS5_INVALID_ARG_EX; + } + auto* array = new (params->memory) SpeakerArray{SpeakerArrayMagic, params->numSpeakers, {}}; + std::copy_n(params->positions, params->numSpeakers, array->directions); + *handle = array; return 0; } int APS5_VABI sceAudioOut2SpeakerArrayDestroy(AudioOut2SpeakerArrayHandle handle) { - (void)handle; - NotImplemented_nid_no_patch(__func__); + Array(handle).magic = 0; return 0; } int APS5_VABI sceAudioOut2GetSpeakerArrayAmbisonicsCoefficients(AudioOut2SpeakerArrayHandle handle, uint32_t ambisonics_channel, float* coefficients, uint32_t num_coefficients) { - (void)handle; - (void)ambisonics_channel; - (void)coefficients; - (void)num_coefficients; - NotImplemented_nid_no_patch(__func__); + const auto& array = Array(handle); + if (coefficients == nullptr || num_coefficients != array.numSpeakers) APS5_INVALID_ARG_EX; + const std::uint32_t acn = ambisonics_channel & ~AmbisonicsChannelFlag; + if ((ambisonics_channel & AmbisonicsChannelFlag) == 0 || acn >= (MaxAmbisonicsOrder + 1) * (MaxAmbisonicsOrder + 1)) { + throw std::invalid_argument("unsupported ambisonics channel " + std::to_string(ambisonics_channel)); + } + const auto order = static_cast(std::sqrt(static_cast(acn))); + const std::uint32_t decodable = std::max(1, (array.numSpeakers - 1) / 2); + for (std::uint32_t speaker = 0; speaker < array.numSpeakers; ++speaker) { + coefficients[speaker] = order > decodable ? 0.0f : Sn3d(acn, array.directions[speaker]) / static_cast(array.numSpeakers); + } return 0; } @@ -41,11 +112,10 @@ int APS5_VABI sceAudioOut2GetSpeakerArrayCoefficients(AudioOut2SpeakerArrayHandl } size_t APS5_VABI sceAudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d, uint8_t is_ambisonics) { - (void)num_speakers; (void)is_3d; (void)is_ambisonics; - NotImplemented_nid_no_patch(__func__); - return 0; + if (num_speakers == 0 || num_speakers > MaxSpeakers) APS5_INVALID_ARG_EX; + return sizeof(SpeakerArray); } int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo* info, uint32_t flags) { diff --git a/core/libs/prx/libSceContentDelete/Export.cpp b/core/libs/prx/libSceContentDelete/Export.cpp index 74793082b..16dd322fb 100644 --- a/core/libs/prx/libSceContentDelete/Export.cpp +++ b/core/libs/prx/libSceContentDelete/Export.cpp @@ -2,13 +2,32 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { +std::atomic g_initialized{false}; +} extern "C" { int APS5_VABI sceContentDeleteInitialize(const ContentDeleteInitParam* init_param) { - (void)init_param; + if (init_param == nullptr) APS5_INVALID_ARG_EX; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + return 0; +} + +int APS5_VABI sceContentDeleteByPath(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceContentDeleteTerminate(void) { + bool expected = true; + if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + return 0; +} + } diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index 124b82263..aac09f3d1 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -2,13 +2,47 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { +std::atomic g_initialized{false}; +} extern "C" { int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param) { - (void)init_param; + if (init_param == nullptr) APS5_INVALID_ARG_EX; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + return 0; +} + +int APS5_VABI sceContentExportFinish(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceContentExportFromFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportFromFileWithThumbnail(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentExportTerm(void) { + bool expected = true; + if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + return 0; +} + } diff --git a/core/libs/prx/libSceContentSearch/Export.cpp b/core/libs/prx/libSceContentSearch/Export.cpp index bb6042a3c..cfb70402a 100644 --- a/core/libs/prx/libSceContentSearch/Export.cpp +++ b/core/libs/prx/libSceContentSearch/Export.cpp @@ -2,13 +2,52 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { +std::atomic g_initialized{false}; +} extern "C" { int APS5_VABI sceContentSearchInit(const ContentSearchInitParam* init_param) { - (void)init_param; + if (init_param == nullptr) APS5_INVALID_ARG_EX; + bool expected = false; + if (!g_initialized.compare_exchange_strong(expected, true)) throw std::logic_error(std::string(__func__) + ": already initialized"); + return 0; +} + +int APS5_VABI sceContentSearchCloseMetadata(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceContentSearchGetMetadataFieldInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentSearchGetMetadataValue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentSearchOpenMetadata(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentSearchSearchContent(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceContentSearchTerm(void) { + bool expected = true; + if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); + return 0; +} + } diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index 8322715b2..be247e398 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -6,14 +6,12 @@ extern "C" { int APS5_VABI sceGameLiveStreamingInitialize(size_t heap_size) { - (void)heap_size; - NotImplemented_nid_no_patch(__func__); - return 0; + if (heap_size == 0) APS5_INVALID_ARG_EX; + return 0; } int APS5_VABI sceGameLiveStreamingTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0; } } diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index a9bdabf45..b9d2bda7d 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -1,8 +1,19 @@ #include #include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +std::mutex g_keyboardMutex; +std::set g_openKeyboards; + +} + extern "C" { int APS5_VABI sceImeClose_nid_postfix(void) { @@ -19,8 +30,8 @@ int APS5_VABI sceImeGetPanelSize(const Param* param, uint32_t* width, uint32_t* } int APS5_VABI sceImeKeyboardClose(int32_t user_id) { - (void)user_id; - NotImplemented_nid_no_patch(__func__); + std::lock_guard lock(g_keyboardMutex); + if (g_openKeyboards.erase(user_id) == 0) throw std::logic_error("sceImeKeyboardClose: keyboard not open for user " + std::to_string(user_id)); return 0; } @@ -39,9 +50,9 @@ int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArr } int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { - (void)user_id; - (void)param; - NotImplemented_nid_no_patch(__func__); + if (!param) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_keyboardMutex); + if (!g_openKeyboards.insert(user_id).second) throw std::logic_error("sceImeKeyboardOpen: keyboard already open for user " + std::to_string(user_id)); return 0; } @@ -85,8 +96,7 @@ int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geome } int APS5_VABI sceImeUpdate(EventHandler handler) { - (void)handler; - NotImplemented_nid_no_patch(__func__); + if (!handler) APS5_INVALID_ARG_EX; return 0; } diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index dbe06e1d1..b2b70f0aa 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -1,10 +1,44 @@ +#include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { +std::atomic g_status{0}; +} + extern "C" { int SceMsgDialogNativeModuleLoaded_nid_no_patch = 1; +int APS5_VABI sceMsgDialogGetResult(void* result) { + (void)result; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMsgDialogInitialize(void) { + int expected = 0; + if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceMsgDialogInitialize: already initialized"); + return 0; +} + +int APS5_VABI sceMsgDialogOpen(const void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceMsgDialogTerminate(void) { + int expected = 1; + if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceMsgDialogTerminate: not initialized or still running"); + return 0; +} + +int APS5_VABI sceMsgDialogUpdateStatus(void) { + return g_status.load(); +} + } diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index e56b3e859..e9d65b9be 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -65,4 +65,14 @@ int APS5_VABI sceNpCommerceDialogTerminate() { return 0; } +int APS5_VABI sceNpCommerceHidePsStoreIcon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpCommerceShowPsStoreIcon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 1e31e7b5a..695705242 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -2,27 +2,85 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif -// No add-on content is installed; the base game is the full SKU. static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER = static_cast(0x80558003); static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_NOT_FOUND = static_cast(0x80558007); static constexpr uint32_t SKU_FLAG_FULL = 3; +namespace { + +std::filesystem::path EntitlementsPath() { + if (const char* configured = std::getenv("ANYPS5_ENTITLEMENTS"); configured != nullptr && configured[0] != '\0') return configured; +#ifdef _WIN32 + wchar_t module[MAX_PATH]; + const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); + if (length == 0 || length == MAX_PATH) throw std::runtime_error("NpEntitlementAccess: cannot locate the executable"); + return std::filesystem::path(module).parent_path() / "anyps5-entitlements.ini"; +#else + return std::filesystem::read_symlink("/proc/self/exe").parent_path() / "anyps5-entitlements.ini"; +#endif +} + +const std::vector& OwnedAddons() { + static const auto owned = [] { + std::vector addons; + const auto path = EntitlementsPath(); + std::ifstream file(path); + if (!file) { + if (std::getenv("ANYPS5_ENTITLEMENTS") != nullptr || std::filesystem::exists(path)) throw std::runtime_error("NpEntitlementAccess: cannot read " + path.string()); + return addons; + } + std::string line; + while (std::getline(file, line)) { + line.erase(std::min(line.find_first_of("#;"), line.size())); + const auto first = line.find_first_not_of(" \t\r"); + if (first == std::string::npos) continue; + const auto label = line.substr(first, line.find_last_not_of(" \t\r") + 1 - first); + NpEntitlementAccessAddcontEntitlementInfo info{}; + if (label.size() >= sizeof(info.entitlement_label.data)) throw std::runtime_error("NpEntitlementAccess: entitlement label '" + label + "' in " + path.string() + " is longer than 16 characters"); + std::memcpy(info.entitlement_label.data, label.data(), label.size()); + info.package_type = 3; + info.download_status = 4; + addons.push_back(info); + } + return addons; + }(); + return owned; +} + +} + extern "C" { int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfo(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, NpEntitlementAccessAddcontEntitlementInfo* info) { (void)service_label; - (void)entitlement_label; - (void)info; + if (!entitlement_label || !info) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; + for (const auto& addon : OwnedAddons()) { + if (std::strncmp(addon.entitlement_label.data, entitlement_label->data, sizeof(entitlement_label->data)) == 0) { + *info = addon; + return 0; + } + } return SCE_NP_ENTITLEMENT_ACCESS_ERROR_NOT_FOUND; } int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoList(uint32_t service_label, NpEntitlementAccessAddcontEntitlementInfo* list, uint32_t list_num, uint32_t* hit_num) { (void)service_label; - (void)list; - (void)list_num; - if (!hit_num) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; - *hit_num = 0; + if (!hit_num || (!list && list_num != 0)) return SCE_NP_ENTITLEMENT_ACCESS_ERROR_PARAMETER; + const auto& owned = OwnedAddons(); + *hit_num = static_cast(owned.size()); + std::copy_n(owned.begin(), std::min(list_num, owned.size()), list); return 0; } @@ -38,4 +96,29 @@ int APS5_VABI sceNpEntitlementAccessInitialize(const NpEntitlementAccessInitPara return 0; } +int APS5_VABI sceNpEntitlementAccessAbortRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessDeleteRequest(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollConsumeEntitlement(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestConsumeUnifiedEntitlement(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index dac4c2cfa..491eebc0c 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -9,6 +9,7 @@ static constexpr int SCE_NP_ERROR_INVALID_ARGUMENT = static_cast(0x80550003 static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; +static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; static std::atomic g_nextRequest{1}; @@ -87,10 +88,10 @@ int APS5_VABI sceNpGetNpId(int user_id, NpId* np_id) { } int APS5_VABI sceNpGetNpReachabilityState(int user_id, uint32_t* state) { - (void)user_id; - (void)state; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + if (!state) return SCE_NP_ERROR_INVALID_ARGUMENT; + *state = NP_REACHABILITY_STATE_UNAVAILABLE; + return 0; } int APS5_VABI sceNpGetOnlineId(int user_id, NpOnlineId* online_id) { @@ -107,10 +108,10 @@ int APS5_VABI sceNpGetState(int user_id, uint32_t* state) { } int APS5_VABI sceNpHasSignedUp(int user_id, bool* has_signed_up) { - (void)user_id; - (void)has_signed_up; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + if (!has_signed_up) return SCE_NP_ERROR_INVALID_ARGUMENT; + *has_signed_up = false; + return 0; } int APS5_VABI sceNpPollAsync(int req_id, int* result) { @@ -126,31 +127,27 @@ void APS5_VABI sceNpRegisterGamePresenceCallback(void* callback, void* userdata) } int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)userdata; + if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpRegisterPlusEventCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)userdata; + if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)userdata; + if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) { - (void)callback; - (void)userdata; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)userdata; + if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + return 0; } int APS5_VABI sceNpSetContentRestriction(const NpContentRestriction* restriction) { @@ -166,8 +163,7 @@ int APS5_VABI sceNpSetNpTitleId(const NpTitleId* title_id, const NpTitleSecret* } int APS5_VABI sceNpUnregisterStateCallback(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0; } int APS5_VABI sceNpGetAccountLanguage2(int req_id, int user_id, void* language) { @@ -193,4 +189,8 @@ int APS5_VABI sceNpUnregisterStateCallbackA(int callback_id) { return 0; } +int APS5_VABI sceNpUnregisterPremiumEventCallback(void) { + return 0; +} + } diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index ef15719ce..a6150edc9 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -7,4 +7,29 @@ extern "C" { int SceNpUtilityModuleLoaded_nid_no_patch = 1; +int APS5_VABI sceNpBandwidthTestAbort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBandwidthTestGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBandwidthTestInitStartDownload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBandwidthTestInitStartUpload(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpBandwidthTestShutdown(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePlayerInvitationDialog/Export.cpp b/core/libs/prx/libScePlayerInvitationDialog/Export.cpp new file mode 100644 index 000000000..50ecd53dc --- /dev/null +++ b/core/libs/prx/libScePlayerInvitationDialog/Export.cpp @@ -0,0 +1,18 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePlayerInvitationDialogTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePlayerInvitationDialogUpdateStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePlayerSelectionDialog/Export.cpp b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp new file mode 100644 index 000000000..ef0798b7b --- /dev/null +++ b/core/libs/prx/libScePlayerSelectionDialog/Export.cpp @@ -0,0 +1,13 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI scePlayerSelectionDialogTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 7c7110e30..27bfe2560 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -10,4 +10,22 @@ uint32_t APS5_VABI sceRazorCpuIsCapturing(void) { return 0; } +int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args) { + (void)args; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuJobManagerJob(const void* args) { + (void)args; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRazorCpuJobManagerSequence(const void* args) { + (void)args; + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceRemoteplay/Export.cpp b/core/libs/prx/libSceRemoteplay/Export.cpp index cf953dfcc..1c3e5bbad 100644 --- a/core/libs/prx/libSceRemoteplay/Export.cpp +++ b/core/libs/prx/libSceRemoteplay/Export.cpp @@ -1,26 +1,27 @@ #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +constexpr int REMOTEPLAY_CONNECTION_STATUS_DISCONNECT = 0; + extern "C" { int APS5_VABI sceRemoteplayGetConnectionStatus(int user_id, int* status) { (void)user_id; - (void)status; - NotImplemented_nid_no_patch(__func__); + if (!status) APS5_INVALID_ARG_EX; + *status = REMOTEPLAY_CONNECTION_STATUS_DISCONNECT; return 0; } int APS5_VABI sceRemoteplayInitialize(void* heap, size_t heap_size) { - (void)heap; - (void)heap_size; - NotImplemented_nid_no_patch(__func__); + if (!heap || heap_size == 0) APS5_INVALID_ARG_EX; return 0; } int APS5_VABI sceRemoteplayTerminate(void) { - NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceSharePlay/Export.cpp b/core/libs/prx/libSceSharePlay/Export.cpp index 03970342a..d0289f4bb 100644 --- a/core/libs/prx/libSceSharePlay/Export.cpp +++ b/core/libs/prx/libSceSharePlay/Export.cpp @@ -6,15 +6,12 @@ extern "C" { int APS5_VABI sceSharePlayInitialize(void* heap, size_t heap_size) { - (void)heap; - (void)heap_size; - NotImplemented_nid_no_patch(__func__); - return 0; + if (heap == nullptr || heap_size == 0) APS5_INVALID_ARG_EX; + return 0; } int APS5_VABI sceSharePlayTerminate(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return 0; } } diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 9ff7183b3..3778c5f9d 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -20,8 +20,7 @@ int APS5_VABI sceSystemServiceLoadExec(const char* path, const char* const* argu } int APS5_VABI sceSystemServiceDisableNoticeScreenSkipFlagAutoSet(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceGetDisplaySafeAreaInfo(SystemServiceDisplaySafeAreaInfo* info) { @@ -103,6 +102,15 @@ int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info) { } int APS5_VABI sceSystemServiceSetNoticeScreenSkipFlag(void) { + return SYSTEM_SERVICE_OK; +} + +int APS5_VABI sceSystemServiceInitializePlayerDialogParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceLaunchPlayerDialog(void) { NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 26d2bdb67..fcc4fa910 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -59,6 +59,16 @@ public: return std::make_shared(cfg, index, cfg->bufferReuse[index].Capture()); } + void WaitForFlipRoom() override { + std::unique_lock queueLock(queue->mutex); + const bool room = queue->changed.wait_for(queueLock, std::chrono::seconds(60), [&] { + return queue->failure || queue->stopping || cfg->shutdownToken.stop_requested() || queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY; + }); + if (queue->failure) std::rethrow_exception(queue->failure); + if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; + require(room, "flip queue stayed full for 60 s: the presenter is not completing flips"); + } + std::shared_ptr Reserve(const AgcDriver::FlipInfo& info) override { // All flip modes are presented at the next vsync. require(info.mode >= VIDEO_OUT_FLIP_MODE_VSYNC && info.mode <= 6, "unsupported flip mode"); @@ -70,23 +80,11 @@ public: request->outputHandle = info.handle; request->flipMode = static_cast(info.mode); request->flipArg = info.argument; - // Debug aid: APS5_FLIP_QUEUE_WAIT=1 waits for room instead of failing the flip, so a - // presenter stall keeps the process alive for a debugger (reported every 5 s). - static const bool waitWhenFull = std::getenv("APS5_FLIP_QUEUE_WAIT") != nullptr; - if (waitWhenFull) { - int waited = 0; - while (queue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { - if (cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - if (++waited % 500 == 0) std::fprintf(stderr, "[flip] queue full for %d s; waiting (APS5_FLIP_QUEUE_WAIT)\n", waited / 100); - } - } std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); - require(queue->reservations.load() < VIDEO_OUT_FLIP_QUEUE_CAPACITY, "flip queue full"); if (info.index >= 0) { request->buffer = cfg->buffers[info.index]; require(request->buffer.Occupied(), "flip buffer is not registered"); @@ -479,6 +477,7 @@ void VideoOutDriver::presentLoop(std::stop_token token) { flipQueue->requests.pop_front(); } } + SDL_Event event; while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { @@ -504,7 +503,10 @@ void VideoOutDriver::presentLoop(std::stop_token token) { } current->timing->Print(current->outputHandle, current->index, current->flipArg, finished, interval); } - current.reset(); + if (current) { + current.reset(); + flipQueue->changed.notify_all(); + } } } catch (const ProcessShutdown&) { } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp new file mode 100644 index 000000000..9400810ba --- /dev/null +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -0,0 +1,78 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +std::mutex g_infoMutex; +std::map> g_info; +} + +extern "C" { + +int APS5_VABI sceVideoRecordingClose(int discard) { + (void)discard; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingGetInfo(int32_t info, void* data, size_t size) { + if (data == nullptr) APS5_INVALID_ARG_EX; + std::lock_guard lock(g_infoMutex); + const auto found = g_info.find(info); + if (found == g_info.end()) throw std::logic_error(std::string(__func__) + ": info " + std::to_string(info) + " was never set"); + if (size < found->second.size()) APS5_INVALID_ARG_EX; + std::memcpy(data, found->second.data(), found->second.size()); + return 0; +} + +int APS5_VABI sceVideoRecordingGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingOpen(void* buffer, size_t size, const void* param) { + (void)buffer; + (void)size; + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingQueryMemSize(const void* param) { + (void)param; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingSetInfo(int32_t info, const void* data, size_t size) { + if (data == nullptr && size != 0) APS5_INVALID_ARG_EX; + const auto* bytes = static_cast(data); + std::lock_guard lock(g_infoMutex); + g_info[info].assign(bytes, bytes + size); + return 0; +} + +int APS5_VABI sceVideoRecordingStart(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoRecordingStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +APS5_EXPORT("iQS6DUtLybE", videoRecordingUnknown_iQS6DUtLybE); +int APS5_VABI videoRecordingUnknown_iQS6DUtLybE(void) { + NotImplemented_nid_no_patch("iQS6DUtLybE"); + return 0; +} + +} diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 91ae9bd80..735f0697e 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -1,118 +1,207 @@ #include #include +#include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::int32_t PortInDevice = 0; +constexpr std::int32_t PortInPcm = 1; +constexpr std::int32_t PortInVoice = 2; +constexpr std::int32_t PortOutPcm = 3; +constexpr std::int32_t PortOutVoice = 4; +constexpr std::int32_t PortOutDevice = 5; +constexpr std::int32_t PortStateReady = 1; +constexpr std::uint32_t VoiceFrameMs = 20; + +struct Port { + std::int32_t type; + float volume; + std::uint32_t bitrate; +}; + +struct Voice { + std::mutex mutex; + bool initialized = false; + bool started = false; + std::uint32_t nextId = 0; + std::map ports; + std::set> connections; +}; + +Voice& State() { + static Voice voice; + return voice; +} + +[[noreturn]] void Fail(const char* function, const std::string& why) { + throw std::logic_error(std::string(function) + ": " + why); +} + +Port& RequirePort(Voice& voice, std::uint32_t id, const char* function) { + if (!voice.initialized) Fail(function, "library not initialized"); + const auto found = voice.ports.find(id); + if (found == voice.ports.end()) Fail(function, "unknown port " + std::to_string(id)); + return found->second; +} + +bool IsInput(std::int32_t type) { return type == PortInDevice || type == PortInPcm || type == PortInVoice; } + +} + extern "C" { -int APS5_VABI sceVoiceConnectIPortToOPort(uint32_t input_port_id, uint32_t output_port_id) { - (void)input_port_id; - (void)output_port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { - (void)port_id; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { - (void)port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceDisconnectIPortFromOPort(uint32_t input_port_id, uint32_t output_port_id) { - (void)input_port_id; - (void)output_port_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceEnd_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetBitRate(uint32_t port_id, uint32_t* bitrate) { - (void)port_id; - (void)bitrate; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { - (void)port_id; - (void)attr; - (void)value; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { - (void)port_id; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { - (void)port_id; - (void)volume; - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceVoiceInit(VoiceInitParam* param, int32_t version) { - (void)param; - (void)version; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_t* size) { - (void)output_port_id; - (void)data; - (void)size; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)version; + if (param == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (voice.initialized) Fail(__func__, "already initialized"); + voice.initialized = true; + return 0; } int APS5_VABI sceVoiceSetThreadsParams(void* params) { - (void)params; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceVoiceSetVolume(uint32_t port_id, float volume) { - (void)port_id; - (void)volume; - NotImplemented_nid_no_patch(__func__); - return 0; + if (params == nullptr) APS5_INVALID_ARG_EX; + return 0; } int APS5_VABI sceVoiceStart(const VoiceStartParam* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + if (param == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.started = true; + return 0; } int APS5_VABI sceVoiceStop(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.started = false; + return 0; +} + +int APS5_VABI sceVoiceEnd_nid_postfix(void) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + voice.ports.clear(); + voice.connections.clear(); + voice.started = false; + voice.initialized = false; + return 0; +} + +int APS5_VABI sceVoiceCreatePort(uint32_t* port_id, const VoicePortParam* param) { + if (port_id == nullptr || param == nullptr) APS5_INVALID_ARG_EX; + if (param->port_type < PortInDevice || param->port_type > PortOutDevice) Fail(__func__, "unknown port type " + std::to_string(param->port_type)); + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!voice.initialized) Fail(__func__, "library not initialized"); + const bool voicePort = param->port_type == PortInVoice || param->port_type == PortOutVoice; + const auto id = voice.nextId++; + voice.ports.emplace(id, Port{param->port_type, param->volume, voicePort ? static_cast(param->voice.bitrate) : 0u}); + *port_id = id; + return 0; +} + +int APS5_VABI sceVoiceDeletePort(uint32_t port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + std::erase_if(voice.connections, [port_id](const auto& link) { return link.first == port_id || link.second == port_id; }); + voice.ports.erase(port_id); + return 0; +} + +int APS5_VABI sceVoiceConnectIPortToOPort(uint32_t input_port_id, uint32_t output_port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (!IsInput(RequirePort(voice, input_port_id, __func__).type)) Fail(__func__, "first port is not an input port"); + if (IsInput(RequirePort(voice, output_port_id, __func__).type)) Fail(__func__, "second port is not an output port"); + voice.connections.emplace(input_port_id, output_port_id); + return 0; +} + +int APS5_VABI sceVoiceDisconnectIPortFromOPort(uint32_t input_port_id, uint32_t output_port_id) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, input_port_id, __func__); + RequirePort(voice, output_port_id, __func__); + if (voice.connections.erase({input_port_id, output_port_id}) == 0) Fail(__func__, "ports are not connected"); + return 0; +} + +int APS5_VABI sceVoiceGetBitRate(uint32_t port_id, uint32_t* bitrate) { + if (bitrate == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + const auto& port = RequirePort(voice, port_id, __func__); + if (port.type != PortInVoice && port.type != PortOutVoice) Fail(__func__, "port does not carry encoded voice"); + *bitrate = port.bitrate; + return 0; +} + +int APS5_VABI sceVoiceSetVolume(uint32_t port_id, float volume) { + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__).volume = volume; + return 0; +} + +int APS5_VABI sceVoiceGetVolume(uint32_t port_id, float* volume) { + if (volume == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + *volume = RequirePort(voice, port_id, __func__).volume; + return 0; +} + +int APS5_VABI sceVoiceGetPortInfo(uint32_t port_id, VoicePortInfo* info) { + if (info == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + const auto& port = RequirePort(voice, port_id, __func__); + *info = VoicePortInfo{}; + info->port_type = port.type; + info->state = PortStateReady; + if (port.bitrate != 0) info->frame_size = (port.bitrate * VoiceFrameMs + 7999u) / 8000u; + return 0; +} + +int APS5_VABI sceVoiceGetPortAttr(uint32_t port_id, int32_t attr, void* value, int32_t size) { + (void)value; + (void)size; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + RequirePort(voice, port_id, __func__); + Fail(__func__, "attribute " + std::to_string(attr) + " not implemented"); +} + +int APS5_VABI sceVoiceReadFromOPort(uint32_t output_port_id, void* data, uint32_t* size) { + if (data == nullptr || size == nullptr) APS5_INVALID_ARG_EX; + auto& voice = State(); + std::lock_guard lock(voice.mutex); + if (IsInput(RequirePort(voice, output_port_id, __func__).type)) Fail(__func__, "port is not an output port"); + *size = 0; + return 0; } int APS5_VABI sceVoiceWriteToIPort(uint32_t input_port_id, const void* data, uint32_t* size, int16_t frame_gaps) { - (void)input_port_id; - (void)data; - (void)size; - (void)frame_gaps; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)input_port_id; + (void)data; + (void)size; + (void)frame_gaps; + NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 4ef69b42b..416407839 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -1,18 +1,45 @@ +#include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { +std::atomic g_status{0}; +} + extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + int expected = 0; + if (!g_status.compare_exchange_strong(expected, 1)) throw std::logic_error("sceWebBrowserDialogInitialize: already initialized"); + return 0; } int APS5_VABI sceWebBrowserDialogTerminate(void) { + int expected = 1; + if (!g_status.compare_exchange_strong(expected, 0)) throw std::logic_error("sceWebBrowserDialogTerminate: not initialized or still running"); + return 0; +} + +int APS5_VABI sceWebBrowserDialogClose(void) { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceWebBrowserDialogGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogUpdateStatus(void) { + return g_status.load(); +} + } diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 0311cbd7e..894dd0012 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -5,7 +5,6 @@ #include // Guest virtual memory is placed inside one reserved arena below the PS5 application map limit -// (0xFC_0000_0000), in ascending first-fit order like the PS5 kernel. Guest code indexes tables by // absolute address and breaks on host addresses outside that range. namespace GuestArena { @@ -19,6 +18,15 @@ void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes); // The reserved range, and whether it was reserved with page write watching (Windows MEM_WRITE_WATCH). void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes); bool GuestArenaWriteWatched_nid_postfix(); +#ifdef _WIN32 +void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection); +bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address); +bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection); +bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear); +void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule); +void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes); +void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); +#endif } diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 2b2775270..19d02b612 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -203,11 +203,11 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo output.Value(spec + 's', utf8.c_str()); } else if (conversion == 's' && (length.empty() || length == "h")) { const char* value = args.Next(); - if (!value) throw std::invalid_argument("Null formatted string"); + if (!value) value = "(null)"; output.Value(spec + 's', value); } else if ((conversion == 's' && length == "l") || (conversion == 'S' && length.empty())) { const char16_t* value = args.Next(); - if (!value) throw std::invalid_argument("Null formatted wide string"); + if (!value) value = u"(null)"; std::string utf8; while (*value) { char32_t code = *value++; diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp new file mode 100644 index 000000000..9f008ffad --- /dev/null +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -0,0 +1,308 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSMAPPINGS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSMAPPINGS_HPP + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GuestArena { + +class WindowsMappings { +public: + static WindowsMappings& Get() { + static WindowsMappings mappings; + return mappings; + } + + void* Reserve(void* address, std::size_t bytes) { + return allocate(GetCurrentProcess(), address, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); + } + + void Commit(void* address, std::size_t bytes, DWORD protection, std::size_t granule, bool watched) { + std::lock_guard lock(mutex); + const auto end = reinterpret_cast(address) + bytes; + for (auto cursor = reinterpret_cast(address); cursor < end;) { + const auto memory = query(cursor); + const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + if (memory.State == MEM_RESERVE) { + const auto limit = std::min(end, cursor + granule); + auto placeholderEnd = stop; + while (placeholderEnd < limit) { + const auto next = query(placeholderEnd); + if (next.State != MEM_RESERVE) break; + placeholderEnd = reinterpret_cast(next.BaseAddress) + next.RegionSize; + } + const auto size = std::min(limit, placeholderEnd) - cursor; + reset(cursor, size); + const DWORD flags = MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER | (watched ? MEM_WRITE_WATCH : 0); + if (!allocate(GetCurrentProcess(), reinterpret_cast(cursor), size, flags, protection, nullptr, 0)) fail("replace guest placeholder with private memory"); + cursor += size; + } else { + if (memory.State != MEM_COMMIT) throw std::runtime_error("guest memory is not committed"); + const auto mapped = views.find(cursor & ~(pageBytes - 1)); + if (mapped != views.end()) { + mapped->second.protection = protection; + mapped->second.armed = false; + invalidate(*mapped->second.page); + } + DWORD previous; + if (!VirtualProtect(reinterpret_cast(cursor), stop - cursor, protection, &previous)) fail("protect guest memory"); + cursor = stop; + } + } + } + + void Reset(void* address, std::size_t bytes) { + std::lock_guard lock(mutex); + reset(reinterpret_cast(address), bytes); + } + + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection) { + std::lock_guard lock(mutex); + auto cursor = reinterpret_cast(address); + reset(cursor, bytes); + for (std::size_t done = 0; done < bytes; done += pageBytes) { + split(cursor + done, pageBytes); + void* page = reinterpret_cast(cursor + done); + if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); + DWORD previous; + if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); + const auto key = std::make_pair(reinterpret_cast(section), offset + done); + auto shared = physical[key].lock(); + if (!shared) { + shared = std::make_shared(); + physical[key] = shared; + } + const auto base = cursor + done; + shared->aliases.push_back(base); + views.emplace(base, View{shared, protection, 0, false}); + invalidate(*shared); + } + } + + void SetProtection(std::uintptr_t address, std::size_t bytes, DWORD protection) { + std::lock_guard lock(mutex); + for (auto it = views.lower_bound(address); it != views.end() && it->first < address + bytes; ++it) { + it->second.protection = protection; + it->second.armed = false; + invalidate(*it->second.page); + } + } + + bool HandleWrite(std::uintptr_t address) { + std::lock_guard lock(mutex); + const auto base = address & ~(pageBytes - 1); + const auto found = views.find(base); + if (found == views.end() || !writable(found->second.protection)) return false; + auto& view = found->second; + invalidate(*view.page); + DWORD previous; + if (!VirtualProtect(reinterpret_cast(base), pageBytes, view.protection, &previous)) fail("resume shared memory write"); + view.armed = false; + return true; + } + + bool Protection(std::uintptr_t address, std::uint32_t* protection) { + std::lock_guard lock(mutex); + const auto found = views.find(address & ~(pageBytes - 1)); + if (found == views.end()) return false; + *protection = found->second.protection; + return true; + } + + bool Collect(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + std::lock_guard lock(mutex); + const auto capacity = *count; + *count = 0; + const auto end = address + bytes; + for (auto cursor = address; cursor < end;) { + const auto nextClean = cleanRanges.upper_bound(cursor); + if (nextClean != cleanRanges.begin()) { + const auto clean = std::prev(nextClean); + if (cursor < clean->second) { + cursor = std::min(end, clean->second); + continue; + } + } + const auto base = cursor & ~(pageBytes - 1); + const auto found = views.find(base); + if (found != views.end()) { + auto& view = found->second; + const auto stop = std::min(end, base + pageBytes); + if (view.protection == PAGE_NOACCESS) return false; + if (view.seen != view.page->generation) { + const auto needed = (stop - cursor + 4095) / 4096; + if (needed > capacity - *count) { + for (auto at = cursor; *count < capacity; at += 4096) pages[(*count)++] = reinterpret_cast(at); + return true; + } + for (auto at = cursor; at < stop; at += 4096) pages[(*count)++] = reinterpret_cast(at); + } + if (clear) { + for (const auto alias : view.page->aliases) { + auto& other = views.at(alias); + if (!writable(other.protection) || other.armed) continue; + DWORD previous; + const DWORD protection = other.protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; + if (!VirtualProtect(reinterpret_cast(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking"); + other.armed = true; + } + view.seen = view.page->generation; + rememberClean(base, base + pageBytes); + } + cursor = stop; + } else { + const auto memory = query(cursor); + if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; + const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + ULONG_PTR available = capacity - *count; + if (available == 0) return true; + DWORD granularity = 0; + if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(cursor), stop - cursor, pages + *count, &available, &granularity) != 0) fail("collect private guest writes"); + *count += available; + if (*count == capacity) return true; + cursor = stop; + } + } + return true; + } + +private: + static constexpr std::size_t pageBytes = 0x4000; + struct SharedPage { + std::uint64_t generation = 1; + std::vector aliases; + }; + struct View { + std::shared_ptr page; + DWORD protection; + std::uint64_t seen; + bool armed; + }; + void forgetClean(std::uintptr_t start, std::uintptr_t end) { + auto it = cleanRanges.lower_bound(start); + if (it != cleanRanges.begin() && std::prev(it)->second > start) --it; + while (it != cleanRanges.end() && it->first < end) { + const auto first = it->first; + const auto last = it->second; + it = cleanRanges.erase(it); + if (first < start) cleanRanges.emplace(first, start); + if (last > end) it = cleanRanges.emplace(end, last).first; + } + } + + void rememberClean(std::uintptr_t start, std::uintptr_t end) { + auto it = cleanRanges.lower_bound(start); + if (it != cleanRanges.begin() && std::prev(it)->second >= start) --it; + while (it != cleanRanges.end() && it->first <= end) { + start = std::min(start, it->first); + end = std::max(end, it->second); + it = cleanRanges.erase(it); + } + cleanRanges.emplace(start, end); + } + + void invalidate(SharedPage& page) { + ++page.generation; + for (const auto alias : page.aliases) forgetClean(alias, alias + pageBytes); + } + + static bool writable(DWORD protection) { + return protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE; + } + using AllocateFunction = PVOID (WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG); + using MapFunction = PVOID (WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG); + using UnmapFunction = BOOL (WINAPI*)(HANDLE, PVOID, ULONG); + + WindowsMappings() { + const auto module = GetModuleHandleW(L"KernelBase.dll"); + if (!module) fail("load Windows memory API"); + allocate = reinterpret_cast(GetProcAddress(module, "VirtualAlloc2")); + map = reinterpret_cast(GetProcAddress(module, "MapViewOfFile3")); + unmap = reinterpret_cast(GetProcAddress(module, "UnmapViewOfFile2")); + if (!allocate || !map || !unmap) throw std::runtime_error("Windows placeholder memory APIs are required"); + } + + [[noreturn]] static void fail(const char* operation) { + throw std::system_error(static_cast(GetLastError()), std::system_category(), operation); + } + + static MEMORY_BASIC_INFORMATION query(std::uintptr_t address) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory)) fail("query guest memory"); + return memory; + } + + static void split(std::uintptr_t address, std::size_t bytes) { + auto memory = query(address); + memory = query(reinterpret_cast(memory.AllocationBase)); + if (memory.State != MEM_RESERVE) throw std::runtime_error("guest mapping requires a placeholder"); + const auto base = reinterpret_cast(memory.BaseAddress); + if (address != base) { + if (!VirtualFree(reinterpret_cast(base), address - base, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder prefix"); + memory = query(address); + } + if (memory.RegionSize < bytes) throw std::runtime_error("guest placeholder is too small"); + if (memory.RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); + } + + void reset(std::uintptr_t address, std::size_t bytes) { + const auto end = address + bytes; + forgetClean(address, end); + for (auto cursor = address; cursor < end;) { + const auto memory = query(cursor); + if (memory.State == MEM_RESERVE) { + cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); + continue; + } + if (reinterpret_cast(memory.AllocationBase) != cursor) throw std::runtime_error("cannot release part of a host allocation"); + auto allocationEnd = cursor; + do { + const auto part = query(allocationEnd); + if (part.AllocationBase != memory.AllocationBase) break; + allocationEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + } while (allocationEnd < end); + if (allocationEnd > end || query(allocationEnd).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); + if (memory.Type == MEM_MAPPED) { + if (!unmap(GetCurrentProcess(), reinterpret_cast(cursor), MEM_PRESERVE_PLACEHOLDER)) fail("unmap shared guest page"); + const auto found = views.find(cursor); + if (found != views.end()) { + std::erase(found->second.page->aliases, cursor); + views.erase(found); + } + } else if (memory.Type == MEM_PRIVATE) { + if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); + } else { + throw std::runtime_error("unsupported guest mapping type"); + } + cursor = allocationEnd; + } + const auto last = query(reinterpret_cast(query(end - 1).AllocationBase)); + const auto lastBase = reinterpret_cast(last.BaseAddress); + if (lastBase + last.RegionSize > end) split(lastBase, end - lastBase); + const auto first = query(address); + split(address, std::min(bytes, reinterpret_cast(first.BaseAddress) + first.RegionSize - address)); + if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders"); + } + + std::map cleanRanges; + std::map views; + std::map, std::weak_ptr> physical; + std::mutex mutex; + AllocateFunction allocate = nullptr; + MapFunction map = nullptr; + UnmapFunction unmap = nullptr; +}; + +} +#endif + +#endif diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 30395fd3e..744b07805 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -16,6 +16,113 @@ #include "prx/libc/include/WindowsFormatting.hpp" #endif +namespace { + +std::string HostLength(const std::string& length, char conversion) { +#ifdef _WIN32 + if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll"; +#else + (void)conversion; +#endif + return length; +} + +template +int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer nextPointer, NextCapacity nextCapacity) { + if (buffer == nullptr || format == nullptr) return EOF; + const char* input = buffer; + int assigned = 0; + bool converted = false; + const char* cursor = format; + const auto finish = [](int result) { return result; }; + while (*cursor != '\0') { + if (std::isspace(static_cast(*cursor))) { + while (std::isspace(static_cast(*cursor))) ++cursor; + while (std::isspace(static_cast(*input))) ++input; + continue; + } + if (*cursor != '%' || cursor[1] == '%') { + const char expected = *cursor == '%' ? '%' : *cursor; + cursor += *cursor == '%' ? 2 : 1; + if (expected == '%') while (std::isspace(static_cast(*input))) ++input; + if (*input != expected) return finish(!converted && *input == '\0' ? EOF : assigned); + ++input; + continue; + } + ++cursor; + const bool suppress = *cursor == '*'; + if (suppress) ++cursor; + unsigned long width = 0; + while (std::isdigit(static_cast(*cursor))) width = width * 10 + static_cast(*cursor++ - '0'); + const char* lengthStart = cursor; + while (std::strchr("hljztL", *cursor) != nullptr && *cursor != '\0') ++cursor; + const std::string length(lengthStart, cursor); + const char conversion = *cursor; + if (conversion == '\0') return finish(assigned); + std::string specifier(1, conversion); + if (conversion == '[') { + const char* setStart = cursor++; + if (*cursor == '^') ++cursor; + if (*cursor == ']') ++cursor; + while (*cursor != '\0' && *cursor != ']') ++cursor; + if (*cursor != ']') return finish(assigned); + specifier.assign(setStart, cursor + 1); + } + ++cursor; + if (conversion == 'n') { + if (!suppress) *static_cast(nextPointer()) = static_cast(input - buffer); + continue; + } + if (conversion != 'c' && conversion != '[') { + const char* probe = input; + while (std::isspace(static_cast(*probe))) ++probe; + if (*probe == '\0') return finish(converted ? assigned : EOF); + } + const bool sized = secure && (conversion == 's' || conversion == 'c' || conversion == '['); + std::string directive = "%"; + void* target = nullptr; + if (suppress) directive += '*'; + else target = nextPointer(); + if (sized && !suppress) { + const auto capacity = static_cast(nextCapacity()); + const unsigned long limit = conversion == 'c' ? capacity : (capacity == 0 ? 0 : capacity - 1); + const unsigned long wanted = conversion == 'c' ? (width == 0 ? 1 : width) : width; + if (limit == 0 || (conversion == 'c' && wanted > limit)) { + if (capacity != 0) static_cast(target)[0] = '\0'; + return finish(assigned); + } + width = wanted == 0 ? limit : std::min(wanted, limit); + } + if (width != 0) directive += std::to_string(width); + directive += HostLength(length, conversion); + directive += specifier; + directive += "%n"; + int consumed = -1; + const int matched = suppress ? std::sscanf(input, directive.c_str(), &consumed) : std::sscanf(input, directive.c_str(), target, &consumed); + if (consumed < 0 || (!suppress && matched != 1)) return finish(!converted && *input == '\0' ? EOF : assigned); + if (sized && !suppress && conversion != 'c') { + const char next = input[consumed]; + bool overflow = false; + if (conversion == 's') overflow = next != '\0' && !std::isspace(static_cast(next)); + else if (next != '\0') { + char probe[2]; + const std::string test = "%1" + specifier; + overflow = std::sscanf(input + consumed, test.c_str(), probe) == 1; + } + if (overflow) { + static_cast(target)[0] = '\0'; + return finish(assigned); + } + } + input += consumed; + converted = true; + if (!suppress) ++assigned; + } + return finish(assigned); +} + +} + extern "C" { int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaList* args) { @@ -135,6 +242,8 @@ int APS5_VABI sprintf_nid_postfix(VA_ARGS) { #endif +#ifndef _WIN32 + int APS5_VABI sscanf_nid_postfix(VA_ARGS) { LibcDetail::RegSaveArea regs; LibcDetail::FillRegSaveArea(regs, rdx, rcx, r8, r9, 0, 0, @@ -149,113 +258,34 @@ int APS5_VABI sscanf_nid_postfix(VA_ARGS) { ); } +#endif + #ifdef _WIN32 +int APS5_VABI sscanf_nid_postfix(const char* buffer, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + const int result = ScanGuest(buffer, format, false, [&] { return __builtin_va_arg(args, void*); }, [&] { return __builtin_va_arg(args, unsigned int); }); + __builtin_sysv_va_end(args); + return result; +} + int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) { - (void)buffer; - (void)format; - NotImplemented_nid_no_patch(__func__); - return 0; + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + const int result = ScanGuest(buffer, format, true, [&] { return __builtin_va_arg(args, void*); }, [&] { return __builtin_va_arg(args, unsigned int); }); + __builtin_sysv_va_end(args); + return result; } #else int APS5_VABI sscanf_s_nid_postfix(const char* buffer, const char* format, ...) { - if (buffer == nullptr || format == nullptr) return EOF; std::va_list args; va_start(args, format); - const char* input = buffer; - int assigned = 0; - bool converted = false; - const char* cursor = format; - const auto finish = [&](int result) { - va_end(args); - return result; - }; - while (*cursor != '\0') { - if (std::isspace(static_cast(*cursor))) { - while (std::isspace(static_cast(*cursor))) ++cursor; - while (std::isspace(static_cast(*input))) ++input; - continue; - } - if (*cursor != '%' || cursor[1] == '%') { - const char expected = *cursor == '%' ? '%' : *cursor; - cursor += *cursor == '%' ? 2 : 1; - if (expected == '%') while (std::isspace(static_cast(*input))) ++input; - if (*input != expected) return finish(!converted && *input == '\0' ? EOF : assigned); - ++input; - continue; - } - ++cursor; - const bool suppress = *cursor == '*'; - if (suppress) ++cursor; - unsigned long width = 0; - while (std::isdigit(static_cast(*cursor))) width = width * 10 + static_cast(*cursor++ - '0'); - const char* lengthStart = cursor; - while (std::strchr("hljztL", *cursor) != nullptr && *cursor != '\0') ++cursor; - const std::string length(lengthStart, cursor); - const char conversion = *cursor; - if (conversion == '\0') return finish(assigned); - std::string specifier(1, conversion); - if (conversion == '[') { - const char* setStart = cursor++; - if (*cursor == '^') ++cursor; - if (*cursor == ']') ++cursor; - while (*cursor != '\0' && *cursor != ']') ++cursor; - if (*cursor != ']') return finish(assigned); - specifier.assign(setStart, cursor + 1); - } - ++cursor; - if (conversion == 'n') { - if (!suppress) *va_arg(args, int*) = static_cast(input - buffer); - continue; - } - if (conversion != 'c' && conversion != '[') { - const char* probe = input; - while (std::isspace(static_cast(*probe))) ++probe; - if (*probe == '\0') return finish(converted ? assigned : EOF); - } - const bool sized = conversion == 's' || conversion == 'c' || conversion == '['; - std::string directive = "%"; - void* target = nullptr; - if (suppress) directive += '*'; - else target = va_arg(args, void*); - if (sized && !suppress) { - const auto capacity = static_cast(va_arg(args, unsigned int)); - const unsigned long limit = conversion == 'c' ? capacity : (capacity == 0 ? 0 : capacity - 1); - const unsigned long wanted = conversion == 'c' ? (width == 0 ? 1 : width) : width; - if (limit == 0 || (conversion == 'c' && wanted > limit)) { - if (capacity != 0) static_cast(target)[0] = '\0'; - return finish(assigned); - } - width = wanted == 0 ? limit : std::min(wanted, limit); - } - if (width != 0) directive += std::to_string(width); - directive += length; - directive += specifier; - directive += "%n"; - int consumed = -1; - const int matched = suppress ? std::sscanf(input, directive.c_str(), &consumed) : std::sscanf(input, directive.c_str(), target, &consumed); - if (consumed < 0 || (!suppress && matched != 1)) return finish(!converted && *input == '\0' ? EOF : assigned); - if (sized && !suppress && conversion != 'c') { - const char next = input[consumed]; - bool overflow = false; - if (conversion == 's') overflow = next != '\0' && !std::isspace(static_cast(next)); - else if (next != '\0') { - char probe[2]; - const std::string test = "%1" + specifier; - overflow = std::sscanf(input + consumed, test.c_str(), probe) == 1; - } - if (overflow) { - static_cast(target)[0] = '\0'; - return finish(assigned); - } - } - input += consumed; - converted = true; - if (!suppress) ++assigned; - } - return finish(assigned); + const int result = ScanGuest(buffer, format, true, [&] { return va_arg(args, void*); }, [&] { return va_arg(args, unsigned int); }); + va_end(args); + return result; } #endif diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index b0f022aaf..8686e99a5 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include #include @@ -35,6 +37,14 @@ std::atomic generation{1}; std::atomic invalidator{nullptr}; std::atomic pinWaiter{nullptr}; +std::chrono::milliseconds pinWait() { + static const std::chrono::milliseconds value{[] { + const char* text = std::getenv("APS5_PIN_WAIT_MS"); + return text != nullptr ? std::strtoull(text, nullptr, 10) : 60000ull; + }()}; + return value; +} + void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(reason); } @@ -181,6 +191,12 @@ void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t ranges.emplace(address, std::make_shared(Range{address, bytes, readable, writable, address, bytes})); } +[[noreturn]] void PinnedFailure(std::uintptr_t address, std::size_t bytes, const char* why) { + char message[160]; + std::snprintf(message, sizeof(message), "guest allocation 0x%llx+0x%llx is owned by an active GPU command (%s)", static_cast(address), static_cast(bytes), why); + throw std::runtime_error(message); +} + void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes) { const auto address = reinterpret_cast(pointer); require(bytes <= std::numeric_limits::max() - address, "guest allocation range overflow"); @@ -192,12 +208,8 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi // this registry, so waiting with the lock held would deadlock them. The scan restarts after the // lock is retaken, since another mutation may have run meanwhile. Without a waiter the lease is // dropped by another thread (a synchronous draw or dispatch), so yielding suffices. A waiter - // round that finished nothing (the holder is not recorded GPU work) counts as a spin, so a lease - // nobody releases fails as fast as it did without a waiter; rounds that did finish work are - // bounded by the deadline only, as each one is a GPU wait. auto* state = static_cast(mutation); - const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60); - int idleRounds = 0; + const auto deadline = std::chrono::steady_clock::now() + pinWait(); int syncedRounds = 0; for (;;) { bool pinned = false; @@ -212,7 +224,7 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi } } if (!pinned) return; - require(std::chrono::steady_clock::now() < deadline, "guest allocation is owned by an active GPU command"); + if (std::chrono::steady_clock::now() >= deadline) PinnedFailure(address, bytes, ("waited " + std::to_string(pinWait().count()) + " ms for the lease (APS5_PIN_WAIT_MS)").c_str()); const auto waiter = pinWaiter.load(std::memory_order_acquire); bool progressed = false; if (waiter != nullptr && state != nullptr && state->lock.owns_lock()) { @@ -229,7 +241,6 @@ void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* poi if (++syncedRounds == 8) std::fprintf(stderr, "[gpu] leased allocation 0x%llx+0x%llx still pinned after %d recorder syncs\n", static_cast(address), static_cast(bytes), syncedRounds); continue; } - require(++idleRounds < 1000000, "guest allocation is owned by an active GPU command"); } } diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 4baedeeaf..68892f438 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/WindowsMappings.hpp" #include #include #include @@ -15,7 +16,9 @@ namespace GuestArena { namespace { -constexpr std::uintptr_t PreferredBase = 0x0000001000000000ull; +constexpr std::uintptr_t PreferredBase = 0x0000000200000000ull; +constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; +constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; constexpr std::size_t MaximumSize = 0x0000007000000000ull; constexpr std::size_t MinimumSize = 0x0000001000000000ull; constexpr std::uintptr_t MapAreaEnd = 0x000000FC00000000ull; @@ -89,16 +92,12 @@ private: for (std::uintptr_t base = PreferredBase; base + MinimumSize <= MapAreaEnd; base += MinimumSize) { for (std::size_t size = MaximumSize; size >= MinimumSize; size /= 2) { if (base + size > MapAreaEnd) continue; - // Write watching lets the GPU driver learn which pages the CPU wrote instead of comparing - // whole resources; the plain reservation is the fallback. Debug aid: APS5_NO_WRITE_WATCH=1 - // skips it, to measure what the write faults it re-arms cost the game's threads. - static const bool noWriteWatch = std::getenv("APS5_NO_WRITE_WATCH") != nullptr; - void* reserved = noWriteWatch ? nullptr : VirtualAlloc(reinterpret_cast(base), size, MEM_RESERVE | MEM_WRITE_WATCH, PAGE_NOACCESS); - _writeWatched = reserved != nullptr; - if (!reserved) reserved = VirtualAlloc(reinterpret_cast(base), size, MEM_RESERVE, PAGE_NOACCESS); + _writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr; + void* reserved = WindowsMappings::Get().Reserve(reinterpret_cast(base), size); if (!reserved) continue; _base = reinterpret_cast(reserved); _end = _base + size; + if (_base < SystemReservedEnd && SystemReservedStart < _end) _used.emplace(std::max(_base, SystemReservedStart), std::min(_end, SystemReservedEnd)); return; } } @@ -147,6 +146,39 @@ void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes) { *bytes = Arena::Get().Size(); } +#ifdef _WIN32 +void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) { + WindowsMappings::Get().SetProtection(address, bytes, protection); +} + +bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address) { + return WindowsMappings::Get().HandleWrite(address); +} + +bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection) { + return WindowsMappings::Get().Protection(address, protection); +} + +bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { + return WindowsMappings::Get().Collect(address, bytes, pages, count, clear); +} + +void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule) { + if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("commit outside the guest arena"); + WindowsMappings::Get().Commit(pointer, bytes, protection, granule, Arena::Get().WriteWatched()); +} + +void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { + if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("reset outside the guest arena"); + WindowsMappings::Get().Reset(pointer, bytes); +} + +void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { + if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("shared mapping outside the guest arena"); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection); +} +#endif + bool GuestArenaWriteWatched_nid_postfix() { return Arena::Get().WriteWatched(); } diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index f47016d02..9d5f0d017 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -107,7 +107,7 @@ private: static void commit(void* pointer, std::size_t bytes) { #ifdef _WIN32 - if (!VirtualAlloc(pointer, bytes, MEM_COMMIT, PAGE_READWRITE)) throw std::bad_alloc(); + GuestArena::GuestArenaCommit_nid_postfix(pointer, bytes, PAGE_READWRITE, bytes); #else (void)pointer; (void)bytes; @@ -165,7 +165,7 @@ private: static void release(void* raw, std::size_t bytes) { #ifdef _WIN32 - if (!VirtualFree(raw, bytes, MEM_DECOMMIT)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "guest heap decommit failed"); + GuestArena::GuestArenaReset_nid_postfix(raw, bytes); GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(raw), bytes); #endif GuestArena::GuestArenaRelease_nid_postfix(raw, bytes); diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 5359fcd33..a4f388e1e 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -1,5 +1,6 @@ #ifdef _WIN32 #include +#include "prx/libc/include/GuestArena.hpp" #include #include #include @@ -235,6 +236,8 @@ bool HandleSse4a(EXCEPTION_POINTERS* info) { LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { static std::atomic reported{false}; + const auto* fault = info->ExceptionRecord; + if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 1 && GuestArena::GuestArenaHandleWrite_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION; if (HandleWatch(info)) return EXCEPTION_CONTINUE_EXECUTION; if (HandleSse4a(info)) return EXCEPTION_CONTINUE_EXECUTION; const auto* record = info->ExceptionRecord; diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index a9eab7266..dc5540138 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp @@ -22,6 +22,7 @@ AppTitle GetAppTitle_nid_postfix(); AppTitleId GetAppTitleId_nid_postfix(); bool HasAppIcon_nid_postfix(); AppIconData GetAppIconData_nid_postfix(); +std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix(); } diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index dd60d00c5..a52b5da50 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -1,12 +1,14 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMJSONPARSER_HPP #define CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMJSONPARSER_HPP +#include #include #include struct ParsedParamJson { std::string title; std::string titleId; + std::uint64_t downloadDataSizeMiB = 0; }; ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath); diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index d727008d7..b99d5aa7f 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -17,6 +17,7 @@ bool g_iconAttempted = false; bool g_iconLoaded = false; char g_title[128] = {}; char g_titleId[12] = {}; +std::uint64_t g_downloadDataSizeMiB = 0; std::vector g_iconBytes; constexpr const char* AppMetadataParamJsonGuestPath = "/app0/sce_sys/param.json"; @@ -35,6 +36,7 @@ void ensureTitleLoaded() { const auto parsed = parseParamJson(resolvedPath); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); + g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; g_titleLoaded = true; } @@ -68,6 +70,11 @@ AppTitle GetAppTitle_nid_postfix() { return result; } +std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix() { + ensureTitleLoaded(); + return g_downloadDataSizeMiB; +} + AppTitleId GetAppTitleId_nid_postfix() { ensureTitleLoaded(); AppTitleId result{}; diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 30756344f..89b53c3f2 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -259,5 +259,9 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { ParsedParamJson result; result.title = title; result.titleId = titleId; + if (const JsonValue* downloadData = findObjectMember(root, "downloadDataSize")) { + if (downloadData->type != JsonType::Number || downloadData->numberValue < 0) throw std::runtime_error("param.json downloadDataSize is not a size"); + result.downloadDataSizeMiB = static_cast(downloadData->numberValue); + } return result; } diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8d25f8f30..8d53f8bdd 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -3,6 +3,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" +#include "prx/libkernel/File/include/NativeStat.hpp" #include #include #include @@ -186,6 +187,34 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizes(const char** paths, return 0; } +int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint64_t* sizes, uint32_t* error_index) { + if (!prefix || !paths || !ids) return _fail(GUEST_EINVAL); + for (uint32_t index = 0; index < count; ++index) { + const std::string path = paths[index] ? std::string(prefix) + paths[index] : std::string(); + if (path.empty() || !_resolve(path.c_str(), &ids[index], sizes ? &sizes[index] : nullptr)) { + if (error_index) *error_index = index; + return _fail(GUEST_ENOENT); + } + } + return 0; +} + +int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIds(const char* prefix, const char** paths, uint32_t count, uint32_t* ids, uint32_t* error_index) { + return sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes(prefix, paths, count, ids, nullptr, error_index); +} + +int APS5_VABI sceKernelAprGetFileSize(uint32_t id, uint64_t* size) { + if (!size) return _fail(GUEST_EINVAL); + *size = _file(id).size; + return 0; +} + +int APS5_VABI sceKernelAprGetFileStat(uint32_t id, FileStat* stat) { + if (!stat) return _fail(GUEST_EINVAL); + File::FillFileStat(_file(id).path, stat); + return 0; +} + int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* buffer, uint32_t priority) { (void)priority; if (!buffer || buffer->type != Apr::BufferType::Apr) return _fail(GUEST_EINVAL); @@ -193,4 +222,26 @@ int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* bu return 0; } +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* id) { + if (!id) return _fail(GUEST_EINVAL); + const int result = sceKernelAprSubmitCommandBuffer(buffer, priority); + if (result != 0) return result; + static std::atomic nextId{1}; + *id = nextId.fetch_add(1); + return 0; +} + +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* result, uint32_t* id) { + if (!result) return _fail(GUEST_EINVAL); + const int submitted = sceKernelAprSubmitCommandBufferAndGetId(buffer, priority, id); + if (submitted != 0) return submitted; + *result = 0; + return 0; +} + +int APS5_VABI sceKernelAprWaitCommandBuffer(uint32_t id) { + (void)id; + return 0; +} + } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index aff9e0a7b..dc6eaa05b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Open.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index c003b3564..a1440a0f5 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1,4 +1,4 @@ -#include "DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include @@ -9,12 +9,16 @@ #include #include #include +#include +#include #include #include #include +#include #if defined(__linux__) #include +#include #include #include #include @@ -53,8 +57,7 @@ struct KernelArena { }; static void CommitArenaRange(void* addr, size_t len, DWORD winProt) { - if (!VirtualAlloc(addr, len, MEM_COMMIT, winProt)) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc commit failed"); + GuestArena::GuestArenaCommit_nid_postfix(addr, len, winProt, PS5_PAGE_SIZE); } static void* mmap_aligned(size_t len, int prot, size_t alignment) { @@ -95,9 +98,9 @@ static void* mmap(void* addr, size_t len, int prot, int flags, int, int) { } static int munmap(void* addr, size_t len) { - if (!VirtualFree(addr, len, MEM_DECOMMIT)) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualFree decommit failed"); auto& arena = KernelArena::Get(); + if (arena.Contains(addr, len)) GuestArena::GuestArenaReset_nid_postfix(addr, len); + else if (!VirtualFree(addr, len, MEM_DECOMMIT)) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualFree decommit failed"); if (arena.Contains(addr, len)) arena.Release(addr, len); return 0; } @@ -109,6 +112,7 @@ static int munmap_release(void* addr) { } static int mprotect(void* addr, size_t len, int prot) { + GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(addr), len, WinProtFromPosix(prot)); if (prot != PROT_NONE && KernelArena::Get().Contains(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot)); auto cursor = reinterpret_cast(addr); const auto end = cursor + len; @@ -215,6 +219,111 @@ void Trace(const char* format, ...) { va_end(args); } +class PhysicalBacking { +public: + explicit PhysicalBacking(std::size_t bytes, int memoryType) : memoryType(memoryType) { +#ifdef _WIN32 + const auto size = static_cast(bytes); + section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, static_cast(size >> 32), static_cast(size), nullptr); + if (!section) throw std::system_error(static_cast(GetLastError()), std::system_category(), "create direct memory backing"); +#else + file = std::tmpfile(); + if (!file) throw std::system_error(errno, std::generic_category(), "create direct memory backing"); + if (ftruncate(fileno(file), static_cast(bytes)) != 0) { + const int error = errno; + std::fclose(file); + throw std::system_error(error, std::generic_category(), "size direct memory backing"); + } +#endif + } + + int MemoryType() const { return memoryType; } + + ~PhysicalBacking() { +#ifdef _WIN32 + CloseHandle(section); +#else + std::fclose(file); +#endif + } + + PhysicalBacking(const PhysicalBacking&) = delete; + PhysicalBacking& operator=(const PhysicalBacking&) = delete; + + void Map(std::uintptr_t address, std::size_t bytes, std::uint64_t offset, int protection) const { +#ifdef _WIN32 + GuestArena::GuestArenaMap_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); +#else + if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, fileno(file), static_cast(offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); +#endif + } + +private: + int memoryType; +#ifdef _WIN32 + HANDLE section = nullptr; +#else + std::FILE* file = nullptr; +#endif +}; + +struct PhysicalPage { + std::shared_ptr backing; + std::uint64_t offset; +}; + +std::mutex g_directLock; +std::map g_physPages; + +struct DirectMapping { + std::uintptr_t end; + std::uint64_t phys; + int memoryType; + std::shared_ptr backing; +}; + +std::map g_directMappings; + +void EraseMappings(std::uintptr_t start, std::uintptr_t end) { + auto it = g_directMappings.lower_bound(start); + if (it != g_directMappings.begin() && std::prev(it)->second.end > start) --it; + while (it != g_directMappings.end() && it->first < end) { + const auto base = it->first; + const auto mapping = it->second; + it = g_directMappings.erase(it); + if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing}); + if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + } +} + + +void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { + for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { + if (!g_physPages.contains(phys + offset)) { + char message[160]; + std::snprintf(message, sizeof(message), "direct memory range 0x%llx+0x%zx references unallocated physical page 0x%llx", static_cast(phys), len, static_cast(phys + offset)); + throw std::invalid_argument(message); + } + } +} + +void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int nativeProt) { + ValidatePhysicalRange(phys, len); + EraseMappings(address, address + len); + for (std::size_t offset = 0; offset < len;) { + const auto& page = g_physPages.at(phys + offset); + std::size_t bytes = PS5_PAGE_SIZE; + while (offset + bytes < len) { + const auto& next = g_physPages.at(phys + offset + bytes); + if (next.backing != page.backing || next.offset != page.offset + bytes) break; + bytes += PS5_PAGE_SIZE; + } + page.backing->Map(address + offset, bytes, page.offset, nativeProt); + g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing}); + offset += bytes; + } +} + bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, size_t len, int prot, int flags, int64_t physStart = -1) { constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; @@ -223,10 +332,19 @@ bool RemapFixedIntoRegistered(GuestAllocations::Mutation& mutation, void* addr, Trace("remap fixed %p+0x%zx prot=0x%x phys=0x%llx", addr, len, prot, static_cast(physStart)); const auto nativeProtection = LinuxProtFromSce(prot); mutation.Protect(addr, len, (prot & 3) != 0, (prot & 2) != 0, [&] { + const auto address = reinterpret_cast(addr); + std::lock_guard lock(g_directLock); + if (physStart >= 0) { + AddMapping(address, len, static_cast(physStart), nativeProtection); + } else { + EraseMappings(address, address + len); #ifdef _WIN32 - if (!VirtualAlloc(addr, len, MEM_COMMIT, WinProtFromPosix(nativeProtection))) throw std::system_error(static_cast(GetLastError()), std::system_category(), "VirtualAlloc commit failed"); + GuestArena::GuestArenaReset_nid_postfix(addr, len); + if (nativeProtection != PROT_NONE) CommitArenaRange(addr, len, WinProtFromPosix(nativeProtection)); +#else + if (::mmap(addr, len, nativeProtection, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "remap anonymous memory"); #endif - if (mprotect(addr, len, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); + } }); return true; } @@ -306,6 +424,32 @@ void* MapAligned(void* addr, size_t len, int prot, int flags, size_t alignment) return aligned; } +struct ProtectedRange { + std::uintptr_t end; + int prot; +}; +std::mutex g_protectionLock; +std::map g_protections; + +void EraseProtections(std::uintptr_t start, std::uintptr_t end) { + auto it = g_protections.lower_bound(start); + if (it != g_protections.begin() && std::prev(it)->second.end > start) --it; + while (it != g_protections.end() && it->first < end) { + const auto rangeStart = it->first; + const auto range = it->second; + it = g_protections.erase(it); + if (rangeStart < start) g_protections.emplace(rangeStart, ProtectedRange{start, range.prot}); + if (range.end > end) it = g_protections.emplace(end, ProtectedRange{range.end, range.prot}).first; + } +} + +void RecordProtection(const void* addr, size_t len, int prot) { + const auto start = reinterpret_cast(addr); + std::lock_guard lock(g_protectionLock); + EraseProtections(start, start + len); + if (prot >= 0) g_protections.emplace(start, ProtectedRange{start + len, prot}); +} + void ValidateOutput(void** addr) { if (!addr) { // return SCE_KERNEL_ERROR_EINVAL; @@ -322,16 +466,24 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return SCE_KERNEL_ERROR_EINVAL; } GuestAllocations::Mutation mutation; - if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) return 0; + if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags, physStart)) { + RecordProtection(*addr, len, prot); + return 0; + } if (*addr != nullptr && (flags & GuestMapFixedFlag) != 0) mutation.RequireAvailable(*addr, len); - void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, alignment); + std::lock_guard lock(g_directLock); + ValidatePhysicalRange(static_cast(physStart), len); + void* mapped = MapAligned(*addr, len, PROT_NONE, flags, alignment); try { + AddMapping(reinterpret_cast(mapped), len, static_cast(physStart), LinuxProtFromSce(prot)); mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0); } catch (...) { + EraseMappings(reinterpret_cast(mapped), reinterpret_cast(mapped) + len); Unmap(mapped, len); throw; } *addr = mapped; + RecordProtection(mapped, len, prot); Trace("map direct %p+0x%zx phys=0x%llx prot=0x%x flags=0x%x align=0x%zx", mapped, len, static_cast(physStart), prot, flags, alignment); return 0; } @@ -340,7 +492,10 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; - if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) return 0; + if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) { + RecordProtection(*addr, len, prot); + return 0; + } if (*addr != nullptr && (flags & GuestMapFixedFlag) != 0) mutation.RequireAvailable(*addr, len); void* mapped = MapAligned(*addr, len, LinuxProtFromSce(prot), flags, PS5_PAGE_SIZE); try { @@ -350,6 +505,7 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { throw; } *addr = mapped; + RecordProtection(mapped, len, prot); Trace("map anon %p+0x%zx prot=0x%x flags=0x%x", mapped, len, prot, flags); return 0; } @@ -379,6 +535,7 @@ int DoMprotect(const void* addr, size_t len, int prot) { mutation.Protect(pointer, bytes, (prot & 3) != 0, (prot & 2) != 0, [&] { if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); }); + RecordProtection(pointer, bytes, prot); return 0; } @@ -387,6 +544,8 @@ int DoMunmap(void* addr, size_t len) { if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; mutation.Unmap(addr, len, [&](const void* allocation, bool last) { + std::lock_guard lock(g_directLock); + EraseMappings(reinterpret_cast(addr), reinterpret_cast(addr) + len); #if defined(__linux__) Unmap(addr, len); #else @@ -395,6 +554,7 @@ int DoMunmap(void* addr, size_t len) { else munmap(addr, len); #endif }); + RecordProtection(addr, len, -1); return 0; } @@ -413,3 +573,47 @@ int DoReserveVirtual(void** addr, size_t len, size_t alignment) { Trace("reserve %p+0x%zx align=0x%zx", mapped, len, alignment); return 0; } + +void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType) { + std::lock_guard lock(g_directLock); + const auto first = static_cast(start); + for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { + if (g_physPages.contains(first + offset)) throw std::runtime_error("physical allocation overlaps live direct memory"); + } + const auto backing = std::make_shared(len, memoryType); + std::map pages; + for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) pages.emplace(first + offset, PhysicalPage{backing, offset}); + g_physPages.merge(pages); + Trace("allocate physical 0x%llx+0x%zx", static_cast(first), len); +} + +void ForgetDirectMemory(int64_t start, size_t len) { + const auto first = static_cast(start); + std::lock_guard lock(g_directLock); + ValidatePhysicalRange(first, len); + g_physPages.erase(g_physPages.lower_bound(first), g_physPages.lower_bound(first + len)); + Trace("release physical 0x%llx+0x%zx", static_cast(first), len); +} + +bool GuestProtection(uintptr_t addr, int* prot) { + std::lock_guard lock(g_protectionLock); + auto next = g_protections.upper_bound(addr); + if (next == g_protections.begin()) return false; + const auto containing = std::prev(next); + if (addr >= containing->second.end) return false; + *prot = containing->second.prot; + return true; +} + +bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType) { + std::lock_guard lock(g_directLock); + const auto next = g_directMappings.upper_bound(address); + if (next == g_directMappings.begin()) return false; + const auto it = std::prev(next); + if (address >= it->second.end) return false; + *start = it->first; + *end = it->second.end; + *offset = it->second.phys; + *memoryType = it->second.memoryType; + return true; +} diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 53289beff..3dfdcfee6 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -9,20 +9,18 @@ static constexpr size_t DIRECT_MEMORY_SIZE = 13824ULL * 1024 * 1024; static constexpr size_t PS5_PAGE_SIZE = 0x4000; -struct DirectMemoryBlock { - uint64_t start; - uint64_t end; - int memoryType; -}; - int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); -bool DirectMemoryQueryBlock(uint64_t offset, DirectMemoryBlock* block); +void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType); +bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType); +void ForgetDirectMemory(int64_t start, size_t len); +bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags); int DoMprotect(const void* addr, size_t len, int prot); int DoMunmap(void* addr, size_t len); int DoReserveVirtual(void** addr, size_t len, size_t alignment); +bool GuestProtection(uintptr_t addr, int* prot); #endif \ No newline at end of file diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index c7922f496..7b0c1bf8a 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -4,7 +4,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" -#include "DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #define NOMINMAX @@ -134,21 +134,12 @@ int APS5_VABI sceKernelAvailableDirectMemorySize(int64_t search_start, int64_t s } int APS5_VABI sceKernelDirectMemoryQuery(int64_t offset, int flags, void* info, size_t info_size) { - (void)flags; + constexpr int SCE_KERNEL_DMQ_FIND_NEXT = 1; if (!info || offset < 0) return SCE_KERNEL_ERROR_EINVAL; struct DirectMemoryQueryInfo { int64_t start; int64_t end; int memory_type; }; if (info_size < sizeof(DirectMemoryQueryInfo)) return SCE_KERNEL_ERROR_EINVAL; auto* q = static_cast(info); - DirectMemoryBlock block{}; - if (DirectMemoryQueryBlock(static_cast(offset), &block)) { - q->start = static_cast(block.start); - q->end = static_cast(block.end); - q->memory_type = block.memoryType; - } else { - q->start = offset & ~static_cast(PS5_PAGE_SIZE - 1); - q->end = q->start + PS5_PAGE_SIZE; - q->memory_type = -1; - } + if (!DirectMemoryFind(offset, (flags & SCE_KERNEL_DMQ_FIND_NEXT) != 0, &q->start, &q->end, &q->memory_type)) return SCE_KERNEL_ERROR_EACCES; return 0; } @@ -225,9 +216,23 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf info->start = best->allocationAddress; info->end = best->allocationAddress + best->allocationBytes; info->protection = (best->readable ? 1 : 0) | (best->writable ? 2 : 0) | (!best->releasable ? 4 : 0); - info->is_direct = best->releasable ? 1u : 0u; + int recorded = 0; + if (GuestProtection(std::max(address, info->start), &recorded)) info->protection = recorded; + std::uintptr_t directStart = 0; + std::uintptr_t directEnd = 0; + std::uint64_t physicalOffset = 0; + int memoryType = 0; + const bool direct = QueryDirectMapping(std::max(address, info->start), &directStart, &directEnd, &physicalOffset, &memoryType); + info->is_direct = direct ? 1u : 0u; + info->is_flexible = !direct && best->releasable ? 1u : 0u; + if (direct) { + info->start = std::max(info->start, directStart); + info->end = std::min(info->end, directEnd); + info->memory_type = memoryType; + } info->is_committed = 1; ApplyRangeName(std::max(address, info->start), info); + if (direct) info->offset = physicalOffset + info->start - directStart; return 0; } // Memory the registry does not know (the title's own heap blocks, stacks): the host's committed @@ -287,11 +292,12 @@ int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int } int APS5_VABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, int* prot) { - (void)addr; - (void)start; - (void)end; - (void)prot; - NotImplemented_nid_no_patch(__func__); + VirtualQueryInfo info{}; + const int result = sceKernelVirtualQuery(addr, 0, &info, sizeof(info)); + if (result != 0) return result; + if (start) *start = reinterpret_cast(info.start); + if (end) *end = reinterpret_cast(info.end); + if (prot) *prot = info.protection; return 0; } diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 5d4f7e01e..1175ada11 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,6 +1,5 @@ -#define _GLIBCXX_HAS_GTHREADS 0 -#include "MemoryPool.hpp" -#include "DirectMemory.hpp" +#include "prx/libkernel/DirectMemory/MemoryPool.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include #include @@ -20,8 +19,9 @@ struct PhysicalMemoryPool { uint64_t cur = (start + align - 1) & ~(align - 1); while (cur + len <= end && cur + len <= DIRECT_MEMORY_SIZE) { if (_isFree(cur, len)) { + CreateDirectMemoryBacking(static_cast(cur), len, memoryType); _mark(cur, len, true); - _ranges[cur] = {cur, cur + len, memoryType}; + _blocks[cur] = {cur + len, memoryType}; *physOut = static_cast(cur); return 0; } @@ -32,24 +32,28 @@ struct PhysicalMemoryPool { void Free(uint64_t start, size_t len) { std::lock_guard lock(_mutex); + ForgetDirectMemory(static_cast(start), len); _mark(start, len, false); - auto it = _ranges.upper_bound(start); - if (it == _ranges.begin()) return; - --it; - if (start + len > it->second.end) return; - const DirectMemoryBlock block = it->second; - _ranges.erase(it); - if (block.start < start) _ranges[block.start] = {block.start, start, block.memoryType}; - if (start + len < block.end) _ranges[start + len] = {start + len, block.end, block.memoryType}; + const uint64_t end = start + len; + auto it = _blocks.upper_bound(start); + if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; + while (it != _blocks.end() && it->first < end) { + const uint64_t blockStart = it->first; + const Block block = it->second; + it = _blocks.erase(it); + if (blockStart < start) _blocks[blockStart] = {start, block.type}; + if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + } } - bool Query(uint64_t offset, DirectMemoryBlock* block) { + bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { std::lock_guard lock(_mutex); - auto it = _ranges.upper_bound(offset); - if (it == _ranges.begin()) return false; - --it; - if (offset >= it->second.end) return false; - *block = it->second; + auto it = _blocks.upper_bound(offset); + if (it != _blocks.begin() && std::prev(it)->second.end > offset) --it; + else if (!findNext || it == _blocks.end()) return false; + *start = static_cast(it->first); + *end = static_cast(it->second.end); + *memoryType = it->second.type; return true; } @@ -81,9 +85,14 @@ private: _used[first + i] = used; } + struct Block { + uint64_t end; + int type; + }; + std::mutex _mutex; bool _used[NUM_PAGES] = {}; - std::map _ranges; + std::map _blocks; }; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { @@ -94,8 +103,8 @@ void DirectMemoryFree(int64_t start, size_t len) { PhysicalMemoryPool::Instance().Free(static_cast(start), len); } -bool DirectMemoryQueryBlock(uint64_t offset, DirectMemoryBlock* block) { - return PhysicalMemoryPool::Instance().Query(offset, block); +bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { + return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp index 6bf75c51b..b6e799e68 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp @@ -1,6 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP #define CORE_LIBS_PRX_LINKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP -#include "DirectMemory.hpp" +#include +#include + +int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); +void DirectMemoryFree(int64_t start, size_t len); +bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); #endif diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp new file mode 100644 index 000000000..00c3b5895 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -0,0 +1,16 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP + +#include +#include + +namespace File { + +int OpenDirectoryDescriptor(const std::filesystem::path& path); +std::optional DirectoryDescriptorPath(int fd); +void ForgetDirectoryDescriptor(int fd); +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp new file mode 100644 index 000000000..9bd7ac5ed --- /dev/null +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -0,0 +1,91 @@ +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" + +#ifdef _WIN32 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint8_t GuestDirectoryType = 4; +constexpr std::uint8_t GuestRegularType = 8; + +struct DirectoryState { + std::filesystem::path path; + std::vector> entries; + std::size_t cursor = 0; + bool loaded = false; +}; + +std::mutex g_mutex; +std::map g_directories; + +} + +namespace File { + +int OpenDirectoryDescriptor(const std::filesystem::path& path) { + const int fd = ::_open("NUL", _O_RDONLY | _O_BINARY); + if (fd < 0) return -1; + std::lock_guard lock(g_mutex); + g_directories[fd] = DirectoryState{path}; + return fd; +} + +std::optional DirectoryDescriptorPath(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + if (found == g_directories.end()) return std::nullopt; + return found->second.path; +} + +void ForgetDirectoryDescriptor(int fd) { + std::lock_guard lock(g_mutex); + g_directories.erase(fd); +} + +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + if (found == g_directories.end()) return -1; + auto& state = found->second; + if (!state.loaded) { + state.loaded = true; + state.entries.emplace_back(".", GuestDirectoryType); + state.entries.emplace_back("..", GuestDirectoryType); + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { + std::error_code typeError; + state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); + } + } + std::size_t used = 0; + while (state.cursor < state.entries.size()) { + const auto& [name, type] = state.entries[state.cursor]; + const std::size_t record = (8 + name.size() + 1 + 3) & ~std::size_t{3}; + if (used + record > static_cast(nbytes)) break; + char* out = buf + used; + std::memset(out, 0, record); + const auto fileNumber = static_cast(state.cursor + 1); + const auto recordLength = static_cast(record); + std::memcpy(out, &fileNumber, sizeof(fileNumber)); + std::memcpy(out + 4, &recordLength, sizeof(recordLength)); + out[6] = static_cast(type); + out[7] = static_cast(name.size()); + std::memcpy(out + 8, name.c_str(), name.size() + 1); + used += record; + ++state.cursor; + } + return static_cast(used); +} + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index f9f195b81..cc1c0af37 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include #include @@ -11,6 +12,7 @@ static int DoStat(const std::filesystem::path& p, NativeStat* st) { return _wstat64(p.wstring().c_str(), st); } static int DoFstat(int fd, NativeStat* st) { + if (const auto directory = File::DirectoryDescriptorPath(fd)) return DoStat(*directory, st); return _fstat64(fd, st); } #else diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index fd45c02fe..469c4ad12 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "SceTypes.hpp" @@ -98,6 +99,12 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { APS5_LOG_OUT("path=%s flags=0x%X nativeFlags=0x%X mode=0%o", path, flags, MapFlags(flags), mode); auto native = ResolvePath_nid_no_patch(path); int fd = NativeOpen(native, MapFlags(flags), mode); +#ifdef _WIN32 + if (fd < 0 && errno != ENOENT) { + std::error_code error; + if (std::filesystem::is_directory(native, error)) fd = File::OpenDirectoryDescriptor(native); + } +#endif if (fd < 0) { return SceErrorFromErrno(errno); } @@ -105,6 +112,9 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } int APS5_VABI sceKernelClose(int d) { +#ifdef _WIN32 + File::ForgetDirectoryDescriptor(d); +#endif if (NativeClose(d) != 0) { throw std::runtime_error(std::string(__func__) + ": close failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index ece3fae6d..4e35b2dd9 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -6,6 +6,8 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include #include @@ -45,35 +47,33 @@ static int NativeFlock(int descriptor, int operation) { if (operation & 4) flags |= LOCKFILE_FAIL_IMMEDIATELY; return ::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1; } +static std::int64_t NativePositioned(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) { + if (nbytes > static_cast(std::numeric_limits::max())) { + throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); + } + const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); + if (handle == INVALID_HANDLE_VALUE) { + errno = EBADF; + return -1; + } + OVERLAPPED overlapped{}; + overlapped.Offset = static_cast(offset); + overlapped.OffsetHigh = static_cast(static_cast(offset) >> 32u); + DWORD done = 0; + const BOOL ok = write ? ::WriteFile(handle, buf, static_cast(nbytes), &done, &overlapped) + : ::ReadFile(handle, buf, static_cast(nbytes), &done, &overlapped); + if (!ok) { + if (!write && ::GetLastError() == ERROR_HANDLE_EOF) return 0; + errno = EIO; + return -1; + } + return done; +} static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) { - if (nbytes > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("NativePread: nbytes exceeds platform limit"); - } - std::int64_t saved = ::_lseeki64(descriptor, 0, SEEK_CUR); - if (saved < 0) { - return -1; - } - if (::_lseeki64(descriptor, offset, SEEK_SET) < 0) { - return -1; - } - int n = ::_read(descriptor, buf, static_cast(nbytes)); - ::_lseeki64(descriptor, saved, SEEK_SET); - return n; + return NativePositioned(descriptor, buf, nbytes, offset, false); } static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { - if (nbytes > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("NativePwrite: nbytes exceeds platform limit"); - } - std::int64_t saved = ::_lseeki64(descriptor, 0, SEEK_CUR); - if (saved < 0) { - return -1; - } - if (::_lseeki64(descriptor, offset, SEEK_SET) < 0) { - return -1; - } - int n = ::_write(descriptor, buf, static_cast(nbytes)); - ::_lseeki64(descriptor, saved, SEEK_SET); - return n; + return NativePositioned(descriptor, const_cast(buf), nbytes, offset, true); } #else #include @@ -273,9 +273,10 @@ int APS5_VABI unlink_nid_postfix(const char* path) { } int APS5_VABI sceKernelCheckReachability(const char* path) { - (void)path; - NotImplemented_nid_no_patch(__func__); - return 0; + if (path == nullptr) return SCE_KERNEL_ERROR_EINVAL; + if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; + std::error_code error; + return std::filesystem::exists(ResolvePath_nid_no_patch(path), error) ? 0 : SCE_KERNEL_ERROR_ENOENT; } int APS5_VABI sceKernelFstat(int d, FileStat* sb) { @@ -285,28 +286,25 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { } int APS5_VABI sceKernelFsync(int fd) { - (void)fd; - NotImplemented_nid_no_patch(__func__); - return 0; +#ifdef _WIN32 + return ::_commit(fd); +#else + return ::fsync(fd); +#endif } #ifdef _WIN32 -int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { - (void)fd; - (void)buf; - (void)nbytes; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { + if (buf == nullptr) return SceErrorFromErrno(GUEST_EFAULT); + if (nbytes <= 0) return SceErrorFromErrno(GUEST_EINVAL); + if (basep != nullptr) *basep = 0; + const int written = File::ReadDirectoryDescriptor(fd, buf, nbytes); + return written < 0 ? SceErrorFromErrno(GUEST_ENOTDIR) : written; } -int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* basep) { - (void)fd; - (void)buf; - (void)nbytes; - (void)basep; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { + return sceKernelGetdirentries(fd, buf, nbytes, nullptr); } #else @@ -371,21 +369,11 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { #ifdef _WIN32 int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { - (void)d; - (void)buf; - (void)nbytes; - (void)offset; - NotImplemented_nid_no_patch(__func__); - return 0; + return pread_nid_postfix(d, buf, nbytes, offset); } int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t offset) { - (void)d; - (void)buf; - (void)nbytes; - (void)offset; - NotImplemented_nid_no_patch(__func__); - return 0; + return pwrite_nid_disambig1_nid_postfix(d, buf, nbytes, offset); } #else diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index c91c1038a..5a50773e3 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -57,10 +57,8 @@ int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksiz if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); if (stacksize < 16384) throw std::runtime_error(std::string(__func__) + ": too small"); #ifdef _WIN32 - SYSTEM_INFO system{}; - GetSystemInfo(&system); - if (stacksize % system.dwPageSize != 0 || stacksize > std::numeric_limits::max()) - throw std::runtime_error(std::string(__func__) + ": invalid Windows stack size"); + if (stacksize > std::numeric_limits::max() - 0xffffu) + throw std::runtime_error(std::string(__func__) + ": stack size exceeds the Windows limit"); #endif (*attr)->_stacksize = stacksize; return SCE_OK; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 27b84bba6..98d952d68 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -210,11 +210,12 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread #ifdef _WIN32 SYSTEM_INFO system{}; GetSystemInfo(&system); - if (p->stackSize < 16384 || p->stackSize % system.dwPageSize != 0 || p->stackSize > std::numeric_limits::max()) + const std::size_t nativeStack = (p->stackSize + system.dwPageSize - 1) / system.dwPageSize * system.dwPageSize; + if (p->stackSize < 16384 || nativeStack > std::numeric_limits::max()) throw std::runtime_error("scePthreadCreate: invalid Windows stack size"); auto native = std::make_unique(NativeThreadArgs{std::move(args), start.get_future(), {}}); auto initialized = native->initialized.get_future(); - const auto handle = _beginthreadex(nullptr, static_cast(p->stackSize), StartNativeThread, native.get(), 0, nullptr); + const auto handle = _beginthreadex(nullptr, static_cast(nativeStack), StartNativeThread, native.get(), 0, nullptr); if (handle == 0) throw std::system_error(errno, std::generic_category(), "Creating guest thread"); p->nativeHandle = reinterpret_cast(handle); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 386c95ed5..be6727f14 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -225,6 +225,12 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ if (!Address(address, length, native, size)) return -1; return ::bind(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; } +int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { + const auto socket = Lookup(descriptor); + if (!socket) return Fail(9); + if (how < 0 || how > 2) return Fail(22); + return ::shutdown(socket->value, how) ? Fail(NativeError()) : 0; +} int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32_t* length) { const auto socket = Lookup(descriptor); if (!socket) return -1; @@ -235,6 +241,16 @@ int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32 GuestAddress(native, address, length); return 0; } +int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32_t* length) { + const auto socket = Lookup(descriptor); + if (!socket) return -1; + if (!address || !length) return Fail(14); + sockaddr_storage native{}; + socklen_t size = sizeof(native); + if (::getpeername(socket->value, reinterpret_cast(&native), &size)) return Fail(NativeError()); + GuestAddress(native, address, length); + return 0; +} int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { const auto socket = Lookup(descriptor); if (!socket) return -1; diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 9c6cc3986..1efdef414 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -23,4 +24,10 @@ int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void) { return 0; } +int APS5_VABI sceKernelDebugWriteCppExceptionInfo(const void* exception, uint64_t unknown, const char* typeName, const char* what) { + (void)unknown; + std::fprintf(stderr, "[coredump] uncaught C++ exception %p of type %s%s%s\n", exception, typeName ? typeName : "(unknown)", what ? ", what(): " : "", what ? what : ""); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 107359bc9..f5401a80b 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -15,16 +15,6 @@ int APS5_VABI sceCoredumpWriteUserData() { return 0; } -int APS5_VABI sceKernelAprGetFileSize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelAprGetFileStat() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelAprResolveFilepathsToIdsAndFileSizesForEach() { NotImplemented_nid_no_patch(__func__); return 0; @@ -35,16 +25,6 @@ int APS5_VABI sceKernelAprResolveFilepathsToIdsForEach() { return 0; } -int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIds() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizes() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsAndFileSizesForEach() { NotImplemented_nid_no_patch(__func__); return 0; @@ -55,29 +35,9 @@ int APS5_VABI sceKernelAprResolveFilepathsWithPrefixToIdsForEach() { return 0; } -int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceKernelAprWaitCommandBuffer() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelFtruncate() { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI shutdown_nid_postfix() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/ulobjmgr/Export.cpp b/core/libs/prx/ulobjmgr/Export.cpp new file mode 100644 index 000000000..528a0dfc2 --- /dev/null +++ b/core/libs/prx/ulobjmgr/Export.cpp @@ -0,0 +1,20 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI _sceUlobjmgrRegisterObject(void* object) { + (void)object; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _sceUlobjmgrUnregisterObject(void* object) { + (void)object; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 58dd3dfe3..7550e68a9 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -68,6 +68,7 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(snprintf_nid_postfix(buffer, 5, "%s", "abcdef") == 6); Require(std::strcmp(buffer, "abcd") == 0 && buffer[5] == '!'); Require(snprintf_nid_postfix(nullptr, 0, "%s:%d", "abcdef", 123) == 10); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%s]", static_cast(nullptr)) == 8 && std::strcmp(buffer, "[(null)]") == 0); buffer[0] = 'x'; Require(snprintf_nid_postfix(buffer, 1, "%d", 123) == 3 && buffer[0] == 0); Require(sprintf_nid_postfix(buffer, "%hhd %hhu %hd %hu %%", 255, 257, 65535, 65537) == 11); diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index a1953e7b0..0d3233446 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1,5 +1,8 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestHeap.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include #include "SceTypes.hpp" #include #include @@ -18,6 +21,11 @@ int APS5_VABI sceKernelMunmap(void*, std::size_t); int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); } static void Require(bool condition) { @@ -58,8 +66,192 @@ static void CheckNamedAndHintedMappings() { Require(sceKernelMunmap(first, length) == 0); } +static void CheckDirectMemoryFollowsPhysicalPages() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* first = nullptr; + Require(sceKernelMapDirectMemory(&first, page * 2, 3, 0, phys, 0) == 0); + static_cast(first)[0] = 11; + static_cast(first)[page + 5] = 22; + void* alias = nullptr; + Require(sceKernelMapDirectMemory(&alias, page, 3, 0, phys + page, 0) == 0); + Require(alias != first && static_cast(alias)[5] == 22); + static_cast(alias)[5] = 37; + Require(static_cast(first)[page + 5] == 37); + static_cast(first)[page + 6] = 48; + Require(static_cast(alias)[6] == 48); + Require(sceKernelMprotect(alias, page, 1) == 0); + static_cast(first)[page + 5] = 59; + Require(static_cast(alias)[5] == 59); + Require(sceKernelMprotect(alias, page, 3) == 0); + static_cast(alias)[5] = 22; + Require(static_cast(first)[page + 5] == 22); + Require(sceKernelMunmap(alias, page) == 0); + static_cast(first)[page + 5] = 22; + Require(sceKernelMunmap(first, page * 2) == 0); + void* filler = nullptr; + Require(sceKernelMapFlexibleMemory(&filler, page * 2, 3, 0) == 0); + void* second = nullptr; + Require(sceKernelMapDirectMemory(&second, page, 3, 0, phys + page, 0) == 0); + Require(second != first && static_cast(second)[5] == 22); + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); + void* fixed = reserved; + Require(sceKernelMapDirectMemory(&fixed, page, 1, 0x10, phys, 0) == 0); + Require(fixed == reserved && static_cast(fixed)[0] == 11); + Require(sceKernelMunmap(fixed, page) == 0); + Require(sceKernelMunmap(second, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); + std::int64_t again = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &again) == 0 && again == phys); + void* fresh = nullptr; + Require(sceKernelMapDirectMemory(&fresh, page * 2, 3, 0, again, 0) == 0); + Require(static_cast(fresh)[0] == 0 && static_cast(fresh)[page + 5] == 0); + Require(sceKernelMunmap(fresh, page * 2) == 0); + Require(sceKernelMunmap(filler, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(again, page * 2) == 0); +} + +static void CheckSharedDirectMemoryLifecycle() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, page * 3, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&second, page * 3, 3, 0, phys, 0) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(second, 0, &info, sizeof(info)) == 0); + Require(info.is_direct && !info.is_flexible && info.offset == static_cast(phys)); + auto* left = static_cast(first); + auto* right = static_cast(second); + left[0] = 31; + right[page] = 47; + left[page * 2] = 63; + Require(right[0] == 31 && left[page] == 47 && right[page * 2] == 63); + void* inaccessible = nullptr; + Require(sceKernelMapDirectMemory(&inaccessible, page, 0, 0, phys + page, 0) == 0); + left[page] = 48; + Require(sceKernelMprotect(inaccessible, page, 1) == 0); + Require(static_cast(inaccessible)[0] == 48); + Require(sceKernelMunmap(inaccessible, page) == 0); + Require(sceKernelMunmap(left + page, page) == 0); + right[page] = 79; + Require(left[0] == 31 && left[page * 2] == 63); + void* middle = left + page; + Require(sceKernelMapDirectMemory(&middle, page, 3, 0x10, phys + page, 0) == 0); + Require(left[page] == 79); + Require(sceKernelVirtualQuery(middle, 0, &info, sizeof(info)) == 0); + Require(info.offset == static_cast(phys) + page && info.start == reinterpret_cast(middle)); + Require(sceKernelMprotect(second, page * 3, 0) == 0); + left[page] = 95; + Require(sceKernelMprotect(second, page * 3, 1) == 0); + Require(right[page] == 95); + Require(sceKernelMunmap(first, page) == 0); + Require(sceKernelMunmap(left + page * 2, page) == 0); + Require(sceKernelMunmap(middle, page) == 0); + Require(sceKernelMprotect(second, page * 3, 3) == 0); + right[page * 2] = 111; + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 3, 0, 0) == 0); + void* fixed = static_cast(reserved) + page; + Require(sceKernelMapDirectMemory(&fixed, page, 3, 0x10, phys + page * 2, 0) == 0); + Require(static_cast(fixed)[0] == 111); + static_cast(fixed)[0] = 127; + Require(right[page * 2] == 127); + Require(sceKernelMapFlexibleMemory(&fixed, page, 3, 0x10) == 0); + Require(static_cast(fixed)[0] == 0); + Require(sceKernelVirtualQuery(fixed, 0, &info, sizeof(info)) == 0); + Require(!info.is_direct && info.is_flexible && info.offset == 0); + static_cast(fixed)[0] = 143; + Require(right[page * 2] == 127); + Require(sceKernelMunmap(reserved, page * 3) == 0); + Require(sceKernelMunmap(second, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(phys + page, page) == 0); + std::int64_t replacement = 0; + Require(sceKernelAllocateDirectMemory(phys + page, phys + page * 2, page, 0, 0, &replacement) == 0); + Require(replacement == phys + page); + void* mixed = nullptr; + Require(sceKernelMapDirectMemory(&mixed, page * 3, 3, 0, phys, 0) == 0); + const auto* data = static_cast(mixed); + Require(data[0] == 31 && data[page] == 0 && data[page * 2] == 127); + Require(sceKernelMunmap(mixed, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); +} + +static void CheckHeapAfterMappingReuse() { + constexpr std::size_t bytes = 0x30000; + auto* pointer = static_cast(GuestHeap::GuestHeapAllocate_nid_postfix(bytes)); + std::memset(pointer, 0x5a, bytes); + Require(pointer[0] == 0x5a && pointer[bytes - 1] == 0x5a); + GuestHeap::GuestHeapFree_nid_postfix(pointer); + pointer = static_cast(GuestHeap::GuestHeapAllocate_nid_postfix(bytes)); + std::memset(pointer, 0xa5, bytes); + Require(pointer[0] == 0xa5 && pointer[bytes - 1] == 0xa5); + GuestHeap::GuestHeapFree_nid_postfix(pointer); +} + +static void CheckSharedWriteTracking() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); + void* first = nullptr; + void* second = nullptr; + Require(sceKernelMapDirectMemory(&first, page * 3, 3, 0, phys, 0) == 0); + Require(sceKernelMapDirectMemory(&second, page * 3, 3, 0, phys, 0) == 0); + const auto collect = [](void* address, std::size_t bytes, bool clear = true) { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(address), bytes, pages.data(), &count, clear)); + return count; + }; + Require(collect(first, page * 3) == 12); + Require(collect(second, page * 3) == 12); + Require(collect(first, page * 3) == 0); + Require(collect(second, page * 3) == 0); + auto* left = static_cast(first); + auto* right = static_cast(second); + left[page + 5] = 21; + Require(right[page + 5] == 21); + Require(collect(first, page * 3, false) == 4); + Require(collect(first, page * 3, false) == 4); + Require(collect(first, page * 3) == 4); + Require(collect(second, page * 3) == 4); + Require(collect(first, page * 3) == 0); + right[page * 2] = 42; + Require(collect(first, page * 3) == 4); + Require(collect(second, page * 3) == 4); + Require(collect(second, page * 3) == 0); + Require(sceKernelMprotect(first, page * 3, 1) == 0); + collect(first, page * 3); + collect(second, page * 3); + right[0] = 63; + Require(left[0] == 63 && collect(first, page * 3) == 4); + Require(sceKernelMprotect(first, page * 3, 3) == 0); + collect(first, page * 3); + left[0] = 84; + Require(right[0] == 84 && collect(second, page * 3) != 0); + void* third = nullptr; + Require(sceKernelMapDirectMemory(&third, page, 3, 0, phys + page, 0) == 0); + collect(first, page * 3); + collect(second, page * 3); + static_cast(third)[0] = 105; + Require(collect(first, page * 3) == 4 && collect(second, page * 3) == 4); + Require(sceKernelMunmap(third, page) == 0); + Require(sceKernelMunmap(first, page * 3) == 0); + Require(sceKernelMunmap(second, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); +#endif +} + int main() { CheckNamedAndHintedMappings(); + CheckDirectMemoryFollowsPhysicalPages(); + CheckSharedDirectMemoryLifecycle(); + CheckHeapAfterMappingReuse(); + CheckSharedWriteTracking(); constexpr std::size_t page = 0x4000; const auto failed = reinterpret_cast(static_cast(-1)); const auto reject = [&](std::size_t length, int protection, int flags, int fd, diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 7d5dc0db3..15005aa2a 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -1,6 +1,7 @@ #ifndef CORE_RELINKER_CLI_INCLUDE_CLI_HPP #define CORE_RELINKER_CLI_INCLUDE_CLI_HPP +#include #include #include @@ -20,6 +21,7 @@ struct Args { std::string inputPath; std::string outputPath; std::string runPath = "$ORIGIN/libs"; + std::set excludedSceModules; }; Args ParseArgs(int argc, char* argv[]); diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 2514a24c7..212d084d8 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -15,6 +15,10 @@ Args ParseArgs(int argc, char* argv[]) { args.skipSyscallCheck = true; } else if (arg == "--skip-sce-module") { args.skipSceModule = true; + } else if (arg == "--exclude-sce-module") { + if (i + 1 >= argc) + throw std::runtime_error("--exclude-sce-module requires a file name"); + args.excludedSceModules.insert(argv[++i]); } else if (arg == "--to-intel") { args.toIntel = true; } else if (arg.rfind("unused-filter=", 0) == 0) { @@ -50,6 +54,9 @@ Args ParseArgs(int argc, char* argv[]) { } } + if (args.skipSceModule && !args.excludedSceModules.empty()) + throw std::runtime_error("--exclude-sce-module conflicts with --skip-sce-module"); + if (args.windowsDiagnostics && !args.toWindows) throw std::runtime_error("--windows-diagnostics requires --windows"); @@ -58,7 +65,7 @@ Args ParseArgs(int argc, char* argv[]) { if (args.inputPath.empty() || args.outputPath.empty()) throw std::runtime_error( - "Usage: relinker [--windows] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check] [--skip-sce-module] [--to-intel] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + "Usage: relinker [--windows] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check] [--skip-sce-module] [--exclude-sce-module ]... [--to-intel] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf" ); diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index f76cfeb6f..ccd03f983 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -35,6 +35,7 @@ std::vector InstructionScanner::ScanCodeSection( try { length = decoder.Decode(cursor, available); } catch (const CodegenException& e) { + if (std::all_of(cursor, cursor + available, [](const std::uint8_t b) { return b == 0; })) break; throw CodegenException(std::string("Cannot decode instruction: ") + e.what(), codeSectionOffset + i); } diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 280e82980..3d2c49ccd 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -392,6 +393,14 @@ void registerFormExecution() { void registerFormExecution() {} #endif +void scannerZeroTail() { + const auto scanner = Codegen::MakeInstructionScanner(); + const Bytes code{0xC3, 0x00, 0x00, 0x00}; + require(scanner->ScanCodeSection(code, 0, code.size()).size() == 2, "Odd zero padding at segment tail must end the scan"); + const Bytes truncated{0xC3, 0x0F}; + requireFailure([&] { (void)scanner->ScanCodeSection(truncated, 0, truncated.size()); }, "Truncated non-zero tail must still fail"); +} + int main() { try { decoderLengths(); @@ -402,6 +411,7 @@ int main() { converterSegment(); converterFailureOffsets(); linuxPlacement(); + scannerZeroTail(); std::cout << "AMD64-only converter tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 705c3f6b0..0802223bf 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -71,6 +71,7 @@ int main(const int argc, char* argv[]) { std::cout << "System: " << (args.toWindows ? "Windows" : "Linux") << "; unused-filter=" << args.unusedFilterLevel << "\n"; std::cout << "sce_module/sce_modules processing: " << (args.skipSceModule ? "disabled (--skip-sce-module)" : "enabled") << '\n'; + for (const auto& name : args.excludedSceModules) std::cout << "sce_module excluded: " << name << '\n'; auto result = pipeline->Relink(sourceBytes); for (const auto& patch : result.Patches) { if (patch.Offset > sourceBytes.size() || patch.Bytes.size() > sourceBytes.size() - patch.Offset) @@ -80,7 +81,7 @@ int main(const int argc, char* argv[]) { std::vector guestArtifacts; if (!args.skipSceModule) { - guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, *syscallScanner, args.lazyBinding, args.runPath); + guestArtifacts = Relinker::GuestModuleBuilder().Build(args.inputPath, absPath, result.DynamicSection, args.toWindows, args.toIntel, *syscallScanner, args.lazyBinding, args.runPath, args.excludedSceModules); } if (args.writeRegistry) { diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 71cca90fa..f85029698 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include namespace Relinker { @@ -44,7 +46,7 @@ struct GuestArtifact { class GuestModuleBuilder { public: - std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool toIntel, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath) const; + std::vector Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, bool windows, bool toIntel, ISyscallScanner& syscallScanner, bool lazyBinding, const std::string& runPath, const std::set& excludedModules) const; }; } diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index a8655e1e4..aa68ea39d 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -11,7 +11,7 @@ namespace Relinker { -std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool toIntel, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath) const { +std::vector GuestModuleBuilder::Build(const std::filesystem::path& inputPath, const std::filesystem::path& outputPath, Domain::SysVDynamicSection& dynamic, const bool windows, const bool toIntel, ISyscallScanner& syscallScanner, const bool lazyBinding, const std::string& runPath, const std::set& excludedModules) const { const auto root = std::filesystem::absolute(inputPath).parent_path(); const auto singular = root / "sce_module"; const auto plural = root / "sce_modules"; @@ -22,8 +22,10 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto directory = hasSingular ? singular : plural; if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); std::vector paths; + std::set unmatchedExclusions = excludedModules; for (const auto& entry : std::filesystem::directory_iterator(directory)) { if (entry.path().filename().string().ends_with(GuestModuleSuffix)) continue; + if (unmatchedExclusions.erase(entry.path().filename().string()) != 0) continue; if (!entry.is_regular_file()) continue; std::ifstream stream(entry.path(), std::ios::binary); if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + entry.path().string()); @@ -32,6 +34,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + entry.path().string()); if (stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') paths.push_back(entry.path()); } + if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded sce_module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); if (paths.empty()) return {}; if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index 148746e9d..8632c87b9 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -27,7 +27,7 @@ struct Range { }; enum class FaultState : std::uint32_t { Empty, Writing, Ready }; -enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit }; +enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned }; struct Fault { FaultState state; @@ -45,6 +45,14 @@ static_assert(offsetof(Range, deviceAddress) == 16 && offsetof(Range, permission static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v && sizeof(Fault) == 32); static_assert(offsetof(Fault, address) == 8 && offsetof(Fault, instruction) == 24); +inline constexpr std::uint32_t WrittenPageShift = 12; +inline constexpr std::uint32_t WrittenPageSlots = 4096; +inline constexpr std::uint32_t WrittenPageProbes = 8; +inline constexpr std::uint32_t WrittenOverflowWord = sizeof(Fault) / sizeof(std::uint32_t); +inline constexpr std::uint32_t WrittenSlotsWord = WrittenOverflowWord + 1; +inline constexpr std::size_t FaultBufferBytes = (WrittenSlotsWord + WrittenPageSlots) * sizeof(std::uint32_t); +static_assert((WrittenPageSlots & (WrittenPageSlots - 1)) == 0); + } #endif diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 24e4b66b4..bf86cb817 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -103,6 +103,7 @@ private: append(key, value.groupIdEnable); append(key, value.tgSizeEnable); append(key, value.threadIdComponentCount); + append(key, value.PartialGroups()); } static void append(std::vector& key, const ShaderPixelStageInfo& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index be8e4e2f7..0588b5da7 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -660,7 +660,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(2u); + writer.WriteU32(3u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -670,6 +670,11 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writeGraphicsCompileContext(writer, *request.graphics); } writer.WriteBool(request.useCache); + if (request.context.compute.has_value()) { + for (const std::uint32_t value : request.context.compute->partialThreads) { + writer.WriteU32(value); + } + } return base64Encode(buffer); } @@ -678,7 +683,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version != 1u && version != 2u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 3u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result); @@ -688,7 +693,12 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.graphicsStorage = std::make_unique(); result.request.graphics = readGraphicsCompileContext(reader, *result.graphicsStorage); } - if (version == 2u) result.request.useCache = reader.ReadBool(); + if (version >= 2u) result.request.useCache = reader.ReadBool(); + if (version >= 3u && result.request.context.compute.has_value()) { + for (std::uint32_t& value : result.request.context.compute->partialThreads) { + value = reader.ReadU32(); + } + } return result; } diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 591c76217..9a138a090 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -288,6 +288,25 @@ bool canReachBefore(const ControlFlowGraph& graph, std::uint32_t start, std::uin return false; } +bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t start, std::uint32_t target) { + std::vector pending = {start}; + std::vector visited(graph.blocks.size(), false); + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + if (blockId == target) { + return true; + } + if (blockId == loop.continueBlock || blockId == loop.mergeBlock || visited[blockId]) { + continue; + } + visited[blockId] = true; + const auto& block = graph.FindBlock(blockId); + pending.insert(pending.end(), block.successors.begin(), block.successors.end()); + } + return false; +} + std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block) { const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); const auto* loop = findInnermostContainingLoop(graph, block.id); @@ -325,6 +344,13 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock if (isLoopControlGateway(graph, *loop, falseTarget) && graph.Dominates(block.id, falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)) { return falseTarget; } + if (globalMerge != InvalidControlFlowId) { + const bool trueJoins = reachesWithinIteration(graph, *loop, trueTarget, globalMerge); + const bool falseJoins = reachesWithinIteration(graph, *loop, falseTarget, globalMerge); + if (trueJoins != falseJoins) { + return trueJoins ? trueTarget : falseTarget; + } + } return globalMerge; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index 9ebdb4267..7ea1c1823 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -49,12 +49,16 @@ struct IrBindingLayout { std::uint32_t pushDataStartDword = PushData::NoStart; std::uint32_t memoryOffsetDword = 0; std::uint32_t memoryOffsetCount = 0; + bool dispatchThreadLimit = false; std::vector userDataRegisters; std::vector descriptors; - [[nodiscard]] std::uint32_t ShaderDataDwords() const { + [[nodiscard]] std::uint32_t DispatchThreadLimitDword() const { return memoryOffsetDword + (memoryOffsetCount + 3u) / 4u; } + [[nodiscard]] std::uint32_t ShaderDataDwords() const { + return DispatchThreadLimitDword() + (dispatchThreadLimit ? 3u : 0u); + } [[nodiscard]] bool UsesPushData() const { return pushDataStartDword != PushData::NoStart; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp index 7abb73613..f5020e2a8 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourceInfo.hpp @@ -45,6 +45,8 @@ struct MemoryInfo { bool idxen = false; bool offen = false; bool planningOnly = false; + bool coherent = false; + bool gpuDescriptor = false; bool operator==(const MemoryInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 26e6f78c1..5d20bc6ac 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -25,6 +25,7 @@ struct BufferResource { bool atomic = false; bool formatted = false; bool scalar = false; + bool empty = false; bool operator==(const BufferResource& other) const = default; }; @@ -49,6 +50,7 @@ struct ImageResource { bool depthCompare = false; bool cube = false; bool r128 = false; + bool depthBits = false; std::uint32_t indirectRoot = NoIndirectImage; std::uint32_t indirectMappingOffset = 0; std::uint32_t indirectSearchIterations = 0; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 39228dc3b..23fe56c39 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -32,6 +32,8 @@ struct ShaderInfo { bool instanceOffsetConflict = false; bool hasBitwiseXor = false; bool usesDma = false; + bool bdaWrites = false; + bool dispatchThreadLimit = false; bool operator==(const ShaderInfo& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index af874c1f8..ec74234c2 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -33,6 +33,7 @@ enum class StageInputKind { LocalInvocationId, LocalInvocationIndex, GlobalInvocationId, + DispatchThreadLimit, Parameter, }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index ce0db25cb..9aa0fa561 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -341,6 +341,7 @@ enum class IrOpcode : std::uint16_t { TtraceData, InstPrefetch, SelectU32, + ImageBvhIntersectRay, Count }; diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index e7aa60504..2c44ed9cb 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -382,6 +382,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("TtraceData", Void), makeMeta("InstPrefetch", Void), makeMeta("SelectU32", U32, U1, U32, U32), + makeMeta("ImageBvhIntersectRay", U32x4, U32x4, ImageAddress, U1), }}; const OpcodeMeta& metaOf(IrOpcode opcode) { diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index f417cbbdf..bac6a047a 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -9,8 +9,8 @@ namespace ShaderRecompiler { class DescriptorBindingBuilder { public: - void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot) const; - void Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot) const; + void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; + void Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; }; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 762ac4cf5..8ef7c5cdb 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -13,6 +13,7 @@ struct ResourceSpecialization { std::uint32_t packedStride = 0; IrBufferFormat descriptorFormat = IrBufferFormat::Invalid; std::uint32_t descriptorSwizzle = ShaderImageIdentitySwizzle; + bool empty = false; bool operator==(const Buffer& other) const; }; @@ -28,6 +29,7 @@ struct ResourceSpecialization { std::uint32_t indirectSearchIterations = 0; bool cube = false; bool fmask = false; + bool depthBits = false; bool operator==(const Image& other) const; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index e1a9a0452..44757fdc8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -147,9 +147,8 @@ struct ShaderVertexInputInfo { }; struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo { - std::uint32_t dispatchThreadsNum[3] = {0, 0, 0}; bool groupId[3] = {false, false, false}; - bool dispatchThreadDimensions = false; + bool partialGroups = false; int threadIdsNum = 0; int workgroupRegister = 0; bool tgSizeEn = false; diff --git a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp index 362fe9cc4..ed0b6987d 100644 --- a/core/shader/recompiler/Optimization/src/BindingAllocator.cpp +++ b/core/shader/recompiler/Optimization/src/BindingAllocator.cpp @@ -95,6 +95,7 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin next.userDataRegisters = collectUserData(program); next.memoryOffsetDword = static_cast(next.userDataRegisters.size()); next.memoryOffsetCount = static_cast(info.buffers.size()); + next.dispatchThreadLimit = info.dispatchThreadLimit; const std::uint32_t pushDataStartDword = layout.pushConstantOffsetBytes / 4u; const std::uint32_t pushConstantSizeDwords = layout.pushConstantSizeBytes / 4u; const bool usesPushData = next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushConstantSizeDwords; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index eac82b77c..cc715ce6a 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -1,6 +1,7 @@ #include "Optimization/DescriptorBindingBuilder.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include +#include #include #include #include @@ -178,7 +179,7 @@ std::vector GuestSamplersDescriptor(const std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot) { +std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); i++) { const std::uint32_t reg = layout.userDataRegisters[i]; @@ -187,18 +188,24 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st } result[i] = snapshot.userData[reg - userDataBase]; } + if (layout.dispatchThreadLimit) { + if (partialThreads == std::array{}) { + fail("DescriptorBindingBuilder::Populate partial-group shader has no dispatch size"); + } + std::copy(partialThreads.begin(), partialThreads.end(), result.begin() + layout.DispatchThreadLimitDword()); + } return result; } } -void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot) const { - Populate(allocation, program.Info(), program.Resources().stage, program.Resources().userDataBase, snapshot); +void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { + Populate(allocation, program.Info(), program.Resources().stage, program.Resources().userDataBase, snapshot, partialThreads); } -void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot) const { +void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { const IrBindingLayout& layout = allocation.layout; - const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot); + const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads); std::vector bindings; bindings.reserve(layout.descriptors.size()); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 37ec5bea2..4fb6ca805 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -42,6 +42,7 @@ struct DecodedImage { std::uint32_t shaderSwizzle = ShaderImageIdentitySwizzle; bool cube = false; bool fmask = false; + bool depthBits = false; }; ShaderBufferResource decodeBufferDescriptor(const DescriptorValue& value) { @@ -161,6 +162,11 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag } const bool rawSintStorage = storage && format == IrBufferFormat::Format32SInt && base.written && !base.read && !base.atomic; decoded.numericClass = SampledTextureNumericClass(format); + if (!storage && !base.depthCompare && IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) { + decoded.depthBits = true; + decoded.numericClass = IrTextureNumericClass::Float; + decoded.shaderSwizzle = descriptorImageSwizzle(descriptor); + } if (storage) { if ((!rawSintStorage && decoded.numericClass == IrTextureNumericClass::Sint) || decoded.numericClass == IrTextureNumericClass::Unsupported) { throw std::runtime_error("storage image descriptor uses an unsupported format"); @@ -175,7 +181,7 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag } bool requiresPointSampler(const ResourceSpecialization::Image& image) { - return image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid; + return image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits; } constexpr std::uint32_t TableEntryBytes = 32; @@ -501,6 +507,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.packedStride = packedStride; entry.descriptorFormat = buffer.formatted ? decoded.Format() : IrBufferFormat::Invalid; entry.descriptorSwizzle = buffer.formatted ? decoded.DstSelXYZW() : DstSel(4, 5, 6, 7); + entry.empty = decoded.GetSize() == 0u || decoded.Base48() == 0u; result.buffers.push_back(entry); } @@ -522,6 +529,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.indirectSearchIterations = 0u; entry.cube = decoded.cube; entry.fmask = decoded.fmask; + entry.depthBits = decoded.depthBits; result.images.push_back(entry); } @@ -583,6 +591,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio buffers[i].packedStride = specialization.buffers[i].packedStride; buffers[i].descriptorFormat = specialization.buffers[i].descriptorFormat; buffers[i].descriptorSwizzle = specialization.buffers[i].descriptorSwizzle; + buffers[i].empty = specialization.buffers[i].empty; } auto images = resources.info.images; @@ -605,6 +614,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio image.indirectMappingOffset = source.indirectMappingOffset; image.indirectSearchIterations = source.indirectSearchIterations; image.cube = source.cube; + image.depthBits = source.depthBits; image.indirectResources.clear(); } for (std::uint32_t index = 0; index < images.size(); index++) { @@ -866,11 +876,11 @@ void ResourceMaterializer::CountBindlessRejection(BindlessRejection reason) { } bool ResourceSpecialization::Buffer::operator==(const Buffer& other) const { - return packedStride == other.packedStride && descriptorFormat == other.descriptorFormat && descriptorSwizzle == other.descriptorSwizzle; + return packedStride == other.packedStride && descriptorFormat == other.descriptorFormat && descriptorSwizzle == other.descriptorSwizzle && empty == other.empty; } bool ResourceSpecialization::Image::operator==(const Image& other) const { - return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask; + return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits; } bool ResourceSpecialization::operator==(const ResourceSpecialization& other) const { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 933ad2454..c7ced39e6 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -96,6 +96,7 @@ public: m_info.samplers.clear(); m_info.sampledPairs.clear(); m_info.usesDma = false; + m_info.bdaWrites = false; } void Run() { @@ -523,6 +524,30 @@ private: source = InternSource(descriptor); } + bool TakeGpuDescriptor(IrValue& inst, std::uint32_t memoryIndex) { + const IrValue* handle = inst.Argument(0)->Resolve(); + if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { + return false; + } + DescriptorSource descriptor; + MakeSource(*handle, 4u, false, false, descriptor); + std::uint32_t badDword = 0; + if (ValidateSource(descriptor, badDword)) { + return false; + } + const auto op = inst.Opcode(); + const bool load = op == IrOpcode::LoadBufferU32 || op == IrOpcode::LoadBufferU32x2 || op == IrOpcode::LoadBufferU32x3 || op == IrOpcode::LoadBufferU32x4 || op == IrOpcode::ReadConstBuffer; + const bool store = op == IrOpcode::StoreBufferU32 || op == IrOpcode::StoreBufferU32x2 || op == IrOpcode::StoreBufferU32x3 || op == IrOpcode::StoreBufferU32x4; + auto& memory = m_program.Resources().memoryInfo[memoryIndex]; + if ((!load && !store) || memory.formatted || memory.typed || memory.dataBits != 32u) { + return false; + } + memory.gpuDescriptor = true; + m_info.usesDma = true; + m_info.bdaWrites = m_info.bdaWrites || store; + return true; + } + void ValidateAddressHandle(IrValue* value) const { const IrValue* handle = value->Resolve(); if (handle->Opcode() != IrOpcode::GetAddressResource) { @@ -659,6 +684,10 @@ private: void Collect(IrValue& inst) { const auto op = inst.Opcode(); + if (op == IrOpcode::ImageBvhIntersectRay) { + m_info.usesDma = true; + return; + } const auto buffer = BufferAccessOf(op); const auto addressInfo = AddressOpcodeInfoOf(op); const auto imageInfo = ImageOpcodeInfoOf(op); @@ -681,6 +710,9 @@ private: std::uint32_t resource = 0; if (buffer != BufferAccess::None) { + if (TakeGpuDescriptor(inst, flags.index)) { + return; + } GetHandle(inst.Argument(0), IrOpcode::GetBufferResource, 4, handle, source); resource = AddBuffer(source, memory, op, flags.pc); if (resource == std::numeric_limits::max()) { diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index ac8d97456..84916b283 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -148,6 +148,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu case StageInputKind::WorkgroupId: case StageInputKind::LocalInvocationId: case StageInputKind::GlobalInvocationId: + case StageInputKind::DispatchThreadLimit: if (component >= 3u) { return Fail("typed invocation builtin component is out of range"); } @@ -249,9 +250,6 @@ void CollectComputeInputs(const ShaderComputeInputInfo* compute, ShaderInfo& inf if (compute->threadIdsNum > 0 || compute->tgSizeEn) { AddInput(info, StageInputKind::LocalInvocationIndex, 0, 1, "gl_LocalInvocationIndex"); } - if (compute->dispatchThreadDimensions) { - AddInput(info, StageInputKind::GlobalInvocationId, 0, 3, "gl_GlobalInvocationID"); - } } void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { @@ -310,6 +308,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::GlobalInvocationId: AddInput(info, kind, 0, 3, "gl_GlobalInvocationID"); break; + case StageInputKind::DispatchThreadLimit: + info.dispatchThreadLimit = true; + break; case StageInputKind::PackedAncillary: case StageInputKind::Parameter: break; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index b6927d343..f043c6c55 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -20,6 +20,7 @@ enum class ImageType : std::uint32_t { [[nodiscard]] bool IsFmaskTextureFormat(IrBufferFormat format); [[nodiscard]] IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapTextureFormat(IrBufferFormat format); +[[nodiscard]] bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3); } diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp index bd8bfbb80..e27d5fb18 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaInstruction.hpp @@ -148,6 +148,7 @@ struct RdnaInstruction { bool formatted = false; bool gds = false; bool glc = false; + bool dlc = false; bool slc = false; bool idxen = false; bool offen = false; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 33cb9cf71..e8d2f594c 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -121,4 +121,11 @@ IrBufferFormat RemapTextureFormat(IrBufferFormat format) { return format == IrBufferFormat::Format11_11_10UInt ? IrBufferFormat::Format32UInt : format; } +bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3) { + const auto format = static_cast((word1 >> 20u) & 0x1ffu); + const auto swizzle = (word3 >> 20u) & 0x1fu; + const bool depthLayout = swizzle == 4u || swizzle == 8u || swizzle == 16u || swizzle == 20u || swizzle == 24u; + return depthLayout && (format == IrBufferFormat::Format32UInt || format == IrBufferFormat::Format32SInt); +} + } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index b9a95735e..5d0aaf657 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -1,5 +1,6 @@ #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include +#include #include #include @@ -82,8 +83,8 @@ void validateFlags(std::uint32_t flags) { if ((flags & ~known) != 0u) { throw std::runtime_error("unknown image address flags"); } - if ((flags & (RdnaImageSampleFlagLodClamp | RdnaImageSampleFlagCd | RdnaImageSampleFlagAdjust)) != 0u) { - throw std::runtime_error("unsupported image clamp, coarse derivative or adjustment address layout"); + if ((flags & RdnaImageSampleFlagCd) != 0u) { + throw std::runtime_error("unsupported image coarse derivative (_cd) address layout"); } const auto lodModes = flags & (RdnaImageSampleFlagLod | RdnaImageSampleFlagBias | RdnaImageSampleFlagDerivative | RdnaImageSampleFlagLevelZero); if (std::popcount(lodModes) > 1) { @@ -225,9 +226,12 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 18u) & 0x7Fu) | ((word0 & 1u) << 7u); - const auto reservedWord0 = opcode == 0xe6u ? 0x000340C0u : 0x000350C0u; + const auto& info = lookupOpcode(opcode); + const auto reservedWord0 = info.sample || info.gather ? 0x000350C0u : 0x000340C0u; if ((word0 & reservedWord0) != 0u || (word1 & 0x3C000000u) != 0u) { - throw std::runtime_error("unsupported or reserved MIMG control bits"); + char message[96]; + std::snprintf(message, sizeof(message), "unsupported or reserved MIMG control bits (words %08x %08x)", word0, word1); + throw std::runtime_error(message); } const auto nsa = (word0 >> 1u) & 3u; const auto wordCount = 2u + nsa; @@ -237,7 +241,6 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span std::numeric_limits::max() - (wordCount * 4u - 1u)) { throw std::runtime_error("invalid MIMG program counter"); } - const auto& info = lookupOpcode(opcode); const bool a16 = (word1 & 0x40000000u) != 0u; const bool d16 = (word1 & 0x80000000u) != 0u; const auto flags = info.flags | (a16 ? RdnaImageSampleFlagA16 : 0u); @@ -254,17 +257,19 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 8u) & 255u; if (nsa != 0u && addressDwords > 1u + nsa * 4u) { @@ -303,7 +308,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 12u) & 1u) != 0u; instruction.idxen = ((word0 >> 13u) & 1u) != 0u; instruction.glc = ((word0 >> 14u) & 1u) != 0u; + instruction.dlc = ((word0 >> 15u) & 1u) != 0u; instruction.slc = ((word1 >> 22u) & 1u) != 0u; applyMemoryInfo(instruction, info); setRawWords(instruction, code, wordIndex, 2u); @@ -462,6 +463,7 @@ RdnaInstruction DecodeRdnaMtbuf(std::uint32_t programCounter, std::span> 12u) & 1u) != 0u; instruction.idxen = ((word0 >> 13u) & 1u) != 0u; instruction.glc = ((word0 >> 14u) & 1u) != 0u; + instruction.dlc = ((word0 >> 15u) & 1u) != 0u; instruction.slc = ((word1 >> 22u) & 1u) != 0u; applyMemoryInfo(instruction, info); setRawWords(instruction, code, wordIndex, 2u); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index cb3fb15f1..63161841b 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -1,4 +1,5 @@ #include "RdnaDecoder/RdnaVectorOpDecoder.hpp" +#include #include #include #include @@ -729,6 +730,8 @@ constexpr Vop1SdwaRule vop1SdwaRules[] = { {RdnaOpcode::VCvtF32Ubyte0, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VNotB32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VFfblB32, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VFfbhU32, sdwaSelAll(), 0, 0, false}, + {RdnaOpcode::VBfrevB32, sdwaSelAll(), 0, 0, false}, {RdnaOpcode::VCvtF32F16, sdwaSelWords() | sdwaSelFull(), 0, 0, true}, {RdnaOpcode::VCvtF16F32, sdwaSelAll(), sdwaSelWords(), sdwaSelAll(), true}, {RdnaOpcode::VCvtF16U16, sdwaSelWords() | sdwaSelFull(), sdwaSelWords(), sdwaSelWords() | sdwaSelFull(), false}, @@ -796,7 +799,9 @@ void validateVop1Sdwa(const RdnaInstruction& instruction, std::uint32_t destinat throw std::invalid_argument("VOP1 SDWA destination selector is not supported"); } if (!isVop1SdwaSourceSupported(instruction.op, sourceSelector, sourceNegate, sourceAbsolute)) { - throw std::invalid_argument("VOP1 SDWA source selector is not supported"); + char text[96]; + std::snprintf(text, sizeof(text), "VOP1 SDWA source selector %u of opcode 0x%x at pc 0x%x is not supported", sourceSelector, instruction.opcodeId, instruction.programCounter); + throw std::invalid_argument(text); } } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 0e0552bf4..3fa71d680 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -97,6 +97,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { TranslateOptions translateOptions {}; translateOptions.stage = stageKind; + translateOptions.shaderHash = request.shader.codeAddress; translateOptions.waveSize = request.context.waveSize; translateOptions.userDataBaseRegister = request.context.userDataBaseRegister; translateOptions.userDataCount = static_cast(request.context.userData.size()); @@ -273,6 +274,10 @@ std::shared_ptr getSource(const RecompileRequest& request) { return source; } +std::array partialThreads(const RecompileRequest& request) { + return request.context.compute ? request.context.compute->partialThreads : std::array{}; +} + CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization) { const auto inputInfo = BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request)); constexpr DeadCodeEliminator deadCodeEliminator; @@ -289,7 +294,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra auto bindings = bindingAllocator.Allocate(program, request.layout); constexpr DescriptorBindingBuilder descriptorBindingBuilder; - descriptorBindingBuilder.Populate(bindings, program, resourceSnapshot); + descriptorBindingBuilder.Populate(bindings, program, resourceSnapshot, partialThreads(request)); SpirvTargetOptions targetOptions {}; targetOptions.vulkanVersion = request.target.vulkanVersion; @@ -310,6 +315,7 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra #endif result.bdaAbiVersion = program.Info().usesDma ? request.target.bdaAbiVersion : 0u; + result.memoryOffsetDword = bindings.layout.memoryOffsetDword; result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr; result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr; result.vertexOffsetShared = program.Info().vertexOffsetShared; @@ -343,7 +349,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil bindings.layout = variant.bindings.layout; bindings.pushConstantOffsetBytes = variant.bindings.pushConstantOffsetBytes; bindings.pushConstantSizeBytes = variant.bindings.pushConstantSizeBytes; - DescriptorBindingBuilder{}.Populate(bindings, variant.info.info, variant.info.stage, variant.info.userDataBase, snapshot); + DescriptorBindingBuilder{}.Populate(bindings, variant.info.info, variant.info.stage, variant.info.userDataBase, snapshot, partialThreads(request)); result.bindings = std::move(bindings.bindings); result.pushConstants = std::move(bindings.pushConstants); for (auto& attribute : result.vertexAttributes) { @@ -466,6 +472,7 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh for (const auto stride : snapshot.uniformFill.groupStride) mix(stride); mix(snapshot.uniformFill.words); mix(snapshot.uniformFill.value); + for (const auto threads : partialThreads(request)) mix(threads); if (request.context.vertex) { const auto& vertex = *request.context.vertex; const auto count = std::min(vertex.resourcesNum, ShaderVertexStageInfo::MaxResources); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 2cf0221f7..e24ea1bcd 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -43,6 +43,11 @@ struct ShaderComputeStageInfo { std::array groupIdEnable; bool tgSizeEnable; std::uint32_t threadIdComponentCount; + std::array partialThreads; + + [[nodiscard]] bool PartialGroups() const { + return partialThreads != std::array{}; + } }; struct ShaderPixelStageInfo { @@ -319,6 +324,7 @@ struct RecompileResult { SharedSpirv spirv; std::vector bindings; std::vector pushConstants; + std::uint32_t memoryOffsetDword = 0; std::uint32_t bdaAbiVersion = 0; std::vector vertexAttributes; std::int32_t vertexOffsetSgpr = -1; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index a8f27bdf0..c28e6ce6f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -18,6 +18,7 @@ struct SpirvRequirements { bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; + bool coherentBuffers = false; std::vector capabilities; std::vector extensions; }; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index be56f6c72..f7c278915 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -20,11 +20,11 @@ void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& targe bool BdaInvocationsMayStop(const IrProgram& program); void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition); std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits); +void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value); // Reads the dwords of a 1-4 dword load at address + offset that the program extracts, with one // table lookup for the whole span; the per-byte lookups of EmitBdaRead remain the fallback (and // the only path under APS5_BDA_BYTE_READS=1). Dwords the program never extracts are neither read -// nor fault-checked, as when every dword was its own instruction. -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords); +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); std::uint32_t AddBdaImmediate(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::int32_t immediate); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 0d73dbf0c..c7fd6db42 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -269,6 +269,7 @@ std::uint32_t EmitDataConsume(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryDimensions(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageQueryLod(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageRead(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageWrite(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageSampleRaw(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageGatherRaw(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 373d8ff2a..497824dbf 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -48,6 +48,7 @@ struct ImageSampleLayout { std::uint32_t bias = NoImageComponent; std::uint32_t coord = 0; std::uint32_t lod = NoImageComponent; + std::uint32_t clamp = NoImageComponent; std::uint32_t gradX = NoImageComponent; std::uint32_t gradY = NoImageComponent; }; @@ -65,6 +66,7 @@ struct MemoryResourceAccess { std::uint32_t indexOffset = 0; std::uint32_t byteOffset = 0; bool addIndexOffset = false; + std::uint32_t memoryAccess = 0; }; struct SpirvEmitterState { @@ -97,6 +99,8 @@ struct SpirvEmitterState { // The lookup without fault recording that wide reads try first (see EmitBdaDwordReads); 0 when // every read takes the byte path. std::uint32_t bdaProbeFunction = 0; + std::uint32_t bdaWritePointerFunction = 0; + std::uint32_t bdaNoteWriteFunction = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). bool bdaStopsInvocations = true; // Execution scope of the barriers that keep one guest wave's LDS accesses in program order across diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 4647104f4..467811218 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -31,7 +31,13 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { throw std::runtime_error("buffer operation has invalid memory metadata"); } const auto& memory = program.Resources().memoryInfo.at(memoryIndex); - if (memory.kind == ResourceKind::Buffer) { + requirements.coherentBuffers = requirements.coherentBuffers || (memory.coherent && !memory.gpuDescriptor); + if (memory.gpuDescriptor) { + if (program.Resources().stage != IrShaderStage::Compute) { + throw std::runtime_error("GPU-selected buffer descriptors outside compute shaders are not implemented"); + } + requirements.subgroupLocalInvocationId = true; + } else if (memory.kind == ResourceKind::Buffer) { if (memory.resource >= program.Info().buffers.size()) { throw std::runtime_error("buffer operation has invalid resource metadata"); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index b67516b27..62a7a4289 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -1,6 +1,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include namespace ShaderRecompiler { @@ -17,9 +18,7 @@ void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition) { } // (u64 address, u32 bytes, u32 instruction) -> u64 device address of the range holding all the bytes, -// or 0. With recordFaults the failure is published first; without, the caller retries byte-wise, and -// the fault is recorded there if the bytes are unmapped for real. -std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults) { +std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); const auto boolean = TypeBool(state); @@ -103,7 +102,7 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r fail(binary(spv::OpINotEqual, boolean, BdaLoadWord(state, at(7)), constant(0)), BdaAbi::FaultReason::InvalidTable); fail(binary(spv::OpUGreaterThanEqual, boolean, begin, finish), BdaAbi::FaultReason::InvalidTable); fail(binary(spv::OpUGreaterThan, boolean, end, finish), BdaAbi::FaultReason::Unmapped); - fail(binary(spv::OpIEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(BdaAbi::Read)), constant(0)), BdaAbi::FaultReason::Permission); + fail(binary(spv::OpIEqual, boolean, binary(spv::OpBitwiseAnd, u32, permissions, constant(permission)), constant(0)), BdaAbi::FaultReason::Permission); fail(binary(spv::OpIEqual, boolean, base, BdaConstant(state, 0)), BdaAbi::FaultReason::InvalidTable); const auto offset = binary(spv::OpISub, u64, address, begin); const auto result = binary(spv::OpIAdd, u64, base, offset); @@ -117,10 +116,54 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r } +static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto constant = [&](std::uint32_t value) { return ConstantU32(state, value); }; + const auto binary = [&](std::uint32_t op, std::uint32_t type, std::uint32_t left, std::uint32_t right) { return Binary(state, op, type, left, right); }; + const auto voidType = state.module.Type(spv::OpTypeVoid); + const auto function = state.module.AllocateId(); + state.module.AddName(function, "note_bda_write"); + state.module.AddFunction(spv::OpFunction, voidType, function, spv::FunctionControlMaskNone, state.module.Type(spv::OpTypeFunction, voidType, TypeScalarU64(state))); + const auto address = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, TypeScalarU64(state), address); + EmitLabel(state, state.module.AllocateId()); + const auto done = state.module.AllocateId(); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, boolean), done, spv::StorageClassFunction); + state.module.AddFunction(spv::OpStore, done, ConstantBool(state, false)); + const auto page = binary(spv::OpIAdd, u32, Unary(state, spv::OpUConvert, u32, binary(spv::OpShiftRightLogical, TypeScalarU64(state), address, BdaConstant(state, BdaAbi::WrittenPageShift))), constant(1)); + const auto hash = binary(spv::OpShiftRightLogical, u32, binary(spv::OpIMul, u32, page, constant(0x9e3779b1u)), constant(32u - static_cast(std::countr_zero(BdaAbi::WrittenPageSlots)))); + const auto scope = constant(spv::ScopeDevice); + const auto relaxed = constant(spv::MemorySemanticsMaskNone); + for (std::uint32_t probe = 0; probe < BdaAbi::WrittenPageProbes; probe++) { + const auto pending = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, boolean, pending, done); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, pending), [&] { + const auto slot = binary(spv::OpIAdd, u32, constant(BdaAbi::WrittenSlotsWord), binary(spv::OpBitwiseAnd, u32, binary(spv::OpIAdd, u32, hash, constant(probe)), constant(BdaAbi::WrittenPageSlots - 1u))); + const auto previous = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicCompareExchange, u32, previous, BdaWord(state, state.faultBufferVariable, slot), scope, relaxed, relaxed, page, constant(0u)); + const auto taken = binary(spv::OpLogicalOr, boolean, binary(spv::OpIEqual, boolean, previous, constant(0u)), binary(spv::OpIEqual, boolean, previous, page)); + state.module.AddFunction(spv::OpStore, done, taken); + }); + } + const auto noted = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, boolean, noted, done); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, noted), [&] { + state.module.AddFunction(spv::OpAtomicStore, BdaWord(state, state.faultBufferVariable, constant(BdaAbi::WrittenOverflowWord)), scope, relaxed, constant(1u)); + }); + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); + return function; +} + void DefineGetBdaPointer(SpirvEmitterState& state) { if (!state.program.Info().usesDma) return; state.bdaPointerFunction = DefineBdaLookup(state, "get_bda_pointer", true); if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false); + if (state.program.Info().bdaWrites) { + state.bdaWritePointerFunction = DefineBdaLookup(state, "get_bda_write_pointer", true, BdaAbi::Write); + state.bdaNoteWriteFunction = DefineBdaNoteWrite(state); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 5f5c947ee..c42a895e1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -16,7 +16,13 @@ void EmitBdaOverflowCheck(SpirvEmitterState& state, std::uint32_t address, std:: StopBdaInvocationIf(state, overflow); } -std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::uint32_t instruction) { +std::uint32_t BdaAccessMask(const SpirvEmitterState& state, const IrValue& inst) { + const auto index = inst.Flags().index; + const auto& memory = state.program.Resources().memoryInfo; + return spv::MemoryAccessAlignedMask | (index < memory.size() && memory[index].coherent ? spv::MemoryAccessVolatileMask : 0u); +} + +std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::uint32_t instruction, std::uint32_t accessMask) { const auto byteType = state.module.Type(spv::OpTypeInt, 8u, 0u); const auto bytePointer = TypePointer(state, spv::StorageClassPhysicalStorageBuffer, byteType); const auto physical = state.module.AllocateId(); @@ -26,7 +32,7 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, physical); const auto loaded = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, spv::MemoryAccessAlignedMask, 1u); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, accessMask, 1u); return Unary(state, spv::OpUConvert, TypeU32(state), loaded); } // Unmapped bytes read as zero; the fault is recorded and every invocation reaches the @@ -41,7 +47,7 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, physical); const auto loaded = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, spv::MemoryAccessAlignedMask, 1u); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, accessMask, 1u); const auto widened = Unary(state, spv::OpUConvert, TypeU32(state), loaded); state.module.AddFunction(spv::OpBranch, merge); EmitLabel(state, merge); @@ -50,11 +56,11 @@ std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::ui return value; } -std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction) { +std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, std::uint32_t accessMask) { auto result = ConstantU32(state, 0u); for (std::uint32_t byte = 0; byte < bytes; ++byte) { const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), address, BdaConstant(state, byte)); - const auto value = EmitBdaByte(state, guest, instruction); + const auto value = EmitBdaByte(state, guest, instruction, accessMask); result = Binary(state, spv::OpBitwiseOr, TypeU32(state), result, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), value, ConstantU32(state, byte * 8u))); } return result; @@ -130,7 +136,25 @@ std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std:: if (bits == 32u) return EmitBdaDwordReads(ctx, inst, address, 0u, 1u)[0]; const auto instruction = ConstantU32(state, inst.Flags().pc); EmitBdaOverflowCheck(state, address, bits / 8u, instruction); - return EmitBdaBytes(state, address, bits / 8u, instruction); + return EmitBdaBytes(state, address, bits / 8u, instruction, BdaAccessMask(state, inst)); +} + +void EmitBdaWrite(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t value) { + auto& state = ctx.state; + if (state.bdaWritePointerFunction == 0 || state.bdaNoteWriteFunction == 0) ctx.Fail(inst, "BDA write functions are missing"); + const auto instruction = ConstantU32(state, inst.Flags().pc); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), address, BdaConstant(state, 3u)), BdaConstant(state, 0u)); + EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 4u), instruction, BdaAbi::FaultReason::Unaligned); }); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), [&] { + const auto physical = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, address, ConstantU32(state, 4u), instruction); + EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)), [&] { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, physical); + state.module.AddFunction(spv::OpStore, pointer, value, BdaAccessMask(state, inst), 4u); + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, address); + }); + }); } // One probe of the span from the first to the last extracted dword replaces 4 lookups per dword (a @@ -140,13 +164,13 @@ std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std:: // unaligned mirror or a wrapping span falls back to the byte path. That path repeats, per extracted // dword in order, the address, overflow check and byte lookups of a separate dword load, so every // fault is recorded exactly as one; the probe records none, and a span it accepts cannot fault. -std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords) { +std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword) { auto& state = ctx.state; if (dwords == 0u || dwords > 4u) ctx.Fail(inst, "unsupported BDA read width"); if (state.bdaPointerFunction == 0) ctx.Fail(inst, "BDA lookup function is missing"); const auto u64 = TypeScalarU64(state); const auto instruction = ConstantU32(state, inst.Flags().pc); - const auto used = UsedBdaDwords(inst, dwords); + const auto used = everyDword ? (1u << dwords) - 1u : UsedBdaDwords(inst, dwords); const auto isUsed = [&](std::uint32_t dword) { return (used & (1u << dword)) != 0u; }; std::array values{}; std::uint32_t first = dwords; @@ -167,7 +191,7 @@ std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const if (!isUsed(dword)) continue; if (dword != first) addresses[dword] = dwordAddress(dword); EmitBdaOverflowCheck(state, addresses[dword], 4u, instruction); - into[dword] = EmitBdaBytes(state, addresses[dword], 4u, instruction); + into[dword] = EmitBdaBytes(state, addresses[dword], 4u, instruction, BdaAccessMask(state, inst)); } }; addresses[first] = dwordAddress(first); @@ -194,7 +218,7 @@ std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, element); wideValues[dword] = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), wideValues[dword], pointer, spv::MemoryAccessAlignedMask, 4u); + state.module.AddFunction(spv::OpLoad, TypeU32(state), wideValues[dword], pointer, BdaAccessMask(state, inst), 4u); } state.module.AddFunction(spv::OpBranch, merge); EmitLabel(state, byteLabel); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 215f616e5..8162a4551 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -78,7 +78,7 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { const auto pc = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), pc, state.loopGuardPc); EmitIfCondition(state, Binary(state, spv::OpINotEqual, TypeBool(state), pc, ConstantU32(state, 0u)), [&] { - RecordBdaFault(state, BdaConstant(state, 0u), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit); + RecordBdaFault(state, BdaConstant(state, state.program.Resources().shaderHash), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit); }); } EmitKillIfPixelValidMaskInactive(state); @@ -112,6 +112,13 @@ bool IsLoopMerge(const IrProgram& program, std::uint32_t block) { return false; } +bool IsContinueTarget(const IrProgram& program, std::uint32_t block) { + for (const auto& info : program.Metadata().blockInfo) { + if (info.terminator.loopHeader && info.terminator.continueBlock == block) return true; + } + return false; +} + std::uint32_t EmitBranchCondition(SpirvValueEmitContext& ctx, const BlockInfo& info) { if (ctx.otherHalf == nullptr || info.terminator.condition == BranchCondition::ScalarInstruction || info.terminator.condition == BranchCondition::GotoVariable) { return ctx.Def(info.condition); @@ -489,6 +496,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::ImageQueryDimensions: return Invoke(EmitImageQueryDimensions, ctx, inst); case IrOpcode::ImageQueryLod: return Invoke(EmitImageQueryLod, ctx, inst); case IrOpcode::ImageRead: return Invoke(EmitImageRead, ctx, inst); + case IrOpcode::ImageBvhIntersectRay: return Invoke(EmitImageBvhIntersectRay, ctx, inst); case IrOpcode::ImageWrite: return Invoke(EmitImageWrite, ctx, inst); case IrOpcode::ImageSampleRaw: return Invoke(EmitImageSampleRaw, ctx, inst); case IrOpcode::ImageGatherRaw: return Invoke(EmitImageGatherRaw, ctx, inst); @@ -629,9 +637,12 @@ void EmitControlFlow(SpirvValueEmitContext& context, StructuredFunctionState& fu if (info == nullptr) { context.Fail("structured control flow block has no terminator metadata"); } + const bool stops = state.bdaStopsInvocations; + state.bdaStopsInvocations = stops && !IsContinueTarget(program, info->id); EmitStructuredBlock(context, functionState, block); functionState.blockExitLabels.emplace(block, state.currentLabel); EmitStructuredTerminator(context, program, *info); + state.bdaStopsInvocations = stops; } PatchStructuredPhis(context, functionState); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 356ee327e..d02d65c3b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1,8 +1,10 @@ #include "SpirvBackend/SpirvImageEmitter.hpp" +#include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" +#include #include #include #include @@ -10,6 +12,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -132,6 +135,9 @@ ImageSampleLayout Layout(const MemoryInfo& mem, RdnaImageDimension dimension) { if (HasFlag(mem, RdnaImageSampleFlagLod)) { layout.lod = cursor++; } + if (HasFlag(mem, RdnaImageSampleFlagLodClamp)) { + layout.clamp = cursor++; + } return layout; } @@ -250,6 +256,9 @@ std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac valueClass = IrTextureNumericClass::Float; } const bool integer = valueClass == IrTextureNumericClass::Uint || valueClass == IrTextureNumericClass::Sint; + if (mem.dataBits == 16u && access.image.depthBits) { + ctx.Fail(access.inst, "reads the bits of a depth plane as 16-bit results"); + } if (mem.dataBits == 16u) { if (mem.dataDwords > 4u) { ctx.Fail(access.inst, "has more than four packed image result dwords"); @@ -292,6 +301,11 @@ std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac component[index] = index == 0u ? F32BitsToU32(ctx, value) : ConstantU32(state, 0); continue; } + const auto selector = (access.image.shaderSwizzle >> (index * 3u)) & 7u; + if (access.image.depthBits && !gather && selector != 4u) { + component[index] = ConstantU32(state, selector == 1u || selector == 7u ? 1u : 0u); + continue; + } const auto scalar = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, ImageScalarType(state, valueClass), scalar, value, index); component[index] = SampledComponentBits(ctx, scalar, valueClass); @@ -647,6 +661,9 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con auto& state = ctx.state; const auto& mem = access.mem; const auto dimension = access.image.dimension; + if (setup.layout.clamp != NoImageComponent) { + ctx.Fail(access.inst, "is a gather with an LOD clamp, which is not implemented"); + } if (dimension == RdnaImageDimension::Dim1D) { if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagOffset) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); @@ -765,6 +782,19 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con operandMask |= spv::ImageOperandsBiasMask; operands.push_back(AddressF32(ctx, access, setup.layout.bias)); } + if (setup.layout.clamp != NoImageComponent) { + const auto& capabilities = state.requirements.capabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityMinLod)) == capabilities.end()) ctx.Fail(access.inst, "clamps its LOD, which needs the device's shaderResourceMinLod"); + const auto clamp = AddressF32(ctx, access, setup.layout.clamp); + if ((operandMask & spv::ImageOperandsLodMask) != 0u) { + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMax, operands.back(), clamp); + operands.back() = clamped; + } else { + operandMask |= spv::ImageOperandsMinLodMask; + operands.push_back(clamp); + } + } const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler, access.slot); const auto sample = state.module.AllocateId(); std::vector words = {opcode, resultType, sample, sampled, setup.coord}; @@ -924,4 +954,223 @@ void EmitImageAtomicXor32(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitImage(ctx, inst); } +void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto f32 = TypeF32(state); + const auto boolean = TypeBool(state); + const auto result4 = TypeU32Composite(state, 4u); + const auto invalid = ConstantU32(state, 0xffffffffu); + const auto uint = [&](std::uint32_t value) { return ConstantU32(state, value); }; + const auto real = [&](std::uint32_t bits) { return ConstantF32(state, bits); }; + const auto op = [&](spv::Op opcode, std::uint32_t type, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, type, lhs, rhs); }; + const auto compose = [&](const std::array& values) { + const auto id = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, result4, id, values[0], values[1], values[2], values[3]); + return id; + }; + const auto asFloat = [&](std::uint32_t value) { return Unary(state, spv::OpBitcast, f32, value); }; + const auto asUint = [&](std::uint32_t value) { return Unary(state, spv::OpBitcast, u32, value); }; + const auto selectF = [&](std::uint32_t condition, std::uint32_t whenTrue, std::uint32_t whenFalse) { return Select(state, f32, condition, whenTrue, whenFalse); }; + const auto selectU = [&](std::uint32_t condition, std::uint32_t whenTrue, std::uint32_t whenFalse) { return Select(state, u32, condition, whenTrue, whenFalse); }; + const auto both = [&](std::uint32_t lhs, std::uint32_t rhs) { return op(spv::OpLogicalAnd, boolean, lhs, rhs); }; + const auto either = [&](std::uint32_t lhs, std::uint32_t rhs) { return op(spv::OpLogicalOr, boolean, lhs, rhs); }; + const auto isNan = [&](std::uint32_t value) { return Unary(state, spv::OpIsNan, boolean, value); }; + const auto widen = [&](std::uint32_t value) { return Unary(state, spv::OpUConvert, u64, value); }; + const auto pair = [&](std::uint32_t low, std::uint32_t high) { return op(spv::OpBitwiseOr, u64, widen(low), op(spv::OpShiftLeftLogical, u64, widen(high), BdaConstant(state, 32u))); }; + + const auto query = [&] { + const auto descriptor = ctx.Arg(inst, 0); + std::array word{}; + for (std::uint32_t i = 0; i < 4u; ++i) { + word[i] = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, word[i], descriptor, i); + } + const IrValue* address = ctx.ImageAddress(inst.Argument(1)); + const auto component = [&](std::uint32_t index) { return ctx.Arg(*address, index); }; + const bool a16 = (ctx.Memory(inst).imageSampleFlags & RdnaImageSampleFlagA16) != 0u; + + const auto node = component(0); + const auto type = op(spv::OpBitwiseAnd, u32, node, uint(7u)); + const auto nodeIndex = op(spv::OpShiftRightLogical, u64, widen(node), BdaConstant(state, 3u)); + const auto baseUnits = pair(word[0], op(spv::OpBitwiseAnd, u32, word[1], uint(0xffu))); + const auto lastNode = pair(word[2], op(spv::OpBitwiseAnd, u32, word[3], uint(0x3ffu))); + const auto boxGrow = op(spv::OpBitwiseAnd, u32, op(spv::OpShiftRightLogical, u32, word[1], uint(23u)), uint(0xffu)); + const auto boxSort = op(spv::OpINotEqual, boolean, op(spv::OpBitwiseAnd, u32, word[1], uint(0x80000000u)), uint(0u)); + const auto barycentrics = op(spv::OpINotEqual, boolean, op(spv::OpBitwiseAnd, u32, word[3], uint(1u << 24u)), uint(0u)); + const auto nodeAddress = op(spv::OpIAdd, u64, op(spv::OpShiftLeftLogical, u64, baseUnits, BdaConstant(state, 8u)), op(spv::OpShiftLeftLogical, u64, widen(op(spv::OpBitwiseAnd, u32, node, uint(~7u))), BdaConstant(state, 3u))); + const auto present = op(spv::OpINotEqual, boolean, baseUnits, BdaConstant(state, 0u)); + const auto valid = both(present, op(spv::OpULessThanEqual, boolean, nodeIndex, lastNode)); + const auto isTriangle = both(valid, op(spv::OpULessThanEqual, boolean, type, uint(1u))); + const auto isBox16 = both(valid, op(spv::OpIEqual, boolean, type, uint(4u))); + const auto isBox32 = both(both(present, op(spv::OpULessThan, boolean, nodeIndex, lastNode)), op(spv::OpIEqual, boolean, type, uint(5u))); + + const auto extent = asFloat(component(1)); + std::array origin{}; + std::array direction{}; + std::array inverse{}; + for (std::uint32_t axis = 0; axis < 3u; ++axis) { + origin[axis] = asFloat(component(2u + axis)); + if (!a16) { + direction[axis] = asFloat(component(5u + axis)); + inverse[axis] = asFloat(component(8u + axis)); + continue; + } + const auto half = [&](std::uint32_t index) { + const auto packed = component(5u + index / 2u); + return EmitF16BitsToF32(state, index % 2u == 0u ? op(spv::OpBitwiseAnd, u32, packed, uint(0xffffu)) : op(spv::OpShiftRightLogical, u32, packed, uint(16u))); + }; + direction[axis] = half(axis); + inverse[axis] = half(3u + axis); + } + + const auto nodeDwords = [&](std::uint32_t count) { + std::vector dwords; + if (state.bdaProbeFunction == 0) { + for (std::uint32_t first = 0; first < count; first += 4u) { + const auto read = EmitBdaDwordReads(ctx, inst, nodeAddress, first * 4u, 4u, true); + dwords.insert(dwords.end(), read.begin(), read.end()); + } + return dwords; + } + const auto pc = uint(inst.Flags().pc); + const auto bytes = uint(count * 4u); + const auto physical = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, u64, physical, state.bdaProbeFunction, nodeAddress, bytes, pc); + const auto mapped = both(op(spv::OpINotEqual, boolean, physical, BdaConstant(state, 0u)), op(spv::OpIEqual, boolean, op(spv::OpBitwiseAnd, u64, physical, BdaConstant(state, 3u)), BdaConstant(state, 0u))); + EmitIfCondition(state, Unary(state, spv::OpLogicalNot, boolean, mapped), [&] { RecordBdaFault(state, nodeAddress, bytes, pc, BdaAbi::FaultReason::Unmapped); }); + for (std::uint32_t first = 0; first < count; first += 4u) { + const auto quad = EmitValueOrDefaultIfCondition(state, mapped, result4, ConstantU32CompositeZero(state, 4u), [&] { + std::array values{}; + for (std::uint32_t i = 0; i < 4u; ++i) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, op(spv::OpIAdd, u64, physical, BdaConstant(state, (first + i) * 4u))); + values[i] = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, values[i], pointer, spv::MemoryAccessAlignedMask, 4u); + } + return compose(values); + }); + for (std::uint32_t i = 0; i < 4u; ++i) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, value, quad, i); + dwords.push_back(value); + } + } + return dwords; + }; + const auto sub = [&](const std::array& a, const std::array& b) { + return std::array{op(spv::OpFSub, f32, a[0], b[0]), op(spv::OpFSub, f32, a[1], b[1]), op(spv::OpFSub, f32, a[2], b[2])}; + }; + const auto cross = [&](const std::array& a, const std::array& b) { + const auto term = [&](std::uint32_t i, std::uint32_t j) { return op(spv::OpFSub, f32, op(spv::OpFMul, f32, a[i], b[j]), op(spv::OpFMul, f32, a[j], b[i])); }; + return std::array{term(1, 2), term(2, 0), term(0, 1)}; + }; + const auto dot = [&](const std::array& a, const std::array& b) { + return op(spv::OpFAdd, f32, op(spv::OpFAdd, f32, op(spv::OpFMul, f32, a[0], b[0]), op(spv::OpFMul, f32, a[1], b[1])), op(spv::OpFMul, f32, a[2], b[2])); + }; + + const auto triangle = [&] { + const auto d = nodeDwords(16u); + const auto second = op(spv::OpIEqual, boolean, type, uint(1u)); + std::array v1{}; + std::array v2{}; + std::array v3{}; + for (std::uint32_t axis = 0; axis < 3u; ++axis) { + v1[axis] = selectF(second, asFloat(d[3u + axis]), asFloat(d[axis])); + v2[axis] = selectF(second, asFloat(d[9u + axis]), asFloat(d[3u + axis])); + v3[axis] = asFloat(d[6u + axis]); + } + const auto e1 = sub(v2, v1); + const auto e2 = sub(v3, v1); + const auto e3 = sub(origin, v1); + const auto s1 = cross(direction, e2); + const auto s2 = cross(e3, e1); + auto tNum = dot(e2, s2); + auto tDenom = dot(s1, e1); + const auto iNum = dot(e3, s1); + const auto jNum = dot(direction, s2); + const auto t = op(spv::OpFDiv, f32, tNum, tDenom); + const auto u = op(spv::OpFDiv, f32, iNum, tDenom); + const auto v = op(spv::OpFDiv, f32, jNum, tDenom); + const auto zero = real(0u); + const auto one = real(0x3f800000u); + const auto missed = either(either(either(op(spv::OpFOrdLessThan, boolean, u, zero), op(spv::OpFOrdGreaterThan, boolean, u, one)), either(op(spv::OpFOrdLessThan, boolean, v, zero), op(spv::OpFOrdGreaterThan, boolean, op(spv::OpFAdd, f32, u, v), one))), op(spv::OpFOrdLessThan, boolean, t, zero)); + tNum = selectF(missed, real(0x7f800000u), tNum); + tDenom = selectF(missed, one, tDenom); + const auto triangleId = d[15]; + const auto shift = op(spv::OpShiftLeftLogical, u32, type, uint(3u)); + const auto barycentric = [&](std::uint32_t sourceShift) { + const auto source = op(spv::OpBitwiseAnd, u32, op(spv::OpShiftRightLogical, u32, triangleId, op(spv::OpIAdd, u32, shift, uint(sourceShift))), uint(3u)); + const auto rest = op(spv::OpFSub, f32, op(spv::OpFSub, f32, tDenom, iNum), jNum); + return selectF(op(spv::OpIEqual, boolean, source, uint(1u)), iNum, selectF(op(spv::OpIEqual, boolean, source, uint(2u)), jNum, rest)); + }; + const auto hit = Unary(state, spv::OpLogicalNot, boolean, missed); + return compose({asUint(tNum), asUint(tDenom), selectU(barycentrics, asUint(barycentric(0u)), triangleId), selectU(barycentrics, asUint(barycentric(2u)), selectU(hit, uint(1u), uint(0u)))}); + }; + + const auto boxes = [&](bool fp16) { + const auto d = nodeDwords(fp16 ? 16u : 28u); + const auto bound = [&](std::uint32_t child, std::uint32_t index) { + const auto position = child * 6u + index; + if (!fp16) { + return asFloat(d[4u + position]); + } + const auto packed = d[4u + position / 2u]; + return EmitF16BitsToF32(state, position % 2u == 0u ? op(spv::OpBitwiseAnd, u32, packed, uint(0xffffu)) : op(spv::OpShiftRightLogical, u32, packed, uint(16u))); + }; + const auto nanMax = [&](std::uint32_t a, std::uint32_t b) { return selectF(either(isNan(a), op(spv::OpFOrdGreaterThan, boolean, a, b)), a, b); }; + const auto nanMin = [&](std::uint32_t a, std::uint32_t b) { return selectF(either(isNan(a), op(spv::OpFOrdLessThan, boolean, a, b)), a, b); }; + const auto zero = real(0u); + const auto growFactor = op(spv::OpFAdd, f32, real(0x3f800000u), op(spv::OpFMul, f32, Unary(state, spv::OpConvertUToF, f32, boxGrow), real(0x33800000u))); + std::array children{}; + std::array keys{}; + for (std::uint32_t child = 0; child < 4u; ++child) { + std::uint32_t enter = 0; + std::uint32_t leave = 0; + for (std::uint32_t axis = 0; axis < 3u; ++axis) { + const auto planeMin = op(spv::OpFMul, f32, op(spv::OpFSub, f32, bound(child, axis), origin[axis]), inverse[axis]); + const auto planeMax = op(spv::OpFMul, f32, op(spv::OpFSub, f32, bound(child, axis + 3u), origin[axis]), inverse[axis]); + const auto positive = op(spv::OpFOrdGreaterThanEqual, boolean, inverse[axis], zero); + const auto near = selectF(positive, planeMin, planeMax); + const auto far = selectF(positive, planeMax, planeMin); + enter = axis == 0u ? near : nanMax(enter, near); + leave = axis == 0u ? far : nanMin(leave, far); + } + const auto nan = either(isNan(enter), isNan(leave)); + const auto minT = selectF(nan, real(0x7f800000u), selectF(op(spv::OpFOrdGreaterThan, boolean, enter, zero), enter, zero)); + const auto maxT = selectF(nan, real(0xff800000u), selectF(op(spv::OpFOrdLessThan, boolean, leave, extent), leave, extent)); + const auto hit = op(spv::OpFOrdLessThanEqual, boolean, minT, op(spv::OpFMul, f32, maxT, growFactor)); + children[child] = selectU(hit, d[child], invalid); + keys[child] = minT; + } + auto sorted = children; + for (const auto [a, b] : std::array, 5>{{{0, 2}, {1, 3}, {0, 1}, {2, 3}, {1, 2}}}) { + const auto bValid = op(spv::OpINotEqual, boolean, sorted[b], invalid); + const auto aInvalid = op(spv::OpIEqual, boolean, sorted[a], invalid); + const auto swap = either(both(bValid, op(spv::OpFOrdLessThan, boolean, keys[b], keys[a])), aInvalid); + const auto sortedA = selectU(swap, sorted[b], sorted[a]); + const auto sortedB = selectU(swap, sorted[a], sorted[b]); + const auto keyA = selectF(swap, keys[b], keys[a]); + const auto keyB = selectF(swap, keys[a], keys[b]); + sorted[a] = sortedA; + sorted[b] = sortedB; + keys[a] = keyA; + keys[b] = keyB; + } + for (std::uint32_t child = 0; child < 4u; ++child) { + children[child] = selectU(boxSort, sorted[child], children[child]); + } + return compose(children); + }; + + const auto none = compose({invalid, invalid, invalid, invalid}); + const auto tested = EmitValueOrDefaultIfCondition(state, isTriangle, result4, none, triangle); + const auto box16 = EmitValueOrDefaultIfCondition(state, isBox16, result4, tested, [&] { return boxes(true); }); + return EmitValueOrDefaultIfCondition(state, isBox32, result4, box16, [&] { return boxes(false); }); + }; + const auto inactive = compose({invalid, invalid, invalid, invalid}); + ctx.Define(inst, EmitValueOrDefaultIfCondition(state, ctx.Arg(inst, 2), result4, inactive, query)); +} + } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 4954e162c..1292d8e47 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -109,8 +109,12 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(SpirvEmitterState& state access.objectPointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, pointerType, access.objectPointer, variable, ConstantU32(state, arrayIndex)); access.byteOffset = state.memoryByteOffsets.at(arrayIndex); + access.memoryAccess = mem.coherent ? spv::MemoryAccessVolatileMask : 0u; access.length = state.module.AllocateId(); state.module.AddFunction(spv::OpArrayLength, TypeU32(state), access.length, access.objectPointer, 0u); + if (mem.resource < state.program.Info().buffers.size() && state.program.Info().buffers[mem.resource].empty) { + access.length = ConstantU32(state, 0u); + } return access; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 926b7ec1a..110176243 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -356,6 +356,10 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationAliased); state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationAliased); } + if (state.requirements.coherentBuffers) { + state.module.AddAnnotation(spv::OpDecorate, state.storageBufferVariable, spv::DecorationCoherent); + if (state.storageBufferU64Variable != 0u) state.module.AddAnnotation(spv::OpDecorate, state.storageBufferU64Variable, spv::DecorationCoherent); + } break; case DescriptorBindingKind::BdaPagetable: state.bdaPagetableVariable = Define(StorageBufferBlockType(state), "bda_pagetable"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 59ac11712..1a9c0cf82 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -110,7 +110,8 @@ std::uint32_t LoadWordInBounds(SpirvValueEmitContext& ctx, const MemoryResourceA auto& state = ctx.state; const auto pointer = EmitMemoryElementPointer(state, resource, index); const auto value = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + if (resource.memoryAccess != 0u) state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer, resource.memoryAccess); + else state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); return value; } @@ -172,6 +173,84 @@ std::uint32_t DeviceAddressFromWords(SpirvEmitterState& state, std::uint32_t low return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low64, high64); } +std::pair RuntimeBufferByteAddress(SpirvEmitterState& state, std::uint32_t index, std::uint32_t offset, std::uint32_t soffset, std::uint32_t immediate, std::uint32_t stride, std::uint32_t swizzle, std::uint32_t indexStride) { + const auto u32 = TypeU32(state); + const auto add = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpIAdd, u32, lhs, rhs); }; + const auto mul = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpIMul, u32, lhs, rhs); }; + if (immediate != 0u) offset = add(offset, ConstantU32(state, immediate)); + const auto linear = add(mul(index, stride), offset); + const auto indexShift = add(indexStride, ConstantU32(state, 3u)); + const auto indices = Binary(state, spv::OpShiftLeftLogical, u32, ConstantU32(state, 1u), indexShift); + const auto indexMsb = Binary(state, spv::OpShiftRightLogical, u32, index, indexShift); + const auto indexLsb = Binary(state, spv::OpBitwiseAnd, u32, index, Binary(state, spv::OpISub, u32, indices, ConstantU32(state, 1u))); + const auto offsetMsb = Binary(state, spv::OpBitwiseAnd, u32, offset, ConstantU32(state, ~3u)); + const auto offsetLsb = Binary(state, spv::OpBitwiseAnd, u32, offset, ConstantU32(state, 3u)); + const auto swizzled = add(mul(add(mul(indexMsb, stride), offsetMsb), indices), add(Binary(state, spv::OpShiftLeftLogical, u32, indexLsb, ConstantU32(state, 2u)), offsetLsb)); + return {offset, add(Select(state, u32, swizzle, swizzled, linear), soffset)}; +} + +template +void ForEachGpuDescriptorDword(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components, TFunction&& function) { + auto& state = ctx.state; + const IrValue* handle = inst.Argument(0)->Resolve(); + if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { + ctx.Fail(inst, "has no GPU-selected V#"); + } + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto word1 = ctx.Arg(*handle, 1); + const auto records = ctx.Arg(*handle, 2); + const auto word3 = ctx.Arg(*handle, 3); + const auto field = [&](std::uint32_t word, std::uint32_t first, std::uint32_t count) { return EmitBitFieldUExtract(state, word, ConstantU32(state, first), ConstantU32(state, count)); }; + const auto nonzero = [&](std::uint32_t value) { return Binary(state, spv::OpINotEqual, boolean, value, ConstantU32(state, 0u)); }; + const auto hasDword = [&](std::uint32_t offset, std::uint32_t size) { + return AndCondition(state, Binary(state, spv::OpUGreaterThanEqual, boolean, size, ConstantU32(state, 4u)), Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u32, size, ConstantU32(state, 4u)))); + }; + const auto stride = field(word1, 16u, 14u); + const auto swizzle = AndCondition(state, nonzero(stride), nonzero(field(word1, 31u, 1u))); + const auto lane = Binary(state, spv::OpBitwiseAnd, u32, EmitSubgroupLocalInvocationId(state), ConstantU32(state, 63u)); + const auto index = Binary(state, spv::OpIAdd, u32, ctx.Arg(inst, 1), Select(state, u32, nonzero(field(word3, 23u, 1u)), lane, ConstantU32(state, 0u))); + const auto soffset = ctx.Arg(inst, 3); + const auto base = DeviceAddressFromWords(state, ctx.Arg(*handle, 0), field(word1, 0u, 16u)); + const auto mode = field(word3, 28u, 2u); + const auto indexInBounds = Binary(state, spv::OpULessThan, boolean, index, records); + const auto scalarInBounds = Binary(state, spv::OpULessThanEqual, boolean, soffset, records); + const auto rawRecords = Binary(state, spv::OpISub, u32, records, soffset); + const auto rawIndexInBounds = Binary(state, spv::OpULessThan, boolean, index, rawRecords); + for (std::uint32_t component = 0; component < components; component++) { + const auto [offset, byte] = RuntimeBufferByteAddress(state, index, ctx.Arg(inst, 2), soffset, mem.offset + component * 4u, stride, swizzle, field(word3, 21u, 2u)); + const auto structured = AndCondition(state, indexInBounds, Binary(state, spv::OpULessThan, boolean, offset, stride)); + const auto raw = AndCondition(state, scalarInBounds, Select(state, boolean, swizzle, AndCondition(state, rawIndexInBounds, hasDword(offset, stride)), hasDword(offset, rawRecords))); + auto inBounds = Select(state, boolean, Binary(state, spv::OpIEqual, boolean, mode, ConstantU32(state, 0u)), structured, indexInBounds); + inBounds = Select(state, boolean, Binary(state, spv::OpULessThan, boolean, mode, ConstantU32(state, 2u)), inBounds, Select(state, boolean, Binary(state, spv::OpIEqual, boolean, mode, ConstantU32(state, 2u)), nonzero(records), raw)); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), base, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte)); + function(component, guest, AndCondition(state, nonzero(field(word3, 12u, 7u)), inBounds)); + } +} + +std::uint32_t ConstructU32Composite(SpirvEmitterState& state, std::uint32_t components, const std::array& values); + +std::uint32_t LoadGpuDescriptor(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t components) { + std::array values{}; + ForEachGpuDescriptorDword(ctx, inst, ctx.Memory(inst), components, [&](std::uint32_t component, std::uint32_t guest, std::uint32_t inBounds) { + values[component] = EmitValueOrZeroIfCondition(ctx.state, inBounds, [&] { return EmitBdaRead(ctx, inst, guest, 32u); }); + }); + return components == 1u ? values[0] : ConstructU32Composite(ctx.state, components, values); +} + +void StoreGpuDescriptor(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t components) { + auto& state = ctx.state; + const auto data = ctx.Arg(inst, inst.ArgumentCount() - 2u); + ForEachGpuDescriptorDword(ctx, inst, ctx.Memory(inst), components, [&](std::uint32_t component, std::uint32_t guest, std::uint32_t inBounds) { + auto value = data; + if (components != 1u) { + value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, data, component); + } + EmitIfCondition(state, inBounds, [&] { EmitBdaWrite(ctx, inst, guest, value); }); + }); +} + std::uint32_t GuestAddressBase(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { auto& state = ctx.state; auto low = ctx.Arg(inst, 1); @@ -289,7 +368,8 @@ std::uint32_t FormattedLoad(SpirvValueEmitContext& ctx, const IrValue& inst, con void StoreWordInBounds(SpirvValueEmitContext& ctx, const MemoryResourceAccess& resource, std::uint32_t index, std::uint32_t data) { auto& state = ctx.state; - state.module.AddFunction(spv::OpStore, EmitMemoryElementPointer(state, resource, index), data); + if (resource.memoryAccess != 0u) state.module.AddFunction(spv::OpStore, EmitMemoryElementPointer(state, resource, index), data, resource.memoryAccess); + else state.module.AddFunction(spv::OpStore, EmitMemoryElementPointer(state, resource, index), data); } void StoreSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryInfo& mem, const MemoryResourceAccess& resource, std::uint32_t address, std::uint32_t index, std::uint32_t bits, std::uint32_t data) { @@ -307,8 +387,13 @@ void StoreSubwordInBounds(SpirvValueEmitContext& ctx, const MemoryInfo& mem, con }; if (mem.kind == ResourceKind::Scratch) { const auto old = state.module.AllocateId(); - state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); - state.module.AddFunction(spv::OpStore, pointer, merge(old)); + if (resource.memoryAccess != 0u) { + state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer, resource.memoryAccess); + state.module.AddFunction(spv::OpStore, pointer, merge(old), resource.memoryAccess); + } else { + state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); + state.module.AddFunction(spv::OpStore, pointer, merge(old)); + } } else { AtomicUpdate(state, pointer, mem.kind, merge); } @@ -563,9 +648,27 @@ const MemoryInfo& BufferMemory(SpirvValueEmitContext& ctx, const IrValue& inst) if (mem.kind != ResourceKind::Buffer) { ctx.Fail(inst, "must access a buffer resource"); } + if (mem.gpuDescriptor) { + ctx.Fail(inst, "accesses a GPU-selected V# in a way only raw dword loads and stores support"); + } return mem; } +bool EmitGpuDescriptorAccess(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t components, bool store) { + if (!ctx.Memory(inst).gpuDescriptor) { + return false; + } + auto& state = ctx.state; + if (store) { + EmitIfCondition(state, ActiveArgument(ctx, inst), [&] { StoreGpuDescriptor(ctx, inst, components); }); + return true; + } + const auto type = components == 1u ? TypeU32(state) : TypeU32Composite(state, components); + const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); + ctx.Define(inst, EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), type, zero, [&] { return LoadGpuDescriptor(ctx, inst, components); })); + return true; +} + const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto& mem = ctx.Memory(inst); if (mem.kind != ResourceKind::Lds && mem.kind != ResourceKind::Gds) { @@ -928,6 +1031,23 @@ void EmitReadConstBuffer(SpirvValueEmitContext& ctx, const IrValue& inst) { } auto& state = ctx.state; const auto address = Binary(state, spv::OpIAdd, TypeU32(state), ctx.Arg(inst, 1), ConstantU32(state, mem.offset)); + if (mem.gpuDescriptor) { + const IrValue* handle = inst.Argument(0)->Resolve(); + if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) { + ctx.Fail(inst, "has no GPU-selected V#"); + } + const auto u32 = TypeU32(state); + const auto word1 = ctx.Arg(*handle, 1); + const auto records = ctx.Arg(*handle, 2); + const auto stride = EmitBitFieldUExtract(state, word1, ConstantU32(state, 16u), ConstantU32(state, 14u)); + const auto size = Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), stride, ConstantU32(state, 0u)), records, Binary(state, spv::OpIMul, u32, stride, records)); + const auto byte = Binary(state, spv::OpBitwiseAnd, u32, address, ConstantU32(state, ~3u)); + const auto inBounds = Binary(state, spv::OpULessThan, TypeBool(state), byte, size); + const auto base = DeviceAddressFromWords(state, ctx.Arg(*handle, 0), EmitBitFieldUExtract(state, word1, ConstantU32(state, 0u), ConstantU32(state, 16u))); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), base, Unary(state, spv::OpUConvert, TypeScalarU64(state), byte)); + ctx.Define(inst, EmitValueOrZeroIfCondition(state, inBounds, [&] { return EmitBdaRead(ctx, inst, guest, 32u); })); + return; + } const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); const auto access = PrepareMemoryResourceAccess(state, mem); const auto element = EmitMemoryElementIndex(state, access, rawIndex); @@ -1000,19 +1120,23 @@ void EmitLoadBufferU16(SpirvValueEmitContext& ctx, const IrValue& inst) { } void EmitLoadBufferU32(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 1u, false)) return; const auto& mem = BufferMemory(ctx, inst); ctx.Define(inst, mem.formatted ? FormattedLoad(ctx, inst, mem) : LoadWord(ctx, inst, mem)); } void EmitLoadBufferU32x2(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 2u, false)) return; ctx.Define(inst, LoadWideBuffer(ctx, inst, BufferMemory(ctx, inst), 2u)); } void EmitLoadBufferU32x3(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 3u, false)) return; ctx.Define(inst, LoadWideBuffer(ctx, inst, BufferMemory(ctx, inst), 3u)); } void EmitLoadBufferU32x4(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 4u, false)) return; ctx.Define(inst, LoadWideBuffer(ctx, inst, BufferMemory(ctx, inst), 4u)); } @@ -1025,6 +1149,7 @@ void EmitStoreBufferU16(SpirvValueEmitContext& ctx, const IrValue& inst) { } void EmitStoreBufferU32(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 1u, true)) return; const auto& mem = BufferMemory(ctx, inst); if (mem.formatted) { FormattedStore(ctx, inst, mem); @@ -1034,14 +1159,17 @@ void EmitStoreBufferU32(SpirvValueEmitContext& ctx, const IrValue& inst) { } void EmitStoreBufferU32x2(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 2u, true)) return; StoreWideBuffer(ctx, inst, BufferMemory(ctx, inst), 2u); } void EmitStoreBufferU32x3(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 3u, true)) return; StoreWideBuffer(ctx, inst, BufferMemory(ctx, inst), 3u); } void EmitStoreBufferU32x4(SpirvValueEmitContext& ctx, const IrValue& inst) { + if (EmitGpuDescriptorAccess(ctx, inst, 4u, true)) return; StoreWideBuffer(ctx, inst, BufferMemory(ctx, inst), 4u); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index fae0a76bd..f82cfba1a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -39,6 +39,9 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: if (kind == StageInputKind::LocalInvocationIndex) { return EmitLocalInvocationIndex(state); } + if (kind == StageInputKind::DispatchThreadLimit) { + return EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.DispatchThreadLimitDword() + component); + } if (state.laneCount == 2 && (kind == StageInputKind::LocalInvocationId || kind == StageInputKind::GlobalInvocationId)) { const auto* workgroup = ShaderWorkgroupInputFor(state); if (workgroup == nullptr) { @@ -193,7 +196,8 @@ std::uint32_t ExportRawComponent(SpirvValueEmitContext& ctx, std::uint32_t vecto std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ExportInfo& exp, bool uintOutput) { auto& state = ctx.state; if (exp.compr && !uintOutput) { - const auto unpack = MrtOutputMode(state, exp) == 5u ? GLSLstd450UnpackUnorm2x16 : GLSLstd450UnpackHalf2x16; + const auto mode = MrtOutputMode(state, exp); + const auto unpack = mode == 5u ? GLSLstd450UnpackUnorm2x16 : mode == 6u ? GLSLstd450UnpackSnorm2x16 : GLSLstd450UnpackHalf2x16; std::array f32 {ConstantF32(state, 0u), ConstantF32(state, 0u), ConstantF32(state, 0u), ConstantF32(state, 0x3f800000u)}; for (std::uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { @@ -221,6 +225,8 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const ConstantU32(state, 0u), ConstantU32(state, uintOutput ? 1u : 0x3f800000u), }; + const auto mode = MrtOutputMode(state, exp); + const auto exported = exp.en & (mode == 1u ? 0x1u : mode == 2u ? 0x3u : mode == 3u ? 0x9u : 0xfu); if (exp.compr) { for (std::uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { @@ -238,7 +244,7 @@ std::uint32_t ExportVector(SpirvValueEmitContext& ctx, std::uint32_t data, const } } else { for (std::uint32_t component = 0; component < 4u; component++) { - if (((exp.en >> component) & 1u) != 0u) { + if (((exported >> component) & 1u) != 0u) { raw.at(component) = ExportRawComponent(ctx, data, component); } } diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index eb9c775ac..b69cded08 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -30,6 +30,7 @@ private: const RdnaOperand& sourceAt(const RdnaInstruction& inst, std::uint32_t index); RdnaOperand destinationOperand(const RdnaInstruction& inst); + RdnaOperand accumulatorOperand(const RdnaInstruction& inst); RdnaOperand offsetOperand(const RdnaOperand& operand, std::uint32_t offset); RdnaOperand scalarDestinationOperand(const RdnaOperand& operand, std::uint32_t offset); RdnaOperand plainOperand(const RdnaOperand& operand); diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 7835f97a0..c7a76848b 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -77,7 +77,7 @@ void TranslationContext::addU32(const RdnaInstruction& inst, bool vector, bool u ir.SetScc(carryOut.Value()); return; } - const IrU1 carryIn = vector ? IrU1(ir.GetVcc()) : IrU1(ir.GetScc()); + const IrU1 carryIn = !vector ? IrU1(ir.GetScc()) : inst.sourceCount >= 3u ? readMask(sourceAt(inst, 2u)) : IrU1(ir.GetVcc()); const IrU32 carryInU32(ir.Select(carryIn.Value(), ir.Constant(1u), ir.Constant(0u))); IrValue& secondAdd = ir.Emit(IrOpcode::IAddCarry32, IrType::U32x2, {&sum.Value(), &carryInU32.Value()}); const IrU32 result(ir.Emit(IrOpcode::CompositeExtractU32x2, IrType::U32, {&secondAdd, &ir.Constant(0u)})); @@ -111,7 +111,7 @@ void TranslationContext::subbU32(const RdnaInstruction& inst, bool vector, bool const IrU32 second = readU32(sourceAt(inst, 1u)); const IrU32& lhs = reverse ? second : first; const IrU32& rhs = reverse ? first : second; - const IrU1 borrowIn = vector ? IrU1(ir.GetVcc()) : IrU1(ir.GetScc()); + const IrU1 borrowIn = !vector ? IrU1(ir.GetScc()) : inst.sourceCount >= 3u ? readMask(sourceAt(inst, 2u)) : IrU1(ir.GetVcc()); const IrU32 borrowInU32(ir.Select(borrowIn.Value(), ir.Constant(1u), ir.Constant(0u))); const IrU32 partial(ir.ISub(lhs.Value(), rhs.Value())); const IrU1 firstBorrow(ir.ULessThan(lhs.Value(), rhs.Value())); diff --git a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp index 66a4ecbb0..6bfc43eca 100644 --- a/core/shader/recompiler/Translation/src/DispatchInstructions.cpp +++ b/core/shader/recompiler/Translation/src/DispatchInstructions.cpp @@ -12,7 +12,9 @@ void DispatchInstruction(IrBuilder& builder, const RdnaInstruction& instruction, throw std::runtime_error("DispatchInstruction not implemented"); } -void TranslationContext::TranslateInstruction(const RdnaInstruction& instruction) { +void TranslationContext::TranslateInstruction(const RdnaInstruction& decoded) { + RdnaInstruction instruction = decoded; + instruction.destination = destinationOperand(decoded); currentOpcode = instruction.op; currentProgramCounter = instruction.programCounter; if (instruction.op == RdnaOpcode::Unknown || instruction.op == RdnaOpcode::Count) { diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index b3a9b4677..64ca4ff4e 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -100,7 +100,7 @@ bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opc bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix) { std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { - const RdnaOperand& operand = accumulator && index == 2u ? inst.destination : sourceAt(inst, index); + const RdnaOperand& operand = accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index); args[index] = mix ? &readMixF32(operand).Value() : &readF16AsF32(operand).Value(); } writeF16(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {args[0], args[1], args[2]}))); @@ -128,7 +128,7 @@ bool TranslationContext::floatBinary(const RdnaInstruction& inst, IrOpcode opcod bool TranslationContext::floatTernary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix) { std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { - const RdnaOperand& operand = accumulator && index == 2u ? inst.destination : sourceAt(inst, index); + const RdnaOperand& operand = accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index); const IrType type = IrOpcodeArgumentType(opcode, index); args[index] = type == IrType::F32 && mix ? &readMixF32(operand).Value() : readOperand(operand, type); } diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index b000be782..73813b189 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -200,6 +200,11 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO initialExec = &entryIr.ULessThan(builtin(StageInputKind::LocalInvocationIndex), entryIr.Constant(totalThreads)); } } + if (options.stage == ShaderStageKind::Compute && options.inputInfo.compute->partialGroups) { + for (std::uint32_t axis = 0; axis < 3u; axis++) { + initialExec = &entryIr.LogicalAnd(*initialExec, entryIr.ULessThan(builtin(StageInputKind::GlobalInvocationId, axis), builtin(StageInputKind::DispatchThreadLimit, axis))); + } + } entryIr.SetExec(*initialExec); IrValue& initialMask = entryIr.Emit(IrOpcode::Ballot, IrOpcodeType(IrOpcode::Ballot), {initialExec}); entryIr.SetExecLo(entryIr.CompositeExtract(initialMask, 0u)); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp index 92b69df70..fa9006ed7 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp @@ -33,7 +33,8 @@ MemoryInfo bufferMemoryInfoFromInstruction(const RdnaInstruction& inst) { } bool TranslationContext::bufferLoad(const RdnaInstruction& inst) { - const MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + memory.coherent = inst.glc || inst.dlc; IrOpcode opcode; switch (memory.dataBits) { case 8u: @@ -80,7 +81,8 @@ bool TranslationContext::bufferLoad(const RdnaInstruction& inst) { } bool TranslationContext::bufferStore(const RdnaInstruction& inst) { - const MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + memory.coherent = inst.glc || inst.dlc; IrValue* resource = getBufferResource(memory); const BufferAddress address = readBufferAddress(inst); const IrU32 data = readU32(inst.destination); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index 916875142..da921ca2e 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -41,9 +41,18 @@ MemoryInfo imageMemoryInfoFromInstruction(const RdnaInstruction& inst) { } bool TranslationContext::imageBvhIntersectRay(const RdnaInstruction& inst) { - IrValue& miss = ir.Constant(0xffffffffu); - for (std::uint32_t i = 0u; i < inst.dataDwordCount; ++i) { - writeOperand(offsetOperand(inst.destination, i), &miss); + if (inst.dataDwordCount != 4u || inst.imageD16) { + throw std::runtime_error("image_bvh_intersect_ray returns four dwords"); + } + MemoryInfo memory; + memory.kind = ResourceKind::Global; + memory.dataDwords = 4u; + memory.imageSampleFlags = inst.imageA16 ? RdnaImageSampleFlagA16 : 0u; + IrValue* descriptor = constructU32x4(inst.source1, 4u); + IrValue* address = makeImageAddress(inst, inst.source0); + IrValue& result = ir.Emit(IrOpcode::ImageBvhIntersectRay, IrOpcodeType(IrOpcode::ImageBvhIntersectRay), {descriptor, address, &ir.GetExec()}, addMemoryInfo(memory, inst.programCounter)); + for (std::uint32_t i = 0u; i < 4u; ++i) { + writeOperand(offsetOperand(inst.destination, i), &ir.CompositeExtract(result, i)); } return true; } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index ecd870cdc..d29849d3a 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -84,6 +84,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues computeStorage.groupId[2] = compute.groupIdEnable[2]; computeStorage.tgSizeEn = compute.tgSizeEnable; computeStorage.threadIdsNum = static_cast(compute.threadIdComponentCount); + computeStorage.partialGroups = compute.PartialGroups(); // Workgroup ids (and the thread-group size word) follow the user SGPRs. computeStorage.workgroupRegister = static_cast(context.userDataBaseRegister + context.userData.size()); ShaderStageInputInfo result; diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index bd1d89b8c..135cc4281 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -36,6 +36,12 @@ RdnaOperand TranslationContext::destinationOperand(const RdnaInstruction& inst) return destination; } +RdnaOperand TranslationContext::accumulatorOperand(const RdnaInstruction& inst) { + RdnaOperand accumulator = inst.destination; + accumulator.dpp = false; + return accumulator; +} + RdnaOperand TranslationContext::offsetOperand(const RdnaOperand& operand, std::uint32_t offset) { if (offset == 0u) { return operand;