Merge branch 'boykopovar:main' into main
This commit is contained in:
commit
da35dcd89b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 9ac4cfd195f192ed8b08566f49c28dbb48d08341
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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__);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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__);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue