Merge branch 'boykopovar:main' into main

This commit is contained in:
carman24r 2026-09-30 05:05:49 -04:00 committed by GitHub
commit da35dcd89b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
181 changed files with 4895 additions and 987 deletions

View File

@ -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

3
.gitmodules vendored
View File

@ -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

1
3rdparty/ffmpeg-core vendored Submodule

@ -0,0 +1 @@
Subproject commit 9ac4cfd195f192ed8b08566f49c28dbb48d08341

View File

@ -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)

View File

@ -458,6 +458,15 @@ if(BUILD_TESTING)
set_tests_properties(mspace PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libc>")
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)

View File

@ -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) {

View File

@ -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() {

View File

@ -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<std::uint32_t>(guestAddress) & ~3u, static_cast<std::uint32_t>(guestAddress >> 32u), 0x0f200000u | (static_cast<std::uint32_t>(cachePolicy) << 28u) | (static_cast<std::uint32_t>(chain) << 20u) | sizeInDwords}, function);
}

View File

@ -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) {

View File

@ -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<std::uint32_t>(perInstanceObjectId) << 14u;
return packet;
}
std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) {

View File

@ -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) {

View File

@ -1,33 +1,114 @@
#include "prx/libSceAgc/Misc/include/ShaderFusion.hpp"
#include <algorithm>
#include <cstring>
#include <stdexcept>
#include <string>
#include <cstdint>
#include <cstddef>
#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<int>(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<ShaderBinaryType>(front->type);
const auto backType = static_cast<ShaderBinaryType>(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<ShaderBinaryType>(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<std::uint8_t>(isGs ? ShaderBinaryType::Gs : ShaderBinaryType::Hs);
if (scratch_mem != nullptr) {
auto* regs = static_cast<ShaderRegister*>(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<std::uint64_t>(front->code);
loRegister.value = static_cast<std::uint32_t>(address >> 8u);
hiRegister.value = (hiRegister.value & 0xffffff00u) | static_cast<std::uint32_t>((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;
}

View File

@ -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<std::uintptr_t>(target);
Agc::Command::CheckAddress(address, 4, function);
Agc::Command::CheckBits(sizeInDwords, 0xfffffu, function);
cmd[1] = static_cast<std::uint32_t>(address);
cmd[2] = static_cast<std::uint32_t>(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<std::uint32_t>(cache_policy) << 28u);
return 0;
}
}

View File

@ -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<std::uint32_t>(data);
cmd[6] = static_cast<std::uint32_t>(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;
}

View File

@ -2,25 +2,31 @@
#include "prx/libSceAgc/Command/include/Memory.hpp"
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include <atomic>
#include <cstdint>
#include <cstddef>
#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<std::uint32_t>(cmd[1]).store(static_cast<std::uint32_t>(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__);
}
}

View File

@ -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<unsigned long long>(a2), static_cast<unsigned long long>(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__);

View File

@ -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

View File

@ -40,6 +40,7 @@ class IVideoOutput {
public:
virtual ~IVideoOutput() = default;
virtual std::shared_ptr<IFlipRequest> Reserve(const FlipInfo& info) = 0;
virtual void WaitForFlipRoom() {}
virtual std::shared_ptr<IRenderingWait> CaptureRenderingWait(std::uint32_t index) {
throw std::runtime_error("VideoOut: rendering waits are unsupported by this output");
}

View File

@ -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<const std::uint32_t*>(static_cast<std::uintptr_t>(guest[cursor + 1] & ~3u) | (static_cast<std::uintptr_t>(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<IVideoOutput> 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<std::int32_t>(words[2]), words[3], std::bit_cast<std::int64_t>(static_cast<std::uint64_t>(words[4]) | (static_cast<std::uint64_t>(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<std::uint32_t> control(*const_cast<std::uint32_t*>(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<std::size_t>::max() / sizeof(std::uint32_t), "command size overflow");
GuestMemory::CheckRange(descriptor.addr, static_cast<std::size_t>(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<const void*>(descriptor.addr));
const auto validated = profile ? std::chrono::steady_clock::now() : start;
@ -550,25 +653,7 @@ public:
rethrowFailure();
checkStopping();
require(accepted != std::numeric_limits<std::uint64_t>::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<std::int32_t>(words[2]), words[3], std::bit_cast<std::int64_t>(static_cast<std::uint64_t>(words[4]) | (static_cast<std::uint64_t>(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<ShaderRecompiler::MemoryRegion, 2> 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<std::uint32_t, 3> 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<unsigned long long>(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<std::uint64_t>(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.

View File

@ -506,7 +506,9 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) {
const auto base = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
if (memory.RegionSize > std::numeric_limits<std::uintptr_t>::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<void*>(cursor), static_cast<std::size_t>(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<std::size_t>(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<void*>(cursor), static_cast<std::size_t>(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<std::size_t>(stop - cursor), tracker.pages.data(), &count, true)) return 0;
for (ULONG_PTR i = 0; i < count; ++i) {
const auto block = (reinterpret_cast<std::uintptr_t>(tracker.pages[i]) - tracker.base) / WriteBlockBytes;
tracker.blocks[block] = tracker.generation;
@ -1365,9 +1367,16 @@ void WriteChanged(std::uint64_t address, std::span<const std::byte> 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);

View File

@ -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<const std::uint32_t> 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<const std::uint32_t> 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<const std::uint32_t> 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();

View File

@ -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<std::uint64_t, std::shared_ptr<ComputePipelineObjects>> computePipelines;
std::unique_ptr<Graphics::GpuColorTransfer> colorTransfer;
std::shared_ptr<Graphics::BufferPool> bufferPool;
std::unique_ptr<Graphics::Buffer> emptyBuffer;
std::unique_ptr<Graphics::TextureCache> textureCache;
std::unique_ptr<Graphics::PipelineCache> pipelineCache;
std::unique_ptr<Graphics::DescriptorCache> 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<PFN_vkCreateCommandPool>("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool");
state->bufferPool = std::make_shared<Graphics::BufferPool>(graphicsContext());
state->emptyBuffer = std::make_unique<Graphics::Buffer>(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
state->pipelineCache = std::make_unique<Graphics::PipelineCache>(graphicsContext());
state->detiler = std::make_unique<Graphics::TextureDetiler>(graphicsContext());
state->textureCache = std::make_unique<Graphics::TextureCache>(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<std::uint64_t>(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<Range> ranges = [] {
std::vector<Range> 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 <hex address>:<hex bytes>[,...]");
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<std::pair<std::uint64_t, std::vector<std::uint8_t>>> history;
static bool written = false;
std::lock_guard lock(mutex);
if (written) return;
static std::vector<std::uint64_t> addresses;
if (addresses.empty()) {
std::vector<std::uint64_t> 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<const void*>(candidate), static_cast<std::size_t>(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<unsigned long long>(addresses[i]), static_cast<unsigned long long>(ranges[i].bytes));
}
std::vector<std::uint8_t> contents;
for (std::size_t i = 0; i < ranges.size(); ++i) {
const auto* bytes = reinterpret_cast<const std::uint8_t*>(addresses[i]);
if (!GuestMemory::Accessible(bytes, static_cast<std::size_t>(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<unsigned long long>(history[i].first));
std::ofstream(name, std::ios::binary).write(reinterpret_cast<const char*>(history[i].second.data()), static_cast<std::streamsize>(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;

View File

@ -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<Buffer> table;
std::unique_ptr<Buffer> fault;
std::size_t tableBytes = 0;

View File

@ -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.

View File

@ -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 <cstdint>
#include <memory>
#include <span>
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<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components);
}
#endif

View File

@ -6,6 +6,7 @@
#include "prx/libc/include/GuestAllocations.hpp"
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
@ -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<AddressCopy> 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<ShaderRecompiler::BdaAbi::Range> 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<Region* const> 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<std::pair<std::uint64_t, std::uint64_t>> writes;
std::vector<std::pair<std::uint64_t, std::vector<std::byte>>> heapReferences;
// UploadPrepare ran (regions are frozen); `uploaded` once UploadFinish ran.
bool prepared = false;
bool uploaded = false;

View File

@ -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<Framebuffer> AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> 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<const CompiledShader> shaders) const;
// The same from an assembled block (a draw recipe's, made by AssemblePushConstants once).
void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span<const std::byte, PipelinePushConstantBytes> 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<CachedFramebuffer> framebuffers;
};

View File

@ -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<std::uint64_t> timedKeys;
std::vector<std::uint64_t> 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);

View File

@ -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<std::uint8_t, 8>& 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).

View File

@ -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<std::byte, PipelinePushConstantBytes> bytes) const {
for (const auto& [position, adjustment] : pushPatches) bytes[position] = static_cast<std::byte>(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<std::uint32_t> 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<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> 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<std::pair<std::uint32_t, std::uint32_t>> 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),

View File

@ -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<DepthTarget> 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<ColorTarget> colors;
@ -64,6 +85,7 @@ struct State {
ShaderStages DecodeShaderStages(const QueueState& queue);
State DecodeState(const QueueState& queue);
std::array<std::uint8_t, 8> 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<DrawKeyRange, 38> 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<DrawKeyRange, 44> 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},

View File

@ -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<StorageTexture>& 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<std::uint32_t, VkImageView> extraViews;
bool attachable = false;
std::map<VkFormat, VkImageView> attachmentViews;
std::map<std::pair<VkFormat, std::uint32_t>, VkImageView> attachmentViews;
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
// Results are on the GPU only (guarded by the pending-write registry lock).
bool dirty = false;

View File

@ -1,5 +1,7 @@
#include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <algorithm>
#include <atomic>
#include <bit>
#include <cstdio>
#include <cstdlib>
@ -120,8 +122,8 @@ std::uint64_t hashRanges(const std::vector<ShaderRecompiler::BdaAbi::Range>& 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<Buffer>(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<Buffer>(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<std::uint32_t>::max(), "BDA table range count overflow");
Require(ranges.size() <= (std::numeric_limits<std::size_t>::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<bool> 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<unsigned long long>(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<std::uint32_t>(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<std::uint32_t*>(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<std::uint64_t>(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));
}
}

View File

@ -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::uint32_t>(std::countr_zero(bytesPerElement));

View File

@ -9,7 +9,7 @@ void ReadColorTarget(const ColorTarget& target, std::span<std::byte> 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<const std::byte> 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;
}

View File

@ -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 <algorithm>
#include <array>
#include <cstdio>
#include <map>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <vector>
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<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("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<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth target");
Check(context.Function<PFN_vkBindImageMemory>("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<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth");
auto* recorder = Recorder::Active();
std::unique_ptr<CommandBatch> batch;
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(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<PFN_vkCmdClearDepthStencilImage>("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<Texture> Sampled(std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components) {
std::array<std::uint32_t, 12> key{};
std::copy_n(words.begin(), std::min<std::size_t>(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<unsigned long long>(target.address), target.extent.width, target.extent.height, static_cast<int>(target.format), resource.width, resource.height, resource.format, static_cast<int>(format),
static_cast<unsigned>(resource.tileMode), static_cast<int>(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<Texture>(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::array<std::uint32_t, 12>, std::shared_ptr<Texture>> textures;
void release() noexcept {
textures.clear();
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("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<std::unique_ptr<DepthSurface>>& surfaces() {
static auto* list = new std::vector<std::unique_ptr<DepthSurface>>();
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<DepthSurface>(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<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> 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; });
}
}

View File

@ -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<const CompiledShader> 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<Buffer>(context, colorLayout.Bytes(), copies);
@ -1319,6 +1327,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
binding.target = std::make_unique<RenderTarget>(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<DrawRecipe>();
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);

View File

@ -40,6 +40,8 @@ struct ImageMirror {
std::uint64_t base = 0;
std::uint64_t bytes = 0;
bool writable = false;
bool heap = false;
std::vector<std::uint64_t> generations;
std::weak_ptr<const GuestAllocations::Range> range;
std::shared_ptr<Buffer> 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<std::uint64_t, std::shared_ptr<ImageMirror>> entries;
std::set<std::uint64_t> 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<unsigned long long>(range.address), static_cast<unsigned long long>(range.bytes), reason, static_cast<unsigned long long>(held >> 20u), static_cast<unsigned long long>(heapMirrorBudget() >> 20u));
std::fflush(stderr);
std::abort();
}
bool sameRange(const std::weak_ptr<const GuestAllocations::Range>& mirrored, const std::shared_ptr<const GuestAllocations::Range>& 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<std::size_t>(block.address - block.mirror->base);
const auto* guest = reinterpret_cast<const std::byte*>(block.address);
if (block.mirror->heap) {
std::memcpy(block.mirror->buffer->Bytes().data() + offset, guest, block.length);
block.mirror->generations[static_cast<std::size_t>(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<const void*>(address), static_cast<std::size_t>(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<ImageMirror*>& mirrors, std::vector<RefreshBlock>& 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<std::uint8_t> 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<std::size_t>(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<std::size_t>(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::size_t>(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<ImageMirror> acquireMirror(const Context& context, const std::shared_ptr<const GuestAllocations::Range>& range, std::vector<RefreshBlock>& blocks) {
std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::shared_ptr<const GuestAllocations::Range>& range, std::vector<RefreshBlock>& blocks, bool heap) {
auto& state = Mirrors();
std::shared_ptr<ImageMirror> mirror;
{
@ -540,26 +597,42 @@ std::shared_ptr<ImageMirror> 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<const void*>(range->address), range->bytes, range->writable)) return nullptr;
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(range->address), range->bytes, range->writable && !heap)) return nullptr;
mirror = std::make_shared<ImageMirror>();
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<std::size_t>(((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<Buffer>(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<unsigned long long>(range->address), static_cast<unsigned long long>(range->bytes), error.what());
std::lock_guard lock(state.mutex);
state.failed.insert(range->address);
@ -571,17 +644,17 @@ std::shared_ptr<ImageMirror> 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<ImageMirror> 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<ImageMirror> 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<ImageMirror> 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<unsigned long long>(state.builds), static_cast<unsigned long long>(state.subranges), static_cast<unsigned long long>(state.rebuilds), static_cast<unsigned long long>(state.refreshes), static_cast<unsigned long long>(state.blocksCompared), static_cast<unsigned long long>(state.blocksCopied), static_cast<unsigned long long>(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<unsigned long long>(state.builds), static_cast<unsigned long long>(state.subranges), static_cast<unsigned long long>(state.rebuilds), static_cast<unsigned long long>(state.refreshes), static_cast<unsigned long long>(state.blocksCompared), static_cast<unsigned long long>(state.blocksCopied), static_cast<unsigned long long>(state.syncs), static_cast<unsigned long long>(state.heapRefills), static_cast<unsigned long long>(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<AddressCopy> 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<unsigned long long>(total >> 20u), copies.size(), static_cast<unsigned long long>(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<unsigned long long>(copy.begin), static_cast<unsigned long long>(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<std::uint64_t>::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<RefreshBlock> blocks;
std::vector<ImageMirror*> 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<AddressCopy> 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<std::uint64_t>::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<std::uint32_t>(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<std::uint64_t>::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<ShaderRecompiler::BdaAbi::Range> 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<const std::byte> current, std::span<const std::byte> 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<std::size_t>(from - address);
if (std::memcmp(current.data() + at, original.data() + at, static_cast<std::size_t>(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<unsigned long long>(from), static_cast<unsigned long long>(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<std::size_t>(first - address);
const auto length = static_cast<std::size_t>(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<std::pair<std::uint64_t, std::uint64_t>> mergeWrites(std::vector<std::pair<std::uint64_t, std::uint64_t>> writes) {
std::sort(writes.begin(), writes.end());
std::vector<std::pair<std::uint64_t, std::uint64_t>> 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<std::size_t>(begin - mirror.base), static_cast<std::size_t>(end - begin));
heapReferences.emplace_back(begin, std::vector<std::byte>(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<std::size_t>(begin - mirror.base);
const auto length = static_cast<std::size_t>(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<const std::byte>(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<std::size_t>(begin - region.begin);
const auto length = static_cast<std::size_t>(end - begin);
GuestMemory::WriteChanged(begin, region.buffer->Bytes().subspan(first, length), std::span<const std::byte>(region.uploaded).subspan(first, length));
storeChanged(begin, region.buffer->Bytes().subspan(first, length), std::span<const std::byte>(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<std::size_t>(to - from);
GuestMemory::WriteChanged(from, region.buffer->Bytes().subspan(static_cast<std::size_t>(from - region.begin), length), std::span<const std::byte>(region.uploaded).subspan(packed + static_cast<std::size_t>(from - first), length));
storeChanged(from, region.buffer->Bytes().subspan(static_cast<std::size_t>(from - region.begin), length), std::span<const std::byte>(region.uploaded).subspan(packed + static_cast<std::size_t>(from - first), length));
}
packed += static_cast<std::size_t>(last - first);
}
@ -2075,6 +2257,7 @@ void GuestBufferMemory::WriteBack() {
committed = true;
lease.clear();
space.reset();
heapReferences.clear();
}
std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> GuestBufferMemory::DirectRegions() const {
@ -2082,16 +2265,15 @@ std::optional<std::vector<std::pair<std::uint64_t, std::uint64_t>>> GuestBufferM
std::vector<std::pair<std::uint64_t, std::uint64_t>> 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) {

View File

@ -103,7 +103,6 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> 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<void>(maxUncompBlkSize);
static_cast<void>(maxCompBlkSize);
// Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so

View File

@ -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<const CompiledShader> 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<const CompiledShader> 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<std::uint32_t>(references.size());
subpass.pColorAttachments = references.empty() ? nullptr : references.data();
const VkAttachmentReference depthReference{static_cast<std::uint32_t>(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<std::uint32_t>(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<std::uint32_t>(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<Framebuffer> Pipeline::AcquireFramebuffer(std::span<const VkImageView> targets, std::span<const std::shared_ptr<StorageTexture>> 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<const CompiledShader> 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<const std::byte, PipelinePushConstantBytes> bytes) const {
if (stages == 0) return;
context.Resolved(&DeviceFunctions::cmdPushConstants, "vkCmdPushConstants")(commands, layout, stages, 0, PipelinePushConstantBytes, bytes.data());
@ -322,6 +337,16 @@ std::vector<std::byte> 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;

View File

@ -166,6 +166,7 @@ std::atomic<int>& WaitersOf(std::uint64_t id) { return unlockedWaiters[id % Wait
std::mutex liveRecordersMutex;
std::vector<std::uint64_t> liveRecorders;
std::atomic<std::uint64_t> nextRecorderId{1};
std::atomic<std::uint64_t> 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<PFN_vkCmdWriteTimestamp>("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<PFN_vkCreateQueryPool>("vkCreateQueryPool")(context.device, &info, nullptr, &batch.samples), "vkCreateQueryPool occlusion");
context.Function<PFN_vkCmdResetQueryPool>("vkCmdResetQueryPool")(batch.commands, batch.samples, 0, 1);
context.Function<PFN_vkCmdBeginQuery>("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<PFN_vkGetQueryPoolResults>("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<std::uint64_t> 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<PFN_vkCmdEndQuery>("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> 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> batch, bool wait, int source) {
void Recorder::release(Batch& batch) noexcept {
if (batch.queries != VK_NULL_HANDLE) context.Function<PFN_vkDestroyQueryPool>("vkDestroyQueryPool")(context.device, batch.queries, nullptr);
batch.queries = VK_NULL_HANDLE;
if (batch.samples != VK_NULL_HANDLE) context.Function<PFN_vkDestroyQueryPool>("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) {

View File

@ -81,6 +81,7 @@ std::span<std::byte> 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;

View File

@ -32,8 +32,12 @@ void ShaderResources::prepareAddressBindings(std::span<const CompiledShader> 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();

View File

@ -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<std::uint8_t, 8>& 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<std::uint8_t, 8> 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
};
}

View File

@ -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 <algorithm>
@ -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 <cstring>
#include <limits>
#include <list>
@ -262,6 +264,12 @@ std::shared_ptr<StorageTexture> sampledStorageSource(const Context& context, con
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> 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<unsigned long long>(resource.baseAddress), resource.width, resource.height, static_cast<unsigned long long>(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<unsigned long long>(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<std::uint32_t, 8> SurfaceKey(const Context& context, const GuestTextu
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> 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<unsigned long long>(resource.baseAddress));
throw std::runtime_error(text);
}
static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr;
if (disabled) return std::make_shared<StorageTexture>(context, *context.detiler, resource, mip);
static_cast<void>(words);
@ -812,7 +825,14 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> 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<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 4, 4), target, indexAddress, indexBytes, written, atomic));
const auto index = addGuestBuffer(std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(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<std::int32_t>(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<std::uint32_t>(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<std::uint32_t>(allocation.pushByte), allocation.adjustment);
}
timing.descriptorsMs += phase(BuildPhase::Descriptors);
noteReusable();
completed = true;
@ -1999,8 +2024,10 @@ std::size_t ShaderResources::addGuestBuffer(std::span<const std::uint32_t> 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<std::size_t>::max(), "shader buffer size exceeds host address space");
const auto size = static_cast<std::size_t>(byteSize);

View File

@ -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<VkCompareOp>(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<std::uint64_t>(high->second & 0xffu) << 40u) | (static_cast<std::uint64_t>(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<std::uint8_t>(read(cx, 0x00a) & 0xffu);
result.depth = depth;
result.depthTest = (depthControl & 2u) != 0;
result.depthWrite = result.depthTest && (depthControl & 4u) != 0 && !depthReadOnly;
result.depthCompare = static_cast<VkCompareOp>((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<std::uint8_t>((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<std::uint64_t>(high) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x318 + stride)) << 8u)) + mipOffset;
color.surfaceAddress = (static_cast<std::uint64_t>(high) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x318 + stride)) << 8u);
color.address = color.surfaceAddress + mipOffset;
color.bytes = colorLayout.Bytes();
GuestMemory::CheckRange(reinterpret_cast<const void*>(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<std::uint8_t, 8> ExportMappings(const State& state) {
std::array<std::uint8_t, 8> 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;

View File

@ -457,6 +457,17 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
}
}
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<PFN_vkCreateImageView>("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<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment");
attachmentViews.emplace(format, created);
attachmentViews.emplace(std::pair{format, mip}, created);
return created;
}

View File

@ -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 <string>
#include <algorithm>
#include <bit>
#include <cstddef>
@ -245,7 +246,7 @@ std::array<std::uint32_t, 3> 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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -2,6 +2,7 @@
#include <cstdint>
#include <cstddef>
#include <stdexcept>
#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;
}

View File

@ -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<void>(serializer.Deserialize(invalid)); }, "serialization version");
reject([&] { static_cast<void>(serializer.Deserialize(invalid)); }, "request signature");
reject([&] { static_cast<void>(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version");
}

View File

@ -2,12 +2,19 @@
#include "ColorTransferTests.hpp"
#include <fstream>
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include <SDL_loadso.h>
#include <array>
#include <iostream>
#include <vector>
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;

View File

@ -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 <windows.h>
#endif
#include <algorithm>
#include <array>
#include <cstring>
#include <functional>
#include <limits>
#include <string>
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<std::uint8_t*>(block);
std::memset(block, 0x11, bytes);
const auto address = reinterpret_cast<std::uintptr_t>(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<void(GuestBufferMemory&)>& 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<std::uint8_t*>(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<std::uintptr_t>(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<std::uintptr_t>(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<std::uintptr_t>(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<std::uint8_t*>(raw);
std::memset(raw, 0x11, size);
const auto base = reinterpret_cast<std::uintptr_t>(raw);
const auto page = base + 65536;
DWORD previous = 0;
Require(VirtualProtect(reinterpret_cast<void*>(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<std::size_t>(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<void*>(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");
}

View File

@ -8,6 +8,7 @@
struct BdaTestAccess {
std::function<std::span<std::byte>(VkBuffer)> bytes;
std::function<VkDescriptorBufferInfo(std::uint32_t)> descriptor;
std::function<std::span<std::byte>(VkDeviceAddress)> addressBytes;
};
void RunBdaResourceTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access);

View File

@ -1,7 +1,10 @@
#include "BdaTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <span>
#include <vector>
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<std::byte, 65536> guest{};
guest.fill(std::byte{0x6b});
static std::vector<std::byte> storage(2 * 65536);
const std::span guest(reinterpret_cast<std::byte*>((reinterpret_cast<std::uintptr_t>(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}), 65536);
std::fill(guest.begin(), guest.end(), std::byte{0x6b});
ColorTarget target{reinterpret_cast<std::uintptr_t>(guest.data()), {2, 2}, VK_FORMAT_R8G8B8A8_UNORM, guest.size(), 0xe4, ColorTileMode::RenderTarget};
std::array<std::byte, 16> pixels{};
pixels.fill(std::byte{0x32});

View File

@ -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 <cstdio>
#include <exception>
#include <vector>
using namespace ShaderRecompiler;
int main() {
const std::vector<std::uint32_t> 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<DppMoveFlags>().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;
}

View File

@ -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<void>(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, state.hasColorTarget, state.color.componentMapping));
static_cast<void>(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<AgcDriver::Graphics::RegisterRead> 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<const void*>(0x1000), 8); }), "not readable");
expectSingleAccepted(changed([](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(0x1000), 8); }), "an unmapped V#");
expectSingleFailure(changed([&](auto& binding) { binding.guestDescriptor = vsharp(reinterpret_cast<const void*>(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<const void*>(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<const void*>(0x1000), 8)); }), "an unmapped V# element");
expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast<const void*>(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<void>(BuildRectListShaders(vertex, fragment, target)); }, "per-vertex interpolation");
fragment.fragmentParameters[0].perVertex = false;
vertex.parameterExports.clear();
expectFailure([&] { static_cast<void>(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<void>(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<std::byte> { 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<VkBuffer>(offset / 0x10000);
return std::span<std::byte>(mock.memories.at(mock.bufferMemory.at(buffer))).subspan(offset % 0x10000);
}
});
Require(mock.live == 0, "BDA resources leaked Vulkan objects");

View File

@ -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<float>(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;

View File

@ -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<std::uint32_t, 4> 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");

View File

@ -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<std::byte> original(sizeof(guest)), current(sizeof(guest));
current[3] = std::byte{7};
guest[100] = 0x55;
AgcDriver::GuestMemory::WriteChanged(reinterpret_cast<std::uintptr_t>(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<std::uint32_t>(rejectedCommands.size()), 0, {}};
destination[0] = 0;
@ -481,6 +498,7 @@ int main(int argc, char** argv) {
return 0;
}
testCatalog();
testWriteChangedKeepsUntouchedBytes();
testRegisters();
testContextAndBases();
testIndexedDraw();

View File

@ -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<volatile std::uint32_t*>(block);
const auto address = reinterpret_cast<std::uint64_t>(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<std::uint64_t>(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);
}

View File

@ -0,0 +1,49 @@
#include "ControlFlow/Structurizer.hpp"
#include <cstdio>
#include <exception>
#include <utility>
#include <vector>
using namespace ShaderRecompiler;
int main() {
const std::vector<std::vector<std::uint32_t>> 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;
}

View File

@ -1,8 +1,11 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include <algorithm>
#include <array>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
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<std::uint32_t, 5>{1, 7, 56, 71, 77}) {
const auto mips = ComputeMipLayout(TextureTileMode::RenderTarget64KB, format, 1024, 513, 11);
std::uint64_t linearEnd = 0;
std::vector<std::pair<std::uint64_t, std::uint64_t>> 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<std::uint64_t>(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");

View File

@ -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<const char*>(result.spirv.data()), static_cast<std::streamsize>(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] <shader.req>...\n the driver writes shader_<address>.req files when APS5_DUMP_SHADERS is set\n");
std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--mem] [--spv] <shader.req>...\n the driver writes shader_<address>.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) {

View File

@ -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)

View File

@ -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;

View File

@ -3,14 +3,21 @@
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "libatrac9.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/log.h>
}
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
// 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<std::uint8_t> JoinInputs(const JobHeader& job, const AjmBuffer* inputs) {
std::vector<std::uint8_t> input;
for (std::uint32_t index = 0; index < job.inputCount; ++index) {
const auto* data = static_cast<const std::uint8_t*>(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<std::uint8_t*>(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<std::uint32_t>((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<std::size_t>(count, instance.gapless.skipSamples - instance.gapless.skippedSamples);
instance.gapless.skippedSamples = static_cast<std::uint16_t>(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::size_t>(std::min<std::uint64_t>(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<std::uint8_t*>(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<std::int32_t>(consumed), static_cast<std::int32_t>(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<std::size_t>(instance.info.channels);
const auto frameSamples = static_cast<std::size_t>(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<std::uint32_t>((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<std::uint8_t> 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<std::uint8_t*>(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<std::size_t>(count, instance.gapless.skipSamples - instance.gapless.skippedSamples);
instance.gapless.skippedSamples = static_cast<std::uint16_t>(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::size_t>(std::min<std::uint64_t>(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<unsigned long long>(job.flags), input.size(), job.inputCount, outputCapacity, job.outputCount, static_cast<unsigned long long>(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<unsigned>(result), static_cast<unsigned>(decodeStatus), frames, consumed, produced, static_cast<unsigned long long>(instance.totalDecodedSamples), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples);
job.instance, static_cast<unsigned long long>(job.flags), input.size(), job.inputCount, pcmOutputs.capacity, job.outputCount, static_cast<unsigned long long>(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<unsigned>(result), static_cast<unsigned>(decodeStatus), frames, consumed, pcmOutputs.produced, static_cast<unsigned long long>(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<std::uint8_t*>(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<std::int32_t>(consumed), static_cast<std::int32_t>(produced), instance.totalDecodedSamples};
write(&stream, sizeof(stream));
}
if (job.flags & SIDEBAND_FORMAT) {
const SidebandFormat format{static_cast<std::uint32_t>(channels), ChannelMask(channels), static_cast<std::uint32_t>(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<std::uint32_t>(channels), ChannelMask(channels), static_cast<std::uint32_t>(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<AVPacket, void (*)(AVPacket*)> packet(av_packet_alloc(), [](AVPacket* p) { av_packet_free(&p); });
std::unique_ptr<AVFrame, void (*)(AVFrame*)> decoded(av_frame_alloc(), [](AVFrame* f) { av_frame_free(&f); });
if (!packet || !decoded) throw std::bad_alloc();
std::vector<std::uint8_t> 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<std::uint8_t*>(input.data() + consumed);
packet->size = static_cast<int>(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<std::size_t>(decoded->ch_layout.nb_channels);
const auto samples = static_cast<std::size_t>(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<const float*>(decoded->extended_data[channel])[sample];
auto* out = pcm.data() + (sample * channels + channel) * sampleBytes;
if (encoding == 0) {
const auto converted = static_cast<std::int16_t>(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::int32_t>(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<unsigned long long>(job.flags), input.size(), pcmOutputs.capacity, static_cast<unsigned>(result), frames, consumed, pcmOutputs.produced,
instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast<unsigned long long>(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<unsigned long long>(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<std::uint32_t> 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<Instance>();
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<void*>(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);

View File

@ -2,12 +2,30 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include <algorithm>
#include <cstring>
#include <filesystem>
#include <stdexcept>
#include <string>
static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast<int>(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<size_t>(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<size_t>(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;
}
}

View File

@ -1,3 +1,5 @@
#include <cstdlib>
#include <cstdio>
#include <algorithm>
#include <array>
#include <atomic>
@ -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<int>(format), static_cast<unsigned>(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;
}
}

View File

@ -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;

View File

@ -1,7 +1,10 @@
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <cstdio>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
#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<std::size_t>(frame) * ch;
const auto first = static_cast<std::size_t>(frame) * ch;
for (std::uint32_t c = 0; c < ch; c++) {
in[c] = port.int16 ? static_cast<const std::int16_t*>(port.data)[first + c] / 32768.0f : static_cast<const float*>(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<AudioOut2PortHandle>(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<unsigned long long>(ctx), static_cast<unsigned long long>(*port), params->port_type, params->data_format,

View File

@ -1,30 +1,101 @@
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <new>
#include <stdexcept>
#include <string>
#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<SpeakerArray*>(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<int>(std::sqrt(static_cast<double>(acn)));
const int m = static_cast<int>(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<float>(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<const GuestVbapParams*>(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<std::uintptr_t>(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::uint32_t>(std::sqrt(static_cast<double>(acn)));
const std::uint32_t decodable = std::max<std::uint32_t>(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<float>(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) {

View File

@ -2,13 +2,32 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
#include <stdexcept>
#include <string>
namespace {
std::atomic<bool> 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;
}
}

View File

@ -2,13 +2,47 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
#include <stdexcept>
#include <string>
namespace {
std::atomic<bool> 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;
}
}

View File

@ -2,13 +2,52 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
#include <stdexcept>
#include <string>
namespace {
std::atomic<bool> 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;
}
}

View File

@ -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;
}
}

View File

@ -1,8 +1,19 @@
#include <cstdint>
#include <cstddef>
#include <mutex>
#include <set>
#include <stdexcept>
#include <string>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
namespace {
std::mutex g_keyboardMutex;
std::set<int32_t> 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;
}

View File

@ -1,10 +1,44 @@
#include <atomic>
#include <cstdint>
#include <stdexcept>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
namespace {
std::atomic<int> 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();
}
}

View File

@ -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;
}
}

View File

@ -2,27 +2,85 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#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<int>(0x80558003);
static constexpr int SCE_NP_ENTITLEMENT_ACCESS_ERROR_NOT_FOUND = static_cast<int>(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<NpEntitlementAccessAddcontEntitlementInfo>& OwnedAddons() {
static const auto owned = [] {
std::vector<NpEntitlementAccessAddcontEntitlementInfo> 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<uint32_t>(owned.size());
std::copy_n(owned.begin(), std::min<std::size_t>(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;
}
}

View File

@ -9,6 +9,7 @@ static constexpr int SCE_NP_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80550003
static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast<int>(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<int> 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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,18 @@
#include <cstdint>
#include <cstddef>
#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;
}
}

View File

@ -0,0 +1,13 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
int APS5_VABI scePlayerSelectionDialogTerminate(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}

View File

@ -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;
}
}

View File

@ -1,26 +1,27 @@
#include <cstdint>
#include <cstddef>
#include <stdexcept>
#include <string>
#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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -59,6 +59,16 @@ public:
return std::make_shared<RenderingWait>(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<AgcDriver::IFlipRequest> 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<int>(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) {

View File

@ -0,0 +1,78 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <cstring>
#include <map>
#include <mutex>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
std::mutex g_infoMutex;
std::map<int32_t, std::vector<unsigned char>> 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<const unsigned char*>(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;
}
}

View File

@ -1,118 +1,207 @@
#include <cstdint>
#include <cstddef>
#include <map>
#include <mutex>
#include <set>
#include <stdexcept>
#include <string>
#include <utility>
#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<std::uint32_t, Port> ports;
std::set<std::pair<std::uint32_t, std::uint32_t>> 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<std::uint32_t>(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;
}
}

View File

@ -1,18 +1,45 @@
#include <atomic>
#include <cstdint>
#include <stdexcept>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
namespace {
std::atomic<int> 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();
}
}

View File

@ -5,7 +5,6 @@
#include <cstdint>
// 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
}

View File

@ -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<const char*>();
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<const char16_t*>();
if (!value) throw std::invalid_argument("Null formatted wide string");
if (!value) value = u"(null)";
std::string utf8;
while (*value) {
char32_t code = *value++;

View File

@ -0,0 +1,308 @@
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSMAPPINGS_HPP
#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSMAPPINGS_HPP
#ifdef _WIN32
#include <windows.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <map>
#include <memory>
#include <vector>
#include <stdexcept>
#include <system_error>
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<std::uintptr_t>(address) + bytes;
for (auto cursor = reinterpret_cast<std::uintptr_t>(address); cursor < end;) {
const auto memory = query(cursor);
const auto stop = std::min(end, reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<void*>(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<void*>(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<std::uintptr_t>(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<std::uintptr_t>(address);
reset(cursor, bytes);
for (std::size_t done = 0; done < bytes; done += pageBytes) {
split(cursor + done, pageBytes);
void* page = reinterpret_cast<void*>(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<std::uintptr_t>(section), offset + done);
auto shared = physical[key].lock();
if (!shared) {
shared = std::make_shared<SharedPage>();
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<void*>(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<void*>(at);
return true;
}
for (auto at = cursor; at < stop; at += 4096) pages[(*count)++] = reinterpret_cast<void*>(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<void*>(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<std::uintptr_t>(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<void*>(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<std::uintptr_t> aliases;
};
struct View {
std::shared_ptr<SharedPage> 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<AllocateFunction>(GetProcAddress(module, "VirtualAlloc2"));
map = reinterpret_cast<MapFunction>(GetProcAddress(module, "MapViewOfFile3"));
unmap = reinterpret_cast<UnmapFunction>(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<int>(GetLastError()), std::system_category(), operation);
}
static MEMORY_BASIC_INFORMATION query(std::uintptr_t address) {
MEMORY_BASIC_INFORMATION memory{};
if (VirtualQuery(reinterpret_cast<void*>(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<std::uintptr_t>(memory.AllocationBase));
if (memory.State != MEM_RESERVE) throw std::runtime_error("guest mapping requires a placeholder");
const auto base = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
if (address != base) {
if (!VirtualFree(reinterpret_cast<void*>(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<void*>(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<std::uintptr_t>(memory.BaseAddress) + memory.RegionSize);
continue;
}
if (reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<void*>(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<void*>(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<std::uintptr_t>(query(end - 1).AllocationBase));
const auto lastBase = reinterpret_cast<std::uintptr_t>(last.BaseAddress);
if (lastBase + last.RegionSize > end) split(lastBase, end - lastBase);
const auto first = query(address);
split(address, std::min(bytes, reinterpret_cast<std::uintptr_t>(first.BaseAddress) + first.RegionSize - address));
if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast<void*>(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders");
}
std::map<std::uintptr_t, std::uintptr_t> cleanRanges;
std::map<std::uintptr_t, View> views;
std::map<std::pair<std::uintptr_t, std::uint64_t>, std::weak_ptr<SharedPage>> physical;
std::mutex mutex;
AllocateFunction allocate = nullptr;
MapFunction map = nullptr;
UnmapFunction unmap = nullptr;
};
}
#endif
#endif

View File

@ -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 <typename NextPointer, typename NextCapacity>
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<unsigned char>(*cursor))) {
while (std::isspace(static_cast<unsigned char>(*cursor))) ++cursor;
while (std::isspace(static_cast<unsigned char>(*input))) ++input;
continue;
}
if (*cursor != '%' || cursor[1] == '%') {
const char expected = *cursor == '%' ? '%' : *cursor;
cursor += *cursor == '%' ? 2 : 1;
if (expected == '%') while (std::isspace(static_cast<unsigned char>(*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<unsigned char>(*cursor))) width = width * 10 + static_cast<unsigned long>(*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<int*>(nextPointer()) = static_cast<int>(input - buffer);
continue;
}
if (conversion != 'c' && conversion != '[') {
const char* probe = input;
while (std::isspace(static_cast<unsigned char>(*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<unsigned long>(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<char*>(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<unsigned char>(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<char*>(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<unsigned char>(*cursor))) {
while (std::isspace(static_cast<unsigned char>(*cursor))) ++cursor;
while (std::isspace(static_cast<unsigned char>(*input))) ++input;
continue;
}
if (*cursor != '%' || cursor[1] == '%') {
const char expected = *cursor == '%' ? '%' : *cursor;
cursor += *cursor == '%' ? 2 : 1;
if (expected == '%') while (std::isspace(static_cast<unsigned char>(*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<unsigned char>(*cursor))) width = width * 10 + static_cast<unsigned long>(*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<int>(input - buffer);
continue;
}
if (conversion != 'c' && conversion != '[') {
const char* probe = input;
while (std::isspace(static_cast<unsigned char>(*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<unsigned long>(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<char*>(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<unsigned char>(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<char*>(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

View File

@ -2,6 +2,8 @@
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <thread>
#include <limits>
#include <iterator>
@ -35,6 +37,14 @@ std::atomic<std::uint64_t> generation{1};
std::atomic<void (*)(std::uintptr_t, std::size_t)> invalidator{nullptr};
std::atomic<bool (*)()> 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<const Range>(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<unsigned long long>(address), static_cast<unsigned long long>(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<std::uintptr_t>(pointer);
require(bytes <= std::numeric_limits<std::uint64_t>::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<MutationState*>(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<unsigned long long>(address), static_cast<unsigned long long>(bytes), syncedRounds);
continue;
}
require(++idleRounds < 1000000, "guest allocation is owned by an active GPU command");
}
}

View File

@ -1,4 +1,5 @@
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/WindowsMappings.hpp"
#include <algorithm>
#include <cstdint>
#include <cstdio>
@ -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<void*>(base), size, MEM_RESERVE | MEM_WRITE_WATCH, PAGE_NOACCESS);
_writeWatched = reserved != nullptr;
if (!reserved) reserved = VirtualAlloc(reinterpret_cast<void*>(base), size, MEM_RESERVE, PAGE_NOACCESS);
_writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr;
void* reserved = WindowsMappings::Get().Reserve(reinterpret_cast<void*>(base), size);
if (!reserved) continue;
_base = reinterpret_cast<std::uintptr_t>(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();
}

View File

@ -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<int>(GetLastError()), std::system_category(), "guest heap decommit failed");
GuestArena::GuestArenaReset_nid_postfix(raw, bytes);
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast<std::uintptr_t>(raw), bytes);
#endif
GuestArena::GuestArenaRelease_nid_postfix(raw, bytes);

Some files were not shown because too many files have changed in this diff Show More