fix(job-manager): refresh profiler on reconciled owned state

This commit is contained in:
Felipebmaitan committed 2026-10-24 18:17:01 -03:00
commit b1efedd841
537 files changed
+31477 -2666

No files matched your search

+1 -1
View File
@@ -641,7 +641,7 @@ jobs:
uses: actions/cache/restore@v6
with:
path: configure-seed.cmake
key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', '.github/workflows/build.yml') }}
key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', 'tools/configure_seed.py', '.github/workflows/build.yml') }}
- shell: cmd
run: if exist configure-seed.cmake (cmake -C configure-seed.cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") else (cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown")
- if: steps.seed-cache.outputs.cache-hit != 'true'
+5 -2
View File
@@ -6,13 +6,12 @@ endif()
project(Relinker CXX)
set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests")
set(ANYPS5_WINDOWS_TEST_EXIT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WindowsTestExit.cpp")
function(add_test_executable target)
add_executable(${target} ${ARGN})
set_target_properties(${target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>")
if(MINGW)
target_sources(${target} PRIVATE "${ANYPS5_WINDOWS_TEST_EXIT}")
target_sources(${target} PRIVATE $<TARGET_OBJECTS:anyps5_windows_test_exit>)
target_link_options(${target} PRIVATE LINKER:--wrap=main)
endif()
endfunction()
@@ -96,6 +95,10 @@ if(APPLE AND NOT ANYPS5_RELINKER_ONLY)
add_link_options(-fexperimental-library)
endif()
if(MINGW)
add_library(anyps5_windows_test_exit OBJECT cmake/WindowsTestExit.cpp)
endif()
if(BUILD_TESTING)
add_test_executable(shader_cache_file_read_tests
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/CacheFileReadTests.cpp)
+31 -1
View File
@@ -382,7 +382,7 @@ if(BUILD_TESTING)
if(WIN32)
add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp)
target_include_directories(windows_write_watch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00)
target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00 APS5_COUNT_MAPPING_QUERIES=1)
if(MINGW)
target_compile_options(windows_write_watch_tests PRIVATE -fasynchronous-unwind-tables)
endif()
@@ -748,6 +748,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCppWebApi>")
endif()
add_test_executable(guest_np_cpp_web_api_offline_tests tests/GuestNpCppWebApiOffline.cpp)
target_include_directories(guest_np_cpp_web_api_offline_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_cpp_web_api_offline_tests PRIVATE libSceNpCppWebApi libc)
configure_windows_unwind(guest_np_cpp_web_api_offline_tests)
add_test(NAME guest_np_cpp_web_api_offline COMMAND guest_np_cpp_web_api_offline_tests)
if(WIN32)
set_tests_properties(guest_np_cpp_web_api_offline PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCppWebApi>")
endif()
add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp)
target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData)
@@ -850,6 +860,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVoiceChat>")
endif()
add_test_executable(guest_gnm_neo_embedded_tests tests/GuestGnmNeoEmbedded.cpp)
target_include_directories(guest_gnm_neo_embedded_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_gnm_neo_embedded_tests PRIVATE libSceGnmDriverForNeoMode libc)
configure_windows_unwind(guest_gnm_neo_embedded_tests)
add_test(NAME guest_gnm_neo_embedded COMMAND guest_gnm_neo_embedded_tests)
if(WIN32)
set_tests_properties(guest_gnm_neo_embedded PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>")
endif()
add_test_executable(guest_np_game_intent_tests tests/GuestNpGameIntent.cpp)
target_include_directories(guest_np_game_intent_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_game_intent_tests PRIVATE libSceNpGameIntent libc)
@@ -860,6 +880,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpGameIntent>")
endif()
add_test_executable(guest_gnm_neo_shaders_tests tests/GuestGnmNeoShaders.cpp)
target_include_directories(guest_gnm_neo_shaders_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_gnm_neo_shaders_tests PRIVATE libSceGnmDriverForNeoMode libc)
configure_windows_unwind(guest_gnm_neo_shaders_tests)
add_test(NAME guest_gnm_neo_shaders COMMAND guest_gnm_neo_shaders_tests)
if(WIN32)
set_tests_properties(guest_gnm_neo_shaders PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>")
endif()
add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp)
target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(guest_vorbis_dec_tests PRIVATE OV_EXCLUDE_STATIC_CALLBACKS)
@@ -102,6 +102,18 @@ uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uin
return Agc::Marker::Push(buf, str, __func__);
}
uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buf, const char* str, uint32_t length, uint32_t color) {
(void)color;
return Agc::Marker::PushSpan(buf, str, length, __func__);
}
uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buf, const char* str, uint32_t length, uint32_t color) {
(void)color;
auto* packet = Agc::Marker::PushSpan(buf, str, length, __func__);
Agc::Marker::Pop(buf, __func__);
return packet;
}
uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf) {
return Agc::Marker::Pop(buf, __func__);
}
@@ -7,6 +7,7 @@
namespace Agc::Marker {
std::uint32_t* Push(CommandBuffer* buf, const char* str, const char* function);
std::uint32_t* PushSpan(CommandBuffer* buf, const char* str, std::uint32_t length, const char* function);
std::uint32_t* Pop(CommandBuffer* buf, const char* function);
}
@@ -22,13 +22,21 @@ namespace Agc::Marker {
std::uint32_t* Push(CommandBuffer* buf, const char* str, const char* function) {
Agc::Command::Require(buf != nullptr, function, "null command buffer");
const char* text = str ? str : "";
const std::size_t bytes = std::strlen(text) + 1;
const std::size_t length = std::strlen(text);
Agc::Command::Require(length < 0x10000u, function, "marker text too long");
return PushSpan(buf, text, static_cast<std::uint32_t>(length), function);
}
std::uint32_t* PushSpan(CommandBuffer* buf, const char* str, std::uint32_t length, const char* function) {
Agc::Command::Require(buf != nullptr, function, "null command buffer");
Agc::Command::Require(str != nullptr || length == 0, function, "null marker text");
const std::size_t bytes = std::size_t{length} + 1;
const auto payload = static_cast<std::uint32_t>((bytes + 3) / 4);
Agc::Command::Require(payload <= 0x4000u, function, "marker text too long");
auto* packet = Agc::Command::Allocate(buf, payload + 1, function);
packet[0] = Agc::Command::Header(OpcodeNop, payload + 1, CustomPushMarker << 2);
std::memset(packet + 1, 0, payload * sizeof(std::uint32_t));
std::memcpy(packet + 1, text, bytes);
if (length != 0) std::memcpy(packet + 1, str, length);
return packet;
}
@@ -60,4 +68,16 @@ uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, uin
return Agc::Marker::Push(buf, str, __func__);
}
uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buf, const char* str, uint32_t length, uint32_t color) {
(void)color;
return Agc::Marker::PushSpan(buf, str, length, __func__);
}
uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buf, const char* str, uint32_t length, uint32_t color) {
(void)color;
auto* packet = Agc::Marker::PushSpan(buf, str, length, __func__);
Agc::Marker::Pop(buf, __func__);
return packet;
}
}
+31 -13
View File
@@ -11,37 +11,55 @@
namespace {
constexpr int GRAPHICS5_ERROR_INVALID_ALIGNMENT = static_cast<int>(0x8a6c0002u);
constexpr int GRAPHICS5_ERROR_OUT_OF_RANGE = static_cast<int>(0x8a6c000bu);
constexpr int GRAPHICS5_ERROR_SEMAPHORE_MEMORY_REGISTERED = static_cast<int>(0x8a6c0048u);
constexpr int GRAPHICS5_ERROR_SEMAPHORE_MEMORY_UNREGISTERED = static_cast<int>(0x8a6c0049u);
constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u;
constexpr std::uint64_t semaphoreMemoryLimit = 0x40000000u;
constexpr std::uint64_t semaphoreLabelBytes = 32u;
std::mutex semaphoreMutex;
std::uint8_t* semaphoreMemory = nullptr;
std::uint64_t semaphoreBytes = 0;
bool aligned(std::uint64_t value) {
return (value & (semaphoreMemoryAlignment - 1u)) == 0;
}
int registerSemaphoreMemory(void* memory, std::uint64_t sizeInBytes, bool limited, const char* name) {
const auto address = reinterpret_cast<std::uintptr_t>(memory);
std::lock_guard lock(semaphoreMutex);
if (semaphoreBytes != 0) return GRAPHICS5_ERROR_SEMAPHORE_MEMORY_REGISTERED;
if (!aligned(sizeInBytes)) return GRAPHICS5_ERROR_INVALID_ALIGNMENT;
if (limited && sizeInBytes > semaphoreMemoryLimit) return GRAPHICS5_ERROR_OUT_OF_RANGE;
if (!aligned(address)) return GRAPHICS5_ERROR_INVALID_ALIGNMENT;
if (sizeInBytes != 0) {
Agc::Command::Require(memory != nullptr, name, "null semaphore memory with a non-zero size");
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, name, "semaphore memory wraps the address space");
std::memset(memory, 0, sizeInBytes);
}
semaphoreMemory = static_cast<std::uint8_t*>(memory);
semaphoreBytes = sizeInBytes;
return 0;
}
}
extern "C" {
int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
const auto address = reinterpret_cast<std::uintptr_t>(memory);
std::lock_guard lock(semaphoreMutex);
if (semaphoreBytes != 0) return static_cast<int>(0x8a6c0048u);
if (((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) != 0) return static_cast<int>(0x8a6c0002u);
Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space");
auto* labels = static_cast<std::uint8_t*>(memory);
std::memset(labels, 0, sizeInBytes);
semaphoreMemory = labels;
semaphoreBytes = sizeInBytes;
return 0;
return registerSemaphoreMemory(memory, sizeInBytes, false, __func__);
}
int APS5_VABI sceAgcSetSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
return registerSemaphoreMemory(memory, sizeInBytes, true, __func__);
}
int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) {
std::lock_guard lock(semaphoreMutex);
if (semaphoreBytes == 0) return static_cast<int>(0x8a6c0049u);
if (index >= semaphoreBytes / semaphoreLabelBytes) return static_cast<int>(0x8a6c000bu);
if (semaphoreBytes == 0) return GRAPHICS5_ERROR_SEMAPHORE_MEMORY_UNREGISTERED;
if (index >= semaphoreBytes / semaphoreLabelBytes) return GRAPHICS5_ERROR_OUT_OF_RANGE;
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output");
*label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes;
return 0;
+5 -25
View File
@@ -5,39 +5,19 @@
extern "C" {
std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcAcquireMemSetEngine(std::uint32_t* packet, std::uint8_t engine) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetSemaphoreMemory(void* memory, std::uint64_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* destination, const void* pattern, std::uint64_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcAcbInitializeDefaultHardwareState() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
@@ -48,6 +48,10 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBu
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buf, const char* str, std::uint32_t length, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buf, const char* str, std::uint32_t length, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buf, const char* str, std::uint32_t length, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buf, const char* str, std::uint32_t length, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64_t indexAddress);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex);
@@ -225,6 +229,29 @@ void testMarkers() {
expectFailure([] { sceAgcAcbSetMarker(nullptr, "frame", 0); });
}
void testMarkerSpans() {
Storage dcb;
Storage acb;
const char text[] = "framebuffer";
const auto* acbPush = sceAgcAcbPushMarkerSpan(&acb.buffer, text, 5, 0x00ff00u);
const auto* dcbPush = sceAgcDcbPushMarkerSpan(&dcb.buffer, text, 5, 0xff0000u);
check(acbPush == acb.words.data() && acbPush[0] == Agc::Command::Header(0x10, 3, 0x0bu << 2u), "ACB push marker span header mismatch");
check(std::strcmp(reinterpret_cast<const char*>(acbPush + 1), "frame") == 0, "ACB push marker span does not stop at its length");
check(dcbPush == dcb.words.data(), "DCB push marker span did not return its packet");
const auto* acbSet = sceAgcAcbSetMarkerSpan(&acb.buffer, nullptr, 0, 0);
const auto* dcbSet = sceAgcDcbSetMarkerSpan(&dcb.buffer, nullptr, 0, 0);
check(acbSet == acb.words.data() + 3 && dcbSet == dcb.words.data() + 3, "set marker span did not return its push packet");
check(acbSet[0] == Agc::Command::Header(0x10, 2, 0x0bu << 2u) && acbSet[1] == 0 && acbSet[2] == Agc::Command::Header(0x10, 2, 0x0cu << 2u), "ACB set marker span is not a push and pop pair");
check(dcb.words == acb.words && acb.buffer.cursor_up == acb.words.data() + 7, "ACB and DCB marker spans differ");
Storage plain;
sceAgcAcbPushMarker(&plain.buffer, "frame", 0);
check(std::equal(plain.words.begin(), plain.words.begin() + 3, acb.words.begin()), "marker span differs from the same text pushed whole");
expectFailure([] { sceAgcAcbPushMarkerSpan(nullptr, "frame", 5, 0); });
expectFailure([] { sceAgcAcbPushMarker(nullptr, reinterpret_cast<const char*>(std::uintptr_t{0x10}), 0); });
expectFailure([] { sceAgcDcbSetMarkerSpan(nullptr, "frame", 5, 0); });
expectFailure([&] { sceAgcDcbPushMarkerSpan(&dcb.buffer, nullptr, 4, 0); });
}
void testWorkloads() {
Storage dcb;
const std::uint32_t ids[2] = {3, 4};
@@ -822,6 +849,7 @@ int main(int argc, char** argv) {
testClearState();
testIndexedIndirectDraws();
testMarkers();
testMarkerSpans();
testWorkloads();
testWorkloadStreams();
testIndexBuffer();
+24 -19
View File
@@ -13,6 +13,7 @@
#include <vector>
extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes);
extern "C" int APS5_VABI sceAgcSetSemaphoreMemory(void* memory, std::uint64_t sizeInBytes);
extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label);
extern "C" std::uint32_t* APS5_VABI sceAgcCbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint32_t operation, std::uint32_t modifier, std::uint32_t value);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation);
@@ -22,12 +23,15 @@ namespace {
constexpr std::uint64_t blockBytes = 0x4000u;
constexpr std::uint64_t labelBytes = 32u;
constexpr std::uint64_t counterBytes = 8u;
constexpr std::uint64_t memoryBytes = 2u * blockBytes;
constexpr std::uint64_t storageBytes = 4u * blockBytes;
constexpr std::uint32_t labelCount = static_cast<std::uint32_t>(memoryBytes / labelBytes);
constexpr std::uint8_t guardByte = 0xa5u;
constexpr std::uint8_t counterByte = 0x5au;
constexpr std::uint8_t labelByte = 0x5au;
constexpr int InvalidAlignment = static_cast<int>(0x8a6c0002u);
constexpr int OutOfRange = static_cast<int>(0x8a6c000bu);
constexpr int AlreadyRegistered = static_cast<int>(0x8a6c0048u);
constexpr int NotRegistered = static_cast<int>(0x8a6c0049u);
struct AlignedRelease {
void operator()(std::uint8_t* memory) const { ::operator delete(memory, std::align_val_t{blockBytes}); }
@@ -64,28 +68,28 @@ void checkUnchanged(const std::vector<std::uint8_t>& before, const char* message
void fillStorage() {
std::fill(storage.get(), storage.get() + storageBytes, guardByte);
for (std::uint64_t label = 0; label < memoryBytes; label += labelBytes) {
std::fill(memory() + label, memory() + label + counterBytes, counterByte);
std::fill(memory() + label + counterBytes, memory() + label + labelBytes, std::uint8_t{0});
}
std::fill(memory(), memory() + memoryBytes, labelByte);
}
void testUnregistered() {
std::array<void*, 3> labels{storage.get(), storage.get(), storage.get()};
check(sceAgcGetSemaphoreLabel(0, &labels[1]) == static_cast<int>(0x8a6c0049u), "unregistered label error mismatch");
check(sceAgcGetSemaphoreLabel(0, nullptr) == static_cast<int>(0x8a6c0049u), "unregistered null label error mismatch");
check(sceAgcGetSemaphoreLabel(0, &labels[1]) == NotRegistered, "a lookup before registration was not rejected as unregistered");
check(sceAgcGetSemaphoreLabel(0, nullptr) == NotRegistered, "a null lookup before registration was not rejected as unregistered");
check(labels[0] == storage.get() && labels[1] == storage.get() && labels[2] == storage.get(), "a rejected semaphore label changed the output");
}
void testRegistrationRejections() {
const auto before = snapshot();
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, memoryBytes); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), 0); });
check(sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes) == static_cast<int>(0x8a6c0002u), "unaligned semaphore memory error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory() + 1, memoryBytes) == InvalidAlignment, "unaligned memory was not rejected");
check(sceAgcSetAmmSemaphoreMemory(memory() + labelBytes, blockBytes) == InvalidAlignment, "memory aligned only to a label was not rejected");
check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes - 1u) == InvalidAlignment, "an unaligned size was not rejected");
check(sceAgcSetAmmSemaphoreMemory(memory(), blockBytes + labelBytes) == InvalidAlignment, "a size aligned only to a label was not rejected");
check(sceAgcSetSemaphoreMemory(memory() + 1, blockBytes) == InvalidAlignment, "unaligned memory was not rejected by the non-AMM call");
check(sceAgcSetSemaphoreMemory(memory(), 0x40000000u + blockBytes) == OutOfRange, "a size above 1 GiB was not rejected by the non-AMM call");
check(sceAgcSetSemaphoreMemory(memory() + 1, 0x40000000u + blockBytes) == OutOfRange, "the size limit was not checked before the memory alignment");
check(sceAgcSetSemaphoreMemory(memory(), blockBytes - 1u) == InvalidAlignment, "the size alignment was not checked first");
check(sceAgcSetAmmSemaphoreMemory(nullptr, 0) == 0, "an empty registration was rejected");
expectFailure([] {
const auto top = std::numeric_limits<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u);
sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(top), memoryBytes);
@@ -98,7 +102,7 @@ void testRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x77});
const auto before = snapshot();
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == 0, "registering the AMM semaphore memory failed");
check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the semaphore counters");
check(std::all_of(memory(), memory() + memoryBytes, [](std::uint8_t byte) { return byte == 0; }), "registration did not clear the whole semaphore memory");
check(std::equal(before.begin(), before.begin() + blockBytes, storage.get()), "registration wrote before the semaphore memory");
check(std::equal(before.begin() + blockBytes + memoryBytes, before.end(), memory() + memoryBytes), "registration wrote past the semaphore memory");
}
@@ -115,7 +119,7 @@ void testLabels() {
void testLabelRejections() {
for (const std::uint32_t index : {labelCount, labelCount + 1u, std::numeric_limits<std::uint32_t>::max()}) {
void* label = storage.get();
check(sceAgcGetSemaphoreLabel(index, &label) == static_cast<int>(0x8a6c000bu), "out-of-range label error mismatch");
check(sceAgcGetSemaphoreLabel(index, &label) == OutOfRange, "a label past the semaphore memory was not rejected");
check(label == storage.get(), "a rejected semaphore label changed the output");
}
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
@@ -124,9 +128,10 @@ void testLabelRejections() {
void testRepeatedRegistration() {
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c});
const auto before = snapshot();
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
check(sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
check(sceAgcSetAmmSemaphoreMemory(nullptr, 0) == static_cast<int>(0x8a6c0048u), "repeated registration error mismatch");
check(sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes) == AlreadyRegistered, "a repeated registration was not rejected");
check(sceAgcSetAmmSemaphoreMemory(storage.get() + 1, blockBytes) == AlreadyRegistered, "registration was not checked before alignment");
check(sceAgcSetSemaphoreMemory(storage.get(), blockBytes) == AlreadyRegistered, "the non-AMM call did not share the registration");
check(sceAgcSetAmmSemaphoreMemory(nullptr, 0) == AlreadyRegistered, "an empty registration was not rejected once memory is registered");
checkUnchanged(before, "a repeated registration changed memory");
void* label = nullptr;
check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory");
+205 -26
View File
@@ -142,28 +142,6 @@ add_custom_command(
VERBATIM
)
set(agcDepthSampleShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/DepthSample.comp)
foreach(agcDepthSampleKind IN ITEMS Single Multisample)
string(TOUPPER ${agcDepthSampleKind} agcDepthSampleSymbol)
if(agcDepthSampleKind STREQUAL "Multisample")
set(agcDepthSampleMultisampled 1)
else()
set(agcDepthSampleMultisampled 0)
endif()
set(agcDepthSampleShaderSpv ${agcTextureDetileGeneratedDir}/DepthSample${agcDepthSampleKind}.spv)
set(agcDepthSampleShaderHeader ${agcTextureDetileGeneratedDir}/DepthSample${agcDepthSampleKind}_spv.h)
add_custom_command(
OUTPUT "${agcDepthSampleShaderHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -DMULTISAMPLED=${agcDepthSampleMultisampled} -o "${agcDepthSampleShaderSpv}" "${agcDepthSampleShaderSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcDepthSampleShaderSpv} -DOUTPUT=${agcDepthSampleShaderHeader}
-DSYMBOL=DEPTH_SAMPLE_${agcDepthSampleSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcDepthSampleShaderSource}" glslang-standalone
VERBATIM
)
list(APPEND agcDepthSampleShaderHeaders ${agcDepthSampleShaderHeader})
endforeach()
set(agcSampleCounterShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleCounter.comp)
set(agcSampleCounterShaderSpv ${agcTextureDetileGeneratedDir}/SampleCounter.spv)
set(agcSampleCounterShaderHeader ${agcTextureDetileGeneratedDir}/SampleCounter_spv.h)
@@ -329,6 +307,8 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderDeviceProfile.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Pm4.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/SubmissionCapture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/FrameCapture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/FrameRecorder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/FloatFlushProbe.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp
@@ -379,7 +359,6 @@ set(agcDriverSources
${agcReadbackHeaders}
${agcMultisampleUploadHeader}
${agcCmaskClearShaderHeader}
${agcDepthSampleShaderHeaders}
${agcMeshArgumentsShaderHeader}
${agcTextureDetileImageShaderHeader}
${agcTextureRepackShaderHeader}
@@ -441,6 +420,7 @@ target_link_libraries(libSceAgcDriver PRIVATE
libc
libkernel
jpeg_turbo
libzstd_static
${CMAKE_DL_LIBS}
)
if(APS5_ENABLE_TRACY)
@@ -508,6 +488,18 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_frame_capture_tests tests/FrameCapture.cpp Execution/src/FrameCapture.cpp)
target_include_directories(agc_frame_capture_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_frame_capture_tests PRIVATE libc libzstd_static)
set_target_properties(agc_frame_capture_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_frame_capture_tests)
add_test(NAME agc_frame_capture COMMAND agc_frame_capture_tests)
set_tests_properties(agc_frame_capture PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_frame_capture PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_write_watch_coverage_tests tests/WriteWatchCoverage.cpp)
target_include_directories(agc_write_watch_coverage_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_write_watch_coverage_tests PRIVATE libc)
@@ -573,6 +565,42 @@ if(BUILD_TESTING)
set_tests_properties(agc_handoff_spin PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_capture_trace_tests tests/CaptureTrace.cpp)
target_include_directories(agc_capture_trace_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_capture_trace_tests PRIVATE libc)
configure_windows_unwind(agc_capture_trace_tests)
set(agcCaptureTraceDirectory ${CMAKE_CURRENT_BINARY_DIR}/capture_trace_tests)
file(MAKE_DIRECTORY "${agcCaptureTraceDirectory}/default")
file(MAKE_DIRECTORY "${agcCaptureTraceDirectory}/explicit")
file(MAKE_DIRECTORY "${agcCaptureTraceDirectory}/failure")
file(MAKE_DIRECTORY "${agcCaptureTraceDirectory}/best-effort")
file(MAKE_DIRECTORY "${agcCaptureTraceDirectory}/disabled")
set(agcCaptureTraceExplicitPath "${agcCaptureTraceDirectory}/explicit/trace.log")
set(agcCaptureTraceFailurePath "${agcCaptureTraceDirectory}/failure/missing-parent/trace.log")
set(agcCaptureTraceBestEffortPath "${agcCaptureTraceDirectory}/best-effort/trace.log")
add_test(NAME agc_capture_trace_path_default COMMAND agc_capture_trace_tests path-default)
add_test(NAME agc_capture_trace_path_empty COMMAND agc_capture_trace_tests path-empty)
set_tests_properties(agc_capture_trace_path_empty PROPERTIES ENVIRONMENT "APS5_CAPTURE_TRACE_PATH=")
add_test(NAME agc_capture_trace_path_explicit COMMAND agc_capture_trace_tests path-explicit)
set_tests_properties(agc_capture_trace_path_explicit PROPERTIES ENVIRONMENT "APS5_CAPTURE_TRACE_PATH=${agcCaptureTraceExplicitPath}")
add_test(NAME agc_capture_trace_explicit_writes COMMAND agc_capture_trace_tests explicit-writes)
set_tests_properties(agc_capture_trace_explicit_writes PROPERTIES ENVIRONMENT "APS5_CAPTURE_TRACE=1;APS5_CAPTURE_TRACE_PATH=${agcCaptureTraceExplicitPath}")
add_test(NAME agc_capture_trace_default_writes COMMAND agc_capture_trace_tests default-writes)
set_tests_properties(agc_capture_trace_default_writes PROPERTIES ENVIRONMENT "APS5_CAPTURE_TRACE=1" WORKING_DIRECTORY "${agcCaptureTraceDirectory}/default")
add_test(NAME agc_capture_trace_open_failure COMMAND agc_capture_trace_tests open-failure)
set_tests_properties(agc_capture_trace_open_failure PROPERTIES ENVIRONMENT "APS5_CAPTURE_TRACE=1;APS5_CAPTURE_TRACE_PATH=${agcCaptureTraceFailurePath}" WORKING_DIRECTORY "${agcCaptureTraceDirectory}/failure")
add_test(NAME agc_capture_trace_best_effort_overflow COMMAND agc_capture_trace_tests best-effort-overflow)
set_tests_properties(agc_capture_trace_best_effort_overflow PROPERTIES ENVIRONMENT "APS5_CAPTURE_TRACE=1;APS5_CAPTURE_TRACE_PATH=${agcCaptureTraceBestEffortPath}" WORKING_DIRECTORY "${agcCaptureTraceDirectory}/best-effort")
add_test(NAME agc_capture_trace_disabled COMMAND agc_capture_trace_tests disabled)
set_tests_properties(agc_capture_trace_disabled PROPERTIES WORKING_DIRECTORY "${agcCaptureTraceDirectory}/disabled")
if(WIN32)
set_tests_properties(agc_capture_trace_path_default agc_capture_trace_path_empty
agc_capture_trace_path_explicit agc_capture_trace_explicit_writes
agc_capture_trace_default_writes agc_capture_trace_open_failure agc_capture_trace_best_effort_overflow
agc_capture_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(WIN32)
@@ -771,7 +799,13 @@ if(BUILD_TESTING)
set_tests_properties(agc_write_tracking_metadata_commit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_resident_direct_remap COMMAND agc_write_tracking_tests --resident-direct-remap)
set_tests_properties(agc_resident_direct_remap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20)
set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap PROPERTIES ENVIRONMENT_MODIFICATION
add_test(NAME agc_rewatched_direct_shared COMMAND agc_write_tracking_tests --rewatched-direct-shared)
set_tests_properties(agc_rewatched_direct_shared PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20)
add_test(NAME agc_import_alias_shared COMMAND agc_write_tracking_tests --import-alias-shared)
set_tests_properties(agc_import_alias_shared PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20)
add_test(NAME agc_compared_shared_generations COMMAND agc_write_tracking_tests --compared-shared-generations)
set_tests_properties(agc_compared_shared_generations PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20)
set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap agc_rewatched_direct_shared agc_import_alias_shared agc_compared_shared_generations PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@@ -2141,6 +2175,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_depth_plane_sampling_evicting COMMAND agc_driver_depth_plane_sampling_tests --evicting)
set_tests_properties(agc_driver_depth_plane_sampling_evicting PROPERTIES ENVIRONMENT "APS5_DEPTH_PLANE_COPY_MIB=1" SKIP_RETURN_CODE 77 TIMEOUT 120)
if(WIN32)
set_tests_properties(agc_driver_depth_plane_sampling_evicting PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp)
@@ -2166,6 +2206,9 @@ if(NOT ANYPS5_AGC_EXECUTION_TESTS OR "PooledBufferRange" IN_LIST ANYPS5_AGC_EXEC
endif()
target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber)
if(ANYPS5_ENABLE_SPIRV_TOOLS)
target_link_libraries(agc_driver_optimize_on_fiber_tests PRIVATE libSceFiber shader_recompiler_agc_driver)
endif()
target_link_libraries(agc_driver_rect_list_corner_tests PRIVATE shader_recompiler_agc_driver)
target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel)
@@ -2182,6 +2225,24 @@ target_include_directories(agc_driver_image_table_cache_bounds_tests PRIVATE $<T
target_include_directories(agc_driver_draw_cache_relocation_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
if(BUILD_TESTING)
add_test(NAME agc_submit_progress COMMAND agc_driver_submit_progress_tests)
set_tests_properties(agc_submit_progress PROPERTIES
ENVIRONMENT "APS5_SUBMIT_PROGRESS=1;ANYPS5_REQUIRE_VULKAN=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_submit_progress PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAgcDriver>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_submit_progress_no_timeline COMMAND agc_driver_submit_progress_tests --fallback-only)
set_tests_properties(agc_submit_progress_no_timeline PROPERTIES
ENVIRONMENT "APS5_SUBMIT_PROGRESS=1;APS5_NO_TIMELINE=1;ANYPS5_REQUIRE_VULKAN=1"
TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_submit_progress_no_timeline PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAgcDriver>;PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(BUILD_TESTING)
add_test(NAME agc_driver_store_pending_identity COMMAND agc_driver_store_over_untracked_tests --pending-identity)
set_tests_properties(agc_driver_store_pending_identity PROPERTIES
@@ -2240,6 +2301,7 @@ if(BUILD_TESTING)
endif()
endif()
target_include_directories(agc_driver_native_multisample_depth_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_partially_resident_snapshot_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
foreach(agcReadbackTestKind IN ITEMS Vertex Uint Depth)
string(TOUPPER ${agcReadbackTestKind} agcReadbackTestSymbol)
@@ -2288,6 +2350,8 @@ target_include_directories(agc_driver_packed_unorm4444_multisample_partial_write
target_include_directories(agc_driver_color1555_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_color_logic_op_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_color565_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_depth_range_remap_draw_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
@@ -2478,10 +2542,21 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_sparse_indirect_indices_unreadable COMMAND agc_driver_sparse_indirect_indices_tests --unreadable)
set_tests_properties(agc_driver_sparse_indirect_indices agc_driver_sparse_indirect_indices_unreadable PROPERTIES
add_test(NAME agc_driver_sparse_indirect_indices_no_size COMMAND agc_driver_sparse_indirect_indices_tests --no-size)
add_test(NAME agc_driver_sparse_indirect_indices_no_size_unreadable COMMAND agc_driver_sparse_indirect_indices_tests --no-size-unreadable)
add_test(NAME agc_driver_sparse_indirect_indices_no_size_unreadable_record COMMAND agc_driver_sparse_indirect_indices_tests --no-size-unreadable-record)
set_tests_properties(agc_driver_sparse_indirect_indices agc_driver_sparse_indirect_indices_unreadable agc_driver_sparse_indirect_indices_no_size agc_driver_sparse_indirect_indices_no_size_unreadable agc_driver_sparse_indirect_indices_no_size_unreadable_record PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_sparse_indirect_indices_unreadable PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_sparse_indirect_indices_unreadable agc_driver_sparse_indirect_indices_no_size agc_driver_sparse_indirect_indices_no_size_unreadable agc_driver_sparse_indirect_indices_no_size_unreadable_record PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_draw_cache_relocation_evicted COMMAND agc_driver_draw_cache_relocation_tests --evicted)
add_test(NAME agc_driver_draw_cache_relocation_kept COMMAND agc_driver_draw_cache_relocation_tests --kept)
set_tests_properties(agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_CACHE_ENTRIES=2" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_pixel_implicit_derivatives PROPERTIES TIMEOUT 30)
@@ -2595,6 +2670,9 @@ if(BUILD_TESTING)
add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint)
set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_driver_compared_shared_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --compared-shared)
set_tests_properties(agc_driver_compared_shared_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_registration_trace PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30)
@@ -2748,6 +2826,28 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set(agcDrawShaderFailureTraceDirectory ${CMAKE_CURRENT_BINARY_DIR}/draw_shader_failure_tests)
file(MAKE_DIRECTORY "${agcDrawShaderFailureTraceDirectory}/failure")
file(MAKE_DIRECTORY "${agcDrawShaderFailureTraceDirectory}/success")
file(MAKE_DIRECTORY "${agcDrawShaderFailureTraceDirectory}/disabled")
file(MAKE_DIRECTORY "${agcDrawShaderFailureTraceDirectory}/failure_correlation_disabled")
set_tests_properties(agc_driver_draw_shader_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_CAPTURE_TRACE=1;APS5_TRACE_SHADER_CORRELATION=1;APS5_CAPTURE_TRACE_PATH=${agcDrawShaderFailureTraceDirectory}/failure/trace.log" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_draw_shader_failure_successful COMMAND agc_driver_draw_shader_failure_tests successful-draw)
set_tests_properties(agc_driver_draw_shader_failure_successful PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_CAPTURE_TRACE=1;APS5_TRACE_SHADER_CORRELATION=1;APS5_CAPTURE_TRACE_PATH=${agcDrawShaderFailureTraceDirectory}/success/trace.log" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_draw_shader_failure_correlation_disabled COMMAND agc_driver_draw_shader_failure_tests disabled-correlation)
set_tests_properties(agc_driver_draw_shader_failure_correlation_disabled PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_CAPTURE_TRACE=1;APS5_CAPTURE_TRACE_PATH=${agcDrawShaderFailureTraceDirectory}/disabled/trace.log" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_draw_shader_failure_request_correlation_disabled COMMAND agc_driver_draw_shader_failure_tests request-failure-correlation-disabled)
set_tests_properties(agc_driver_draw_shader_failure_request_correlation_disabled PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_CAPTURE_TRACE=1;APS5_CAPTURE_TRACE_PATH=${agcDrawShaderFailureTraceDirectory}/failure_correlation_disabled/trace.log" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
file(MAKE_DIRECTORY "${agcDrawShaderFailureTraceDirectory}/materialization")
file(MAKE_DIRECTORY "${agcDrawShaderFailureTraceDirectory}/materialization_correlation_disabled")
add_test(NAME agc_driver_draw_shader_failure_materialization COMMAND agc_driver_draw_shader_failure_tests materialization-failure)
set_tests_properties(agc_driver_draw_shader_failure_materialization PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_CAPTURE_TRACE=1;APS5_TRACE_SHADER_CORRELATION=1;APS5_CAPTURE_TRACE_PATH=${agcDrawShaderFailureTraceDirectory}/materialization/trace.log" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_draw_shader_failure_materialization_correlation_disabled COMMAND agc_driver_draw_shader_failure_tests materialization-failure-correlation-disabled)
set_tests_properties(agc_driver_draw_shader_failure_materialization_correlation_disabled PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_CAPTURE_TRACE=1;APS5_CAPTURE_TRACE_PATH=${agcDrawShaderFailureTraceDirectory}/materialization_correlation_disabled/trace.log" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_draw_shader_failure agc_driver_draw_shader_failure_successful agc_driver_draw_shader_failure_correlation_disabled agc_driver_draw_shader_failure_request_correlation_disabled agc_driver_draw_shader_failure_materialization agc_driver_draw_shader_failure_materialization_correlation_disabled PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests)
set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests)
@@ -3004,6 +3104,13 @@ if(BUILD_TESTING)
endif()
endforeach()
endforeach()
add_test(NAME agc_driver_depth_plane_partial_store_unimported COMMAND agc_driver_depth_plane_partial_store_tests)
set_tests_properties(agc_driver_depth_plane_partial_store_unimported PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_HOST_IMPORT=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_depth_plane_partial_store_unimported PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_array_layer_target_cpu COMMAND agc_driver_array_layer_target_tests)
set_tests_properties(agc_driver_array_layer_target_cpu PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_HOST_IMPORT=1" TIMEOUT 30 SKIP_RETURN_CODE 77)
@@ -3217,6 +3324,18 @@ if(BUILD_TESTING)
endif()
endif()
add_executable(agc_frame_replay EXCLUDE_FROM_ALL
tools/FrameReplay.cpp
Execution/src/FrameCapture.cpp
)
target_include_directories(agc_frame_replay PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_frame_replay PRIVATE libSceAgcDriver libkernel libc libzstd_static)
if(WIN32)
target_link_options(agc_frame_replay PRIVATE LINKER:--image-base=0x500000000000)
endif()
set_target_properties(agc_frame_replay PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_frame_replay)
add_executable(agc_pm4_replay EXCLUDE_FROM_ALL
tools/Pm4Replay.cpp
tools/Pm4Analysis.cpp
@@ -3263,6 +3382,58 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_pm4_replay agc_driver_pm4_replay_tool PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(WIN32)
add_test_executable(agc_driver_frame_capture_eop_tests tests/FrameCaptureEop.cpp Execution/src/FrameCapture.cpp)
target_include_directories(agc_driver_frame_capture_eop_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_driver_frame_capture_eop_tests PRIVATE libSceAgcDriver libkernel libc libzstd_static)
set_target_properties(agc_driver_frame_capture_eop_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_frame_capture_eop_tests)
add_test(NAME agc_driver_frame_capture_eop COMMAND agc_driver_frame_capture_eop_tests)
set_tests_properties(agc_driver_frame_capture_eop PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-capture-eop;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=3"
TIMEOUT 120)
set_tests_properties(agc_driver_frame_capture_eop PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_driver_frame_replay_state_tests tests/FrameReplayState.cpp Execution/src/FrameCapture.cpp)
target_include_directories(agc_driver_frame_replay_state_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_driver_frame_replay_state_tests PRIVATE libSceAgcDriver libkernel libc libzstd_static)
set_target_properties(agc_driver_frame_replay_state_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_frame_replay_state_tests)
set(agcFrameReplayModes order tokens blocking)
if(WIN32)
list(APPEND agcFrameReplayModes zero shader shader_host)
endif()
foreach(mode IN LISTS agcFrameReplayModes)
set(capture "${CMAKE_CURRENT_BINARY_DIR}/frame-replay-${mode}")
add_test(NAME agc_driver_frame_replay_${mode} COMMAND agc_driver_frame_replay_state_tests ${mode} "${capture}")
set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES TIMEOUT 30)
if(mode MATCHES "^shader")
set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES ENVIRONMENT
"APS5_FRAME_CAPTURE=${capture};APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=1")
endif()
if(WIN32)
set_tests_properties(agc_driver_frame_replay_${mode} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
if(WIN32)
add_test_executable(agc_driver_frame_replay_tests tests/FrameReplay.cpp Execution/src/FrameCapture.cpp)
target_include_directories(agc_driver_frame_replay_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_driver_frame_replay_tests PRIVATE libSceAgc libSceAgcDriver libkernel libc libzstd_static)
set_target_properties(agc_driver_frame_replay_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_frame_replay_tests)
add_dependencies(agc_driver_frame_replay_tests agc_frame_replay)
add_test(NAME agc_driver_frame_replay COMMAND agc_driver_frame_replay_tests $<TARGET_FILE:agc_frame_replay>)
set_tests_properties(agc_driver_frame_replay PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_FRAME_CAPTURE=${CMAKE_CURRENT_BINARY_DIR}/frame-replay-capture;APS5_FRAME_CAPTURE_AT_FLIP=1;APS5_FRAME_CAPTURE_FRAMES=2"
TIMEOUT 120)
set_tests_properties(agc_driver_frame_replay PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_executable(agc_compute_pipeline_replay EXCLUDE_FROM_ALL tools/ComputePipelineReplay.cpp)
@@ -3513,6 +3684,14 @@ if(BUILD_TESTING)
set_tests_properties(scalar_call PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(ir_dump_memory_tests ${LIBS_INCLUDE_DIR}/tests/IrDumpMemory.cpp)
target_link_libraries(ir_dump_memory_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(ir_dump_memory_tests)
add_test(NAME ir_dump_memory COMMAND ir_dump_memory_tests)
if(WIN32)
set_tests_properties(ir_dump_memory PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(image_sample_gather_tests ${LIBS_INCLUDE_DIR}/tests/ImageSampleGather.cpp)
target_link_libraries(image_sample_gather_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(image_sample_gather_tests)
@@ -13,6 +13,12 @@
namespace AgcDriver {
inline const char* CaptureTracePath() {
const char* configuredPath = std::getenv("APS5_CAPTURE_TRACE_PATH");
if (configuredPath != nullptr && configuredPath[0] != '\0') return configuredPath;
return "capture-trace.log";
}
class CaptureTrace {
public:
static bool Enabled() {
@@ -30,6 +36,14 @@ public:
trace.append(std::string(line.data(), static_cast<std::size_t>(size)));
}
template<typename... TArgs>
static void LogBestEffort(const char* format, TArgs... args) noexcept {
try {
Log(format, args...);
} catch (...) {
}
}
private:
static CaptureTrace& instance() {
static CaptureTrace trace;
@@ -37,8 +51,13 @@ private:
}
CaptureTrace() {
const char* path = CaptureTracePath();
output.open(path, std::ios::binary);
if (!output.is_open()) {
std::fprintf(stderr, "Capture trace open failed for %s\n", path);
return;
}
output.exceptions(std::ios::badbit | std::ios::failbit);
output.open("capture-trace.log", std::ios::binary);
worker = std::thread([this] { run(); });
}
@@ -48,10 +67,11 @@ private:
stopping = true;
}
changed.notify_one();
worker.join();
if (worker.joinable()) worker.join();
}
void append(std::string line) {
if (!output.is_open()) return;
const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - start).count();
std::lock_guard lock(mutex);
if (pending.size() + line.size() + 64 > 16 * 1024 * 1024) throw std::runtime_error("Capture trace queue exceeded 16 MiB");
@@ -16,6 +16,17 @@
namespace AgcDriver {
struct DeviceDiagnostics {
bool config = false;
bool checkpoints = false;
VkDeviceDiagnosticsConfigFlagsNV flags = 0;
};
inline DeviceDiagnostics ChooseDeviceDiagnostics(bool requested, bool configSupported, bool checkpointsSupported) {
if (!requested || !configSupported) return {};
return {true, checkpointsSupported, VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_SHADER_DEBUG_INFO_BIT_NV | VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_RESOURCE_TRACKING_BIT_NV | VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_AUTOMATIC_CHECKPOINTS_BIT_NV | VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_SHADER_ERROR_REPORTING_BIT_NV};
}
inline const char* DeviceFaultAddressKind(VkDeviceFaultAddressTypeEXT type) {
switch (type) {
case VK_DEVICE_FAULT_ADDRESS_TYPE_NONE_EXT: return "none";
@@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP
#include <cstddef>
#include <cstdint>
#include <span>
#include <cstddef>
@@ -11,6 +12,14 @@ namespace AgcDriver {
void Submit(const Packet* packet, std::uint32_t queue);
struct AwaitedWait {
std::uint64_t address = 0;
std::uint64_t reference = 0;
std::uint64_t mask = 0;
std::uint32_t function = 0;
std::uint32_t bytes = 0;
};
}
extern "C" void AgcDriverPrepareCommandWrite_nid_no_patch(std::uint64_t address, std::size_t bytes);
@@ -21,6 +30,8 @@ extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix();
extern "C" void AgcDriverShutdown_nid_postfix();
extern "C" void AgcDriverRequestScreenshot_nid_postfix();
extern "C" void AgcDriverSuspendPoint_nid_postfix();
extern "C" void AgcDriverSerialProgress_nid_postfix(std::uint64_t* accepted, std::uint64_t* completed);
extern "C" std::size_t AgcDriverAwaitedWaits_nid_postfix(AgcDriver::AwaitedWait* waits, std::size_t capacity);
extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix();
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader);
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
@@ -2,6 +2,7 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRIVER_HPP
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/DeviceAccess.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Packets/PacketHistory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp"
@@ -64,6 +65,12 @@ public:
void SubmitFlip(const FlipInfo& info);
void SuspendPoint();
std::uint32_t ReserveEopFlip(const FlipInfo& info);
std::uint32_t ReserveEopFlipAt(const FlipInfo& info, std::uint32_t token);
std::vector<std::uint32_t> PendingEopFlipTokens();
std::pair<std::uint64_t, std::uint64_t> SerialProgress();
std::uint64_t FlipSubmissions() const;
std::vector<AwaitedWait> AwaitedWaits();
bool WaitIdleFor(std::chrono::milliseconds timeout);
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output);
void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output);
void AttachWindow(const PresentationWindow& window);
@@ -109,6 +116,7 @@ private:
Driver();
void rethrowFailure() const;
void checkStopping() const;
std::uint32_t reserveEopFlip(const FlipInfo& info, std::uint32_t token);
static bool& onWorkerThread();
static void copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words);
static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget);
@@ -126,9 +134,9 @@ private:
static void forgetUnfinishedWrites(QueueWorker& worker, std::span<const std::uint64_t> labelWrites);
static bool& completionDeferred();
void deferCompletion(const Submission& submission);
void submitOpenWork();
void submitOpenWork(const std::shared_ptr<SubmissionProgress>& progress = {});
void wakeQueueZeroForCompletions();
void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites);
void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites, const std::shared_ptr<SubmissionProgress>& progress);
static bool waitFree(const Submission& submission);
bool queue0Before(std::uint64_t received) const;
bool orderReleased(std::uint32_t queue, std::uint64_t received) const;
@@ -183,13 +191,13 @@ private:
std::shared_ptr<DrawDecode> decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context);
void resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile);
void lookupDraw(const Submission& submission, const std::shared_ptr<VulkanDevice>& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr<DrawEntry>& entry, std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, const std::shared_ptr<DrawEntry>& dataEntry, DrawDataCandidates& dataCandidates, std::vector<StageDataHit>& stageData);
std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics);
std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, std::size_t packetOffset, std::uint64_t traceToken, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics);
static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads);
void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<StageDataHit>& stageData, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey, bool shapeNeedsEntry = false);
static bool drawPrecheck();
std::optional<DrawVerdict> precheckDraw(const QueueState& queue, const Submission& submission, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect);
std::optional<DrawVerdict> precheckDraw(const QueueState& queue, const Submission& submission, std::size_t packetOffset, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect);
static std::uint32_t drawUserWord(const DrawProgram& program, std::int32_t sgpr);
DrawVerdict draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::string& rejected);
DrawVerdict draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::size_t packetOffset, std::string& rejected);
DrawVerdict finishDraw(DrawTail& tail, PerformanceTimer& timing);
void addDriverPhases(DispatchClass which, const std::array<double, DriverPhaseCount>& ms, bool hit, bool validated);
static PendingDispatchPhases& pendingDispatchPhases();
@@ -262,7 +270,7 @@ private:
void noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue);
void noteDrawWriters(std::span<const Graphics::CompiledShader> stages, std::uint32_t queue);
static std::vector<std::pair<std::uint64_t, std::uint64_t>> drawWriteRanges(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> stages);
void commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey);
void commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace = {});
bool labelPacketQueues(std::span<const std::uint32_t> packet, std::uint32_t opcode);
bool enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue);
bool enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state);
@@ -367,6 +375,14 @@ private:
std::shared_ptr<FrameTiming> pendingFrameTiming;
std::atomic<std::uint64_t> flipsCounted{0};
struct AwaitedSlot {
std::atomic<std::uint64_t> address{0};
std::atomic<std::uint64_t> reference{0};
std::atomic<std::uint64_t> mask{0};
std::atomic<std::uint64_t> shape{0};
};
std::array<AwaitedSlot, SubmissionQueueEnd - 0x20 + 1> awaitedSlots{};
void noteWaitFields(std::uint32_t queue, std::span<const std::uint32_t> packet);
std::atomic<std::uint64_t> flipSerial{0};
std::atomic<std::uint64_t> flipBatchesUnsignaled{0};
@@ -400,7 +416,7 @@ private:
std::unordered_set<std::uint64_t> traceShapes, traceKeysSeen, traceKeysEvicted;
std::list<std::uint64_t> drawOrder;
std::unordered_map<std::uint64_t, std::shared_ptr<const DrawRelocation>> drawRelocations;
std::unordered_map<std::uint64_t, std::shared_ptr<DrawEntry>> drawAliases;
std::unordered_map<std::uint64_t, std::uint64_t> drawAliases;
DrawRelocationPending drawRelocationPending;
std::unordered_map<std::uint64_t, DrawRelocationTrial> drawRelocationTrials;
DrawRelocationCounters drawRelocationCounters;
@@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionProgress.hpp"
#include <atomic>
#include <chrono>
#include <cstddef>
@@ -22,6 +23,7 @@ struct Submission {
std::uint64_t serial;
std::uint32_t queue;
std::vector<std::uint32_t> commands;
std::shared_ptr<SubmissionProgress> progress;
std::shared_ptr<const ShaderRegistry> shaders;
std::map<std::size_t, std::shared_ptr<IFlipRequest>> flips;
@@ -0,0 +1,44 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_QUEUES_SUBMISSIONPROGRESS_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_QUEUES_SUBMISSIONPROGRESS_HPP
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <memory>
#include <mutex>
#include <utility>
namespace AgcDriver { class VulkanDevice; }
namespace AgcDriver::DriverDetail {
class SubmissionProgress {
public:
using Clock = std::chrono::steady_clock;
using Ready = std::pair<std::shared_ptr<VulkanDevice>, std::uint64_t>;
void Publish(std::shared_ptr<VulkanDevice> device, std::uint64_t epoch) {
{
std::lock_guard lock(mutex);
result = {std::move(device), epoch};
ready = true;
}
changed.notify_one();
}
Ready WaitUntil(Clock::time_point deadline) {
std::unique_lock lock(mutex);
if (!changed.wait_until(lock, deadline, [&] { return ready; })) return {};
return result;
}
private:
std::mutex mutex;
std::condition_variable changed;
Ready result;
bool ready = false;
};
}
#endif
@@ -175,6 +175,8 @@ struct RegisteredShaderState {
Registers shader;
Registers context;
Registers userConfig;
bool operator==(const RegisteredShaderState&) const = default;
};
struct ShaderSnapshot {
@@ -210,6 +212,9 @@ std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnosti
std::uint64_t NullPixelProgramAddress();
ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device);
std::uint64_t DepthCopyPixelProgramAddress(bool multisampled);
std::uint32_t DepthCopyPixelUserWords(bool multisampled);
ShaderSnapshot PrepareDepthCopyPixelProgram(bool multisampled);
std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const ShaderSnapshot& snapshot);
std::optional<ShaderRecompiler::ShaderFloatMode> DispatchFloatMode(const ShaderSnapshot& snapshot, const Registers& live);
std::shared_ptr<const ShaderSnapshot> RegisteredHeader(const ShaderRegistry& registry, const Shader* shader);
@@ -0,0 +1,138 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FRAMECAPTURE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FRAMECAPTURE_HPP
#include "SceShaders.hpp"
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace AgcDriver {
enum class FrameCallKind : std::uint32_t {
RegisterShader = 1,
ResolveShaderAbi,
ResolveGraphicsAbi,
ResolveGraphicsStagesAbi,
RegisterVideoOutput,
SetTessellationFactorRing,
Submit,
Flip,
ReserveEopFlip,
Suspend,
Memory,
FrameStart,
End,
DisplayBuffer,
UnregisterVideoOutput,
};
struct FrameCall {
FrameCallKind kind = FrameCallKind::End;
std::uint64_t ordinal = 0;
std::uint64_t timeNs = 0;
std::uint64_t thread = 0;
std::uint64_t accepted = 0;
std::uint64_t completed = 0;
std::uint32_t queue = 0;
std::uint32_t flags = 0;
bool cpuFlip = false;
std::uint64_t address = 0;
std::uint64_t size = 0;
std::vector<std::uint64_t> values;
std::vector<ShaderRegister> context;
std::vector<ShaderRegister> primitive;
};
enum class FrameMemoryKind : std::uint32_t { Range = 1, Snapshot, Delta, Reference };
enum class FrameRangeKind : std::uint32_t { Watched = 1, Unwatched, Section, Alias, File, Host };
struct FrameRangeAccess {
static constexpr std::uint32_t Readable = 1;
static constexpr std::uint32_t Writable = 2;
static constexpr std::uint32_t Gpu = 4;
};
struct FrameMemoryChunk {
FrameMemoryKind kind = FrameMemoryKind::Range;
FrameRangeKind rangeKind = FrameRangeKind::Watched;
std::uint32_t protection = 0;
std::uint32_t access = 0;
std::uint64_t group = 0;
std::uint64_t completed = 0;
std::uint64_t address = 0;
std::uint64_t bytes = 0;
std::uint64_t compressedBytes = 0;
std::uint64_t offset = 0;
};
class FrameCaptureWriter {
public:
static constexpr std::string_view CallsName = "calls.bin";
static constexpr std::string_view MemoryName = "memory.bin";
static constexpr std::string_view HeaderName = "capture.json";
static constexpr std::string_view CallsMagic = "aps5-frame-calls 1\n";
static constexpr std::string_view MemoryMagic = "aps5-frame-memory 1\n";
static constexpr std::size_t BlockBytes = 64u * 1024u;
static constexpr std::size_t ChunkBytes = 4u * 1024u * 1024u;
explicit FrameCaptureWriter(const std::filesystem::path& directory, int level = 1);
FrameCaptureWriter(const FrameCaptureWriter&) = delete;
FrameCaptureWriter& operator=(const FrameCaptureWriter&) = delete;
void WriteCall(const FrameCall& call);
void WriteRange(std::uint64_t address, std::uint64_t bytes, FrameRangeKind kind, std::uint32_t protection, std::uint32_t access);
std::uint64_t WriteSnapshot(std::uint64_t address, std::span<const std::byte> bytes);
void WriteDelta(std::uint64_t group, std::uint64_t completed, std::uint64_t address, std::span<const std::byte> bytes);
void WriteReference(std::uint64_t index, std::uint64_t address, std::span<const std::byte> pixels);
void Finish(std::string_view header);
std::uint64_t RawBytes() const;
std::uint64_t StoredBytes() const;
const std::filesystem::path& Directory() const;
private:
void writeChunk(const FrameMemoryChunk& chunk, std::span<const std::byte> payload);
std::vector<std::byte> compress(std::span<const std::byte> bytes) const;
std::filesystem::path directory;
int level;
mutable std::mutex mutex;
std::ofstream calls;
std::ofstream memory;
std::uint64_t rawBytes = 0;
std::uint64_t storedBytes = 0;
bool finished = false;
};
class FrameCaptureReader {
public:
explicit FrameCaptureReader(const std::filesystem::path& directory);
const std::vector<FrameCall>& Calls() const;
const std::vector<FrameMemoryChunk>& Chunks() const;
const std::string& Header() const;
bool Truncated() const;
void Read(const FrameMemoryChunk& chunk, std::span<std::byte> destination) const;
std::vector<std::byte> Read(const FrameMemoryChunk& chunk) const;
private:
std::filesystem::path directory;
std::vector<FrameCall> calls;
std::vector<FrameMemoryChunk> chunks;
std::string header;
bool truncated = false;
};
std::string_view FrameCallName(FrameCallKind kind);
bool AllZero(std::span<const std::byte> bytes);
}
#endif
@@ -0,0 +1,113 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FRAMERECORDER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FRAMERECORDER_HPP
#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp"
#include "prx/libSceAgcDriver/Execution/include/FrameCapture.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "SceShaders.hpp"
#include "SceTypes.hpp"
#include <chrono>
#include <cstdint>
#include <deque>
#include <filesystem>
#include <map>
#include <memory>
#include <mutex>
#include <span>
#include <string>
#include <vector>
namespace AgcDriver {
class FrameRecorder {
public:
static constexpr std::uint64_t DefaultFrames = 10;
static constexpr std::size_t ReservationHistory = 256;
static constexpr std::size_t SnapshotPieceBytes = 64u * 1024u * 1024u;
static constexpr std::uint64_t MinimumFreeBytes = std::uint64_t{4} << 30;
static constexpr std::size_t HostPageBytes = 4096;
static FrameRecorder* Active();
void RegisterShader(const Shader* shader);
void ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
void ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType);
void RegisterVideoOutput(std::uint32_t handle);
void UnregisterVideoOutput(std::uint32_t handle);
void Submit(const Packet& packet, std::uint32_t queue);
void Flip(const FlipInfo& info);
void ReserveEopFlip(const FlipInfo& info, std::uint32_t token);
void Suspend();
void PresentBuffer(const DisplayBuffer& buffer);
void FrameBoundary();
private:
enum class State { Waiting, Recording, Done };
struct Range {
std::uint64_t address = 0;
std::uint64_t bytes = 0;
FrameRangeKind kind = FrameRangeKind::Watched;
std::uint32_t protection = 0;
std::uint32_t access = 0;
bool readable = false;
};
FrameRecorder(std::filesystem::path directory, std::filesystem::path trigger, std::uint64_t atBoundary, std::uint64_t frames);
struct Span {
std::uint64_t address = 0;
std::uint64_t bytes = 0;
std::uint64_t since = 0;
};
FrameCall make(FrameCallKind kind);
void record(FrameCall call, bool prologueCall, std::span<const Shader* const> shaders = {});
void emitDelta();
void emitShaderDelta(std::span<const Shader* const> shaders);
void writeDelta(std::span<const Span> spans, std::span<const std::uint64_t> hostPages, std::span<const Range> added = {});
bool triggered();
void start();
void finish();
void snapshot(const Range& range);
void writeReferenceFrames();
bool abandonIfDiskLow();
std::string header() const;
std::vector<Range> committedRanges();
std::vector<Range> newRanges();
std::filesystem::path directory;
std::filesystem::path trigger;
std::uint64_t atBoundary = 0;
std::uint64_t frames = DefaultFrames;
std::mutex mutex;
State state = State::Waiting;
std::unique_ptr<FrameCaptureWriter> writer;
std::vector<FrameCall> prologue;
std::deque<FrameCall> reservations;
std::vector<Range> ranges;
std::map<std::uint64_t, std::uint64_t> shaderMarks;
std::map<std::uint64_t, std::uint64_t> hostPageHashes;
std::mutex displayMutex;
std::map<std::uint64_t, DisplayBuffer> displayBuffers;
std::uint64_t ordinal = 0;
std::uint64_t boundaries = 0;
std::uint64_t startBoundary = 0;
std::uint64_t recordedFrames = 0;
std::uint64_t generation = 0;
std::uint64_t deltaGroups = 0;
std::uint64_t deltaBytes = 0;
std::uint64_t snapshotBytes = 0;
std::uint64_t recordedCalls = 0;
std::uint64_t startAttempts = 0;
std::uint64_t unreadableBytes = 0;
std::uint64_t allocationsGeneration = 0;
std::uint64_t lateRanges = 0;
std::vector<std::string> warnings;
std::chrono::steady_clock::time_point origin;
std::chrono::steady_clock::time_point startedAt;
};
}
#endif
@@ -81,6 +81,8 @@ struct UnchangedQuery {
bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
bool UnchangedSinceAll(std::span<const UnchangedQuery> queries);
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges);
std::uint64_t WriteGeneration();
std::uint64_t CollectWrittenBlocks(std::uint64_t address, std::size_t bytes, std::uint64_t since, std::vector<std::pair<std::uint64_t, std::uint64_t>>& written);
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges, std::uint64_t generation);
// Returns the generation the blocks were stamped with (0 when the arena is not write-watched):
// UnchangedSince(range, it) holds until the next store over the range.
@@ -46,6 +46,7 @@ struct DrawParameters {
std::uint32_t drawIndexLocation;
bool drawIndexEnabled;
std::uint32_t indxOffset;
bool indexLimitKnown = true;
enum class Rule : std::uint8_t { InPlace, Constant };
Rule vertexRule = Rule::Constant;
Rule instanceRule = Rule::Constant;
@@ -272,7 +272,7 @@ struct QueueState {
std::uint64_t indexBase = 0;
std::uint64_t drawIndirectBase = 0;
std::uint64_t dispatchIndirectBase = 0;
std::uint32_t indexBufferSize = 0;
std::optional<std::uint32_t> indexBufferSize;
std::uint32_t indexType = 0;
std::uint32_t instanceCount = 1;
std::vector<std::string> markers;
@@ -55,6 +55,7 @@ public:
extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::FlipInfo& info);
extern "C" void AgcDriverSubmitFlip_nid_postfix(const AgcDriver::FlipInfo& info);
extern "C" std::uint32_t AgcDriverReserveEopFlipAt_nid_postfix(const AgcDriver::FlipInfo& info, std::uint32_t token);
extern "C" void AgcDriverRegisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output);
extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle, const std::shared_ptr<AgcDriver::IVideoOutput>& output);
@@ -105,6 +105,8 @@ public:
bool CanWaitUnlocked() const;
std::uint64_t SubmitAndEpoch();
void WaitRecorded(std::uint64_t serial);
std::uint64_t SubmittedEpoch() const;
bool WaitRecordedFor(std::uint64_t serial, std::uint64_t timeoutNanos);
void ReapRecorded(std::uint64_t serial);
// Completes the batches that already finished (non-blocking); under the mutex.
void ReapRecorded();
@@ -332,17 +334,17 @@ public:
static void NoteRecipe(RecipeEvent event, RecipeKind kind);
enum class DrawRecipePrecheck : std::size_t { NoRecipe, Device };
static void NoteDrawRecipeMiss(DrawRecipePrecheck miss);
using DrawTraceContext = Graphics::DrawTraceContext;
// `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, direct draw
// built for its draw-cache entry (design_cpu_final M8); null otherwise.
void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr);
void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr);
bool DrawInputsInPlace() const;
std::optional<std::string> KnownDrawRejection(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> shaders) const;
void ColorMetadataPass(const Graphics::ColorMetadataPass& pass);
void DepthCopyPass(const Graphics::DepthCopyPass& pass);
// A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after
// the packet's labels; Rebuild when the recipe's device is not this one or DrawWithRecipe
// missed (nothing recorded: the caller runs Draw with the hit's stages and re-attaches).
RecipeOutcome DrawFromRecipe(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe);
RecipeOutcome DrawFromRecipe(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const DrawTraceContext* trace = nullptr);
// The indirect draw features the device enabled, for the driver's decision whether an indirect
// draw's records can be read by the GPU (Graphics::Draw) or must be resolved on the CPU.
struct IndirectDrawSupport {
@@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp"
@@ -9,6 +10,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "prx/libc/include/HostThreadLocal.hpp"
#include <atomic>
@@ -19,12 +21,106 @@
#include <cstdio>
#include <unordered_set>
#include <cstdlib>
#include <algorithm>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <unordered_map>
#include <atomic>
#include <exception>
#include <mutex>
#include <set>
#include <string>
#include <string_view>
#include <typeinfo>
namespace AgcDriver::DriverDetail {
namespace {
bool TraceShaderCorrelation() {
static const bool enabled = std::getenv("APS5_TRACE_SHADER_CORRELATION") != nullptr;
return enabled && AgcDriver::CaptureTrace::Enabled();
}
std::atomic<std::uint64_t>& NextDrawTraceToken() {
static std::atomic<std::uint64_t> next{0};
return next;
}
std::string ExceptionReason(const std::exception_ptr& failure) {
try {
if (failure != nullptr) std::rethrow_exception(failure);
} catch (const std::exception& error) {
return error.what();
} catch (...) {
}
return "unknown exception";
}
const char* ExceptionType(const std::exception_ptr& failure) {
try {
if (failure != nullptr) std::rethrow_exception(failure);
} catch (const std::exception& error) {
return typeid(error).name();
} catch (...) {
}
return "unknown";
}
void TraceMaterializationFailure(std::uint64_t token, const Submission& submission, std::size_t packetOffset, const char* phase, std::size_t completedSubdraws, const std::exception_ptr& failure) {
if (token == 0) return;
try {
const auto reason = ExceptionReason(failure);
AgcDriver::CaptureTrace::Log("draw-materialization-failed token=%llu submission=%llu queue=%x offset=%zu phase=%s exception=%s reason=%.256s completedSubdraws=%zu",
static_cast<unsigned long long>(token), static_cast<unsigned long long>(submission.serial), submission.queue, packetOffset, phase, ExceptionType(failure), reason.c_str(), completedSubdraws);
} catch (...) {
}
}
void TraceSubmissionFailure(std::uint64_t token, const Submission& submission, std::size_t packetOffset, std::size_t completedSubdraws, const std::exception_ptr& failure) {
if (token == 0) return;
try {
const auto reason = ExceptionReason(failure);
AgcDriver::CaptureTrace::Log("draw-submission-failed token=%llu submission=%llu queue=%x offset=%zu exception=%s reason=%.256s completedSubdraws=%zu",
static_cast<unsigned long long>(token), static_cast<unsigned long long>(submission.serial), submission.queue, packetOffset, ExceptionType(failure), reason.c_str(), completedSubdraws);
} catch (...) {
}
}
void TraceDrawAttemptFailure(std::uint64_t token, const char* phase, const char* outcome, std::uint64_t submission, std::uint32_t queue, std::size_t packetOffset, std::string_view reason, std::string_view exceptionType = "none") {
if (token == 0) return;
AgcDriver::CaptureTrace::LogBestEffort("draw-attempt-failed token=%llu phase=%s outcome=%s submission=%llu queue=%x offset=%zu exception=%s reason=%.256s",
static_cast<unsigned long long>(token), phase, outcome, static_cast<unsigned long long>(submission), queue, packetOffset, exceptionType.data(), reason.data());
}
void TraceDrawAttemptException(std::uint64_t token, const char* phase, std::uint64_t submission, std::uint32_t queue, std::size_t packetOffset, const std::exception_ptr& failure) noexcept {
try {
TraceDrawAttemptFailure(token, phase, "exception", submission, queue, packetOffset, ExceptionReason(failure), ExceptionType(failure));
} catch (...) {
}
}
const char* DrawVerdictName(DrawVerdict verdict) {
switch (verdict) {
case DrawVerdict::Drawn: return "Drawn";
case DrawVerdict::Nothing: return "Nothing";
case DrawVerdict::Rejected: return "Rejected";
}
return "Unknown";
}
std::string_view TrimmedReason(std::string_view reason) {
if (reason.size() > 200) reason = reason.substr(0, 200);
return reason;
}
}
struct DrawTail {
using GpuLock = std::unique_lock<std::remove_reference_t<decltype(GuestMemory::GpuMutex())>>;
bool profile = false;
@@ -46,7 +142,8 @@ struct DrawTail {
std::shared_ptr<DrawEntry> dataEntry;
std::uint64_t shapeKey = 0;
ShaderRecompiler::ShaderPixelStageInfo pixel{};
std::vector<DrawProgram> programs;
std::vector<DrawProgram> writablePrograms;
const std::vector<DrawProgram>* programs = nullptr;
std::vector<ShaderRecompiler::MemoryRegion> memory;
std::vector<ShaderRecompiler::LinkedProgram> linked;
std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>> vertexInfos;
@@ -79,6 +176,8 @@ struct DrawTail {
QueueState* queue = nullptr;
std::span<const std::uint32_t> packet;
std::string* rejected = nullptr;
std::uint64_t traceToken = 0;
VulkanDevice::DrawTraceContext trace{};
std::string ownRejected;
GpuLock* gpuLock = nullptr;
GpuLock ownLock{GuestMemory::GpuMutex(), std::defer_lock};
@@ -94,7 +193,7 @@ void noteFinisherHandOff(bool handed) {
if (!stats) return;
const auto tenSeconds = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now().time_since_epoch()).count() / 10;
if (reportedAt.exchange(tenSeconds, std::memory_order_relaxed) == tenSeconds) return;
std::fprintf(stderr, "[finisher] draws (10 s): %llu handed to the draw finisher, %llu finished on the worker%s\n", static_cast<unsigned long long>(handedOff.exchange(0)), static_cast<unsigned long long>(kept.exchange(0)), DrawFinisher::DrainText().c_str());
AgcDriver::ProfilePrint_nid_no_patch("[finisher] draws (10 s): %llu handed to the draw finisher, %llu finished on the worker%s\n", static_cast<unsigned long long>(handedOff.exchange(0)), static_cast<unsigned long long>(kept.exchange(0)), DrawFinisher::DrainText().c_str());
}
}
@@ -147,7 +246,7 @@ DrawFinisher::Pending heldWrites(const DrawTail& tail) {
noteHeld(HeldReason::NoDecode);
return held;
}
if (tail.matched.size() != tail.programs.size()) {
if (tail.matched.size() != tail.programs->size()) {
noteHeld(HeldReason::Size);
return held;
}
@@ -157,10 +256,10 @@ DrawFinisher::Pending heldWrites(const DrawTail& tail) {
const auto& variant = tail.matched[i];
if (variant == nullptr || variant->compiled == nullptr) {
const bool candidates = !tail.drawHit && i < tail.dataCandidates.size() && !tail.dataCandidates[i].empty();
const auto& program = tail.programs[i];
const auto& program = (*tail.programs)[i];
if (!candidates && program.snapshot != nullptr && !ProgramWritesMemory(*program.snapshot, program.codeOffset) && (program.tailContinuation == nullptr || !ProgramWritesMemory(*program.tailContinuation, 0))) continue;
if (!candidates) {
const auto stage = tail.programs[i].binary.stage;
const auto stage = (*tail.programs)[i].binary.stage;
noteHeld(stage == ShaderRecompiler::ShaderStage::Fragment ? HeldReason::NullFragment : stage == ShaderRecompiler::ShaderStage::Vertex ? HeldReason::NullVertex : HeldReason::NullOther);
return held;
}
@@ -206,7 +305,7 @@ std::unordered_map<std::uint64_t, bool>& CpuIndirectPrograms() {
}
DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::string& rejected) {
DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::size_t packetOffset, std::string& rejected) {
PerformanceTimer timing("Driver.Draw");
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
auto tailHolder = std::make_shared<DrawTail>();
@@ -230,8 +329,18 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
pending.tailAt = phaseLap;
return DrawVerdict::Drawn;
};
const std::uint64_t traceToken = TraceShaderCorrelation() ? NextDrawTraceToken().fetch_add(1, std::memory_order_relaxed) + 1 : 0;
if (traceToken != 0) {
AgcDriver::CaptureTrace::LogBestEffort("draw-attempt token=%llu submission=%llu queue=%x offset=%zu",
static_cast<unsigned long long>(traceToken), static_cast<unsigned long long>(submission.serial), submission.queue, packetOffset);
}
auto& drawParameters = tail.drawParameters;
drawParameters = Pm4::ResolveDraw(packet, queue);
try {
drawParameters = Pm4::ResolveDraw(packet, queue);
} catch (...) {
TraceDrawAttemptException(traceToken, "resolve-draw", submission.serial, submission.queue, packetOffset, std::current_exception());
throw;
}
tail.pipelined = DrawPipeline::Active();
const bool pipelined = tail.pipelined;
static const bool pipelinedIndirect = std::getenv("APS5_NO_PIPELINED_INDIRECT") == nullptr;
@@ -242,7 +351,10 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (!pipelinedIndirect || overlaps) pipeline.Drain(DrawPipeline::DrainReason::Indirect);
}
auto& traceIndirect = tail.traceIndirect;
if (const auto verdict = precheckDraw(queue, submission, packet, drawParameters, rejected, traceIndirect)) return *verdict;
if (const auto verdict = precheckDraw(queue, submission, packetOffset, packet, drawParameters, rejected, traceIndirect)) {
TraceDrawAttemptFailure(traceToken, "precheck", "rejected", submission.serial, submission.queue, packetOffset, rejected);
return *verdict;
}
phaseTiming.Phase(DrawRowPrecheck);
using Stage = ShaderRecompiler::ShaderStage;
using Role = ShaderRecompiler::ProgramRole;
@@ -391,17 +503,25 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
}
decode = std::move(refreshed);
}
resolveDrawDecode(queue, submission, localDevice->graphicsContext(), decode, registerKey, drawKey, profile);
try {
resolveDrawDecode(queue, submission, localDevice->graphicsContext(), decode, registerKey, drawKey, profile);
} catch (...) {
TraceDrawAttemptFailure(traceToken, "decode", "exception", submission.serial, submission.queue, packetOffset, ExceptionReason(std::current_exception()));
throw;
}
const auto& graphics = decode->state;
auto& pixel = tail.pixel;
pixel = decode->pixel;
auto& programs = tail.programs;
programs = decode->programs;
for (const auto& patch : userPatches) programs[patch.program].userData[patch.index] = patch.value;
const bool writesPrograms = graphics.stages.mesh || drawParameters.indirect || !userPatches.empty();
auto& writablePrograms = tail.writablePrograms;
if (writesPrograms) writablePrograms = decode->programs;
for (const auto& patch : userPatches) writablePrograms[patch.program].userData[patch.index] = patch.value;
tail.programs = writesPrograms ? &writablePrograms : &decode->programs;
const auto& programs = *tail.programs;
if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn();
const bool emptyIndirectSkip = EmptyIndirectSkip();
auto& cpuIndirectPrograms = CpuIndirectPrograms();
const auto recordDraws = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); };
const auto recordDraws = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || !drawParameters.indirect->indexLimitKnown || arguments.firstVertexOrIndex < drawParameters.indexCount); };
if (emptyIndirectSkip && drawParameters.indirect && !programs.empty()) {
const auto known = cpuIndirectPrograms.find(programs.front().binary.codeAddress);
if (known != cpuIndirectPrograms.end() && known->second) {
@@ -420,7 +540,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
}
const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) {
if (!graphics.stages.mesh) return;
auto& words = programs.front().userData;
auto& words = writablePrograms.front().userData;
require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words");
const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters);
std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord);
@@ -497,7 +617,6 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
auto& compiledStages = tail.compiledStages;
compiledStages.reserve(programs.size());
auto& stages = tail.stages;
results.reserve(programs.size() + 2u);
stages.reserve(programs.size());
auto& pushCursorBytes = tail.pushCursorBytes;
@@ -526,7 +645,12 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
auto& verifyHit = tail.verifyHit;
tail.stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u);
const bool stageEntries = tail.stageEntries;
lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData);
try {
lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData);
} catch (...) {
TraceDrawAttemptException(traceToken, "lookup", submission.serial, submission.queue, packetOffset, std::current_exception());
throw;
}
for (std::size_t i = 0; i < matched.size(); ++i) {
if (matched[i] == nullptr || matched[i]->shaderDataPatches.empty()) continue;
applyShaderDataPatches(*matched[i]);
@@ -538,6 +662,14 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (drawHit || verifyHit) {
++drawRelocationCounters.hits;
++drawRelocationTrials[drawIdentity].hits;
if (const auto alias = drawAliases.find(drawAlias); alias != drawAliases.end()) {
const auto donor = drawCache.find(alias->second);
if (donor != drawCache.end() && drawCacheHits - donor->second->touched > drawCacheEntries() / 8) {
drawOrder.splice(drawOrder.begin(), drawOrder, donor->second->order);
donor->second->touched = drawCacheHits;
++drawEntryCounters.touches;
}
}
}
if (profile && drawRelocationCounters.lookups >= 100000) reportDrawRelocation();
}
@@ -548,6 +680,22 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
}
tail.patchedStages = std::any_of(stageData.begin(), stageData.end(), [](const StageDataHit& hit) { return hit.compiled != nullptr; });
tail.relocatedDecode = relocatedDecode;
std::uint64_t stageDataVerified = 0;
const auto target = graphics.hasColorTarget ? graphics.color.address : 0;
const VulkanDevice::DrawTraceContext trace{traceToken, submission.serial, submission.queue, packetOffset, target};
const auto* tracePtr = trace.token != 0 ? &trace : nullptr;
tail.traceToken = traceToken;
tail.trace = trace;
if (trace.token != 0) {
for (std::size_t i = 0; i < programs.size(); ++i) {
const auto& program = programs[i];
AgcDriver::CaptureTrace::LogBestEffort("draw-selected token=%llu submission=%llu queue=%x offset=%zu snapshot=0x%llx code=%llx entryOffset=%zu entry=%llx stage=%u role=%u drawCacheHit=%u variantHit=%u target=%llx",
static_cast<unsigned long long>(trace.token), static_cast<unsigned long long>(submission.serial), submission.queue, packetOffset,
static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(program.snapshot.get())), static_cast<unsigned long long>(program.snapshot->codeAddress), program.codeOffset,
static_cast<unsigned long long>(program.snapshot->codeAddress + program.codeOffset), static_cast<unsigned>(program.binary.stage), static_cast<unsigned>(roles[i]),
drawHit ? 1u : 0u, matched[i] != nullptr ? 1u : 0u, static_cast<unsigned long long>(target));
}
}
if (registerKey) {
@@ -578,6 +726,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
tail.reg207 = readRegister(queue.context, 0x207);
tail.dumpTarget = dumpTarget;
tail.dumpSlot1 = dumpSlot1;
tail.packetOffset = packetOffset;
const bool handOff = pipelined && !drawParameters.indirect && !profile && !lockedPrepare && dumpTarget == 0 && dumpSlot1 == 0 && !tracePrograms && !verifyDrawDataHits() && deferredLabels().labels.empty() && DrawFinisher::Enabled();
if (handOff) {
tail.epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed);
@@ -617,6 +766,11 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
using Stage = ShaderRecompiler::ShaderStage;
using Role = ShaderRecompiler::ProgramRole;
const bool profile = tail.profile;
const auto traceToken = tail.traceToken;
const auto& trace = tail.trace;
const auto* tracePtr = trace.token != 0 ? &trace : nullptr;
const auto packetOffset = tail.packetOffset;
const auto packet = tail.packet;
auto& phaseMs = tail.phaseMs;
auto& phaseLap = tail.phaseLap;
auto& captures = tail.captures;
@@ -640,7 +794,8 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
auto& shapeKey = tail.shapeKey;
const auto& graphics = decode->state;
auto& pixel = tail.pixel;
auto& programs = tail.programs;
auto& writablePrograms = tail.writablePrograms;
const auto& programs = *tail.programs;
const std::vector<Role>& roles = decode->roles;
auto& memory = tail.memory;
auto& linked = tail.linked;
@@ -682,7 +837,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
};
const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) {
if (!graphics.stages.mesh) return;
auto& words = programs.front().userData;
auto& words = writablePrograms.front().userData;
require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words");
const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters);
std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord);
@@ -746,6 +901,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
throw std::runtime_error(message);
};
std::optional<Graphics::IndirectDrawPath> indirectCpu;
std::size_t completedSubdraws = 0;
std::vector<std::uint32_t> pushOffsets(programs.size(), 0);
std::optional<ShaderRecompiler::MeshBarycentricLayout> meshBarycentrics;
std::vector<std::size_t> resultIndex(programs.size(), 0);
@@ -775,7 +931,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
break;
}
}
compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, packetOffset, trace.token, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[i] = compiledStages.back().get();
if (candidate != dataCandidates[i].end()) verifyDataStage(i, *candidate->first);
@@ -791,10 +947,15 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
} else if (i == 0) {
fold(result, drawParameters);
}
const auto slotEnd = (pushCursorBytes / Graphics::PipelinePushSlotBytes + 1u) * Graphics::PipelinePushSlotBytes;
require(result.pushConstants.size() <= slotEnd - pushCursorBytes, "stage push constants exceed the pipeline push constant block");
stages.push_back({program.binary.stage, &result, result.pushConstants.empty() ? 0u : pushCursorBytes});
pushCursorBytes += static_cast<std::uint32_t>(result.pushConstants.size());
try {
const auto slotEnd = (pushCursorBytes / Graphics::PipelinePushSlotBytes + 1u) * Graphics::PipelinePushSlotBytes;
require(result.pushConstants.size() <= slotEnd - pushCursorBytes, "stage push constants exceed the pipeline push constant block");
stages.push_back({program.binary.stage, &result, result.pushConstants.empty() ? 0u : pushCursorBytes});
pushCursorBytes += static_cast<std::uint32_t>(result.pushConstants.size());
} catch (...) {
TraceMaterializationFailure(trace.token, submission, packetOffset, "push-constant-validation", completedSubdraws, std::current_exception());
throw;
}
}
if (stageDataVerified != 0) {
@@ -836,29 +997,35 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
}
if (tail.queue != nullptr && pipelined && !deferredLabels().labels.empty()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Labels);
if (tail.queue != nullptr && recordQueuedLabelsAfterCapture(submission.queue, memory)) return draw(*tail.queue, tail.packet, submission, rejected);
if (recordQueuedLabelsAfterCapture(submission.queue, memory)) return draw(*tail.queue, packet, submission, packetOffset, rejected);
bool rectListBuilt = false;
std::size_t rectIndex = 0;
const auto buildRectList = [&] {
phaseTiming.Phase(DrawRowVectors);
require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs");
auto rectangle = DrawRectangle(*programs[0].snapshot, programs[1].snapshot, programResults[0]->variantId, programResults[1]->variantId, localDevice->Target());
auto& control = graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? rectangle.lastVertexControl : rectangle.control;
if (rectListBuilt) {
results[rectIndex] = std::move(control);
results[rectIndex + 1] = std::move(rectangle.evaluation);
try {
phaseTiming.Phase(DrawRowVectors);
require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs");
auto rectangle = DrawRectangle(*programs[0].snapshot, programs[1].snapshot, programResults[0]->variantId, programResults[1]->variantId, localDevice->Target());
auto& control = graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? rectangle.lastVertexControl : rectangle.control;
if (rectListBuilt) {
results[rectIndex] = std::move(control);
results[rectIndex + 1] = std::move(rectangle.evaluation);
phaseTiming.Phase(DrawRowRectList);
return;
}
require(stages.size() == 2, "rect-list requires vertex and fragment programs");
results.reserve(programs.size() + 2u);
rectIndex = results.size();
results.push_back(std::move(control));
results.push_back(std::move(rectangle.evaluation));
stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[rectIndex], 0}, {Stage::TessellationEvaluation, &results[rectIndex + 1], 0}});
rectListBuilt = true;
phaseTiming.Phase(DrawRowRectList);
return;
} catch (...) {
TraceMaterializationFailure(trace.token, submission, packetOffset, "rect-list", completedSubdraws, std::current_exception());
throw;
}
require(stages.size() == 2, "rect-list requires vertex and fragment programs");
rectIndex = results.size();
results.push_back(std::move(control));
results.push_back(std::move(rectangle.evaluation));
stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[rectIndex], 0}, {Stage::TessellationEvaluation, &results[rectIndex + 1], 0}});
rectListBuilt = true;
phaseTiming.Phase(DrawRowRectList);
};
if (graphics.rectList) buildRectList();
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active && graphics.stages.path == Graphics::ShaderPath::Geometry) {
@@ -868,7 +1035,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
if (programResults[0]->meshBarycentrics != meshBarycentrics) {
const auto pushBytes = programResults[0]->pushConstants.size();
const auto exports = programResults[0]->parameterExports;
compiledStages.push_back(materializeDrawStage(0, pushOffsets[0], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
compiledStages.push_back(materializeDrawStage(0, pushOffsets[0], queue, submission, packetOffset, traceToken, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[0] = compiledStages.back().get();
stages[0].program = programResults[0];
@@ -878,6 +1045,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
} else if (!graphics.rectList && !programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
require(graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it");
const auto geometryLimits = localDevice->GeometryLimits();
results.reserve(programs.size() + 2u);
if (geometryLimits || !localDevice->Target().tessellation) {
results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits));
stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0});
@@ -919,6 +1087,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
if (drawParameters.indirect && indirectCpu) {
const auto indirect = *drawParameters.indirect;
results.reserve(programs.size() + 2u);
for (std::size_t i = 0; i < programs.size(); ++i) {
if (programResults[i] == nullptr) continue;
resultIndex[i] = results.size();
@@ -937,7 +1106,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
const auto baseVertexWords = UserDataWords(programs, roles, indirect.baseVertexLocation);
const auto startInstanceWords = UserDataWords(programs, roles, indirect.startInstanceLocation);
const auto drawIndexWords = indirect.drawIndexEnabled ? UserDataWords(programs, roles, indirect.drawIndexLocation) : std::vector<std::pair<std::size_t, std::size_t>>{};
const auto drawable = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); };
const auto drawable = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || !indirect.indexLimitKnown || arguments.firstVertexOrIndex < drawParameters.indexCount); };
std::size_t lastDrawable = records.size();
for (std::size_t record = 0; record < records.size(); ++record) {
if (drawable(records[record])) lastDrawable = record;
@@ -949,7 +1118,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
std::set<std::size_t> patched;
const auto patch = [&](const std::vector<std::pair<std::size_t, std::size_t>>& words, std::uint32_t value) {
for (const auto& [program, word] : words) {
programs[program].userData[word] = value;
writablePrograms[program].userData[word] = value;
patched.insert(program);
}
};
@@ -963,10 +1132,16 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
}
Pm4::DrawParameters direct{0, arguments.count, 0, arguments.instances, drawParameters.flags, drawParameters.indexed, 0, 0};
if (drawParameters.indexed) {
if (arguments.firstVertexOrIndex >= drawParameters.indexCount) continue;
if (indirect.indexLimitKnown) {
if (arguments.firstVertexOrIndex >= drawParameters.indexCount) continue;
direct.indexCount = std::min(arguments.count, drawParameters.indexCount - arguments.firstVertexOrIndex);
} else {
const auto end = static_cast<std::uint64_t>(arguments.firstVertexOrIndex) + arguments.count;
require(end <= std::numeric_limits<std::uint32_t>::max(), "indirect draw index range overflow");
require(end * drawParameters.indexSize <= std::numeric_limits<std::uint64_t>::max() - drawParameters.indexAddress, "indirect draw index range address overflow");
direct.indexCount = arguments.count;
}
direct.indexAddress = drawParameters.indexAddress + static_cast<std::uint64_t>(arguments.firstVertexOrIndex) * drawParameters.indexSize;
direct.indexCount = std::min(arguments.count, drawParameters.indexCount - arguments.firstVertexOrIndex);
direct.indexSize = drawParameters.indexSize;
} else {
direct.firstVertex = indirect.indxOffset;
@@ -979,7 +1154,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
auto& result = results[resultIndex[programIndex]];
const auto pushBytes = result.pushConstants.size();
decodeVertexInfo(programIndex);
result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics);
result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, packetOffset, trace.token, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics);
if (!rejected.empty()) return DrawVerdict::Rejected;
require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout");
}
@@ -996,7 +1171,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
const auto commitQueue = submission.queue;
const auto commitPacket = tail.packetOpcode;
const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed);
auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), direct, stages = std::move(stages), snapshots = std::move(snapshots), commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] {
auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), direct, stages = std::move(stages), snapshots = std::move(snapshots), commitQueue, commitPacket, epoch, writablePrograms = std::move(writablePrograms), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] {
DrawPipeline::FollowEpoch(epoch);
GuestMemory::SetCurrentPacket(commitPacket, commitQueue);
commitDraw(localDevice, commitQueue, decode->state, direct, stages, snapshots, nullptr, {}, 0);
@@ -1011,7 +1186,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
lockForDraw();
noteDrawWriters(stages, submission.queue);
phaseTiming.Phase(DrawRowVectors);
localDevice->Draw(graphics, direct, stages, snapshots);
localDevice->Draw(graphics, direct, stages, snapshots, nullptr, tracePtr);
phaseTiming.Phase(DrawRowGraphics);
}
timing.Mark("draw_and_resource_release");
@@ -1042,10 +1217,10 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
const auto commitPacket = tail.packetOpcode;
const auto epoch = DrawFinisher::OnFinisherThread() ? tail.epoch : DrawPipeline::EpochToken().load(std::memory_order_relaxed);
auto reads = QueuedDrawReadRanges(drawParameters, stages);
auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable {
auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, trace = trace, writablePrograms = std::move(writablePrograms), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable {
DrawPipeline::FollowEpoch(epoch);
GuestMemory::SetCurrentPacket(commitPacket, commitQueue);
commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey);
commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey, trace);
};
auto held = std::make_shared<decltype(commit)>(std::move(commit));
DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads), true);
@@ -1058,7 +1233,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
noteDrawWriters(stages, submission.queue);
phaseTiming.Phase(DrawRowVectors);
if (recipe != nullptr) {
if (localDevice->DrawFromRecipe(graphics, drawParameters, stages, snapshots, recipe) == RecipeOutcome::Recorded) {
if (localDevice->DrawFromRecipe(graphics, drawParameters, stages, snapshots, recipe, tracePtr) == RecipeOutcome::Recorded) {
phaseTiming.Phase(DrawRowGraphics);
timing.Mark("draw_and_resource_release");
return drawn();
@@ -1067,14 +1242,15 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, VulkanDevice::RecipeKind::Draw);
}
std::shared_ptr<const DrawRecipe> built;
localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built);
localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr);
phaseTiming.Phase(DrawRowGraphics);
if (built != nullptr) attachDrawRecipe(drawKey, recipeStages, std::move(built));
timing.Mark("draw_and_resource_release");
return drawn();
}
void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey) {
void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace) {
const auto* tracePtr = trace.token != 0 ? &trace : nullptr;
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (auto current = device.Load(); current != nullptr && current != localDevice) localDevice = std::move(current);
@@ -1088,11 +1264,11 @@ void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t
recordLabelsForPacket(localDevice.get(), queue);
noteDrawWriters(stages, queue);
if (recipe != nullptr) {
if (localDevice->DrawFromRecipe(graphics, drawParameters, stages, snapshots, recipe) == RecipeOutcome::Recorded) return;
if (localDevice->DrawFromRecipe(graphics, drawParameters, stages, snapshots, recipe, tracePtr) == RecipeOutcome::Recorded) return;
VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, VulkanDevice::RecipeKind::Draw);
}
std::shared_ptr<const DrawRecipe> built;
localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built);
localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr);
if (built != nullptr) attachDrawRecipe(drawKey, recipeStages, std::move(built));
}
@@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
@@ -11,10 +12,55 @@
#include <span>
#include <sstream>
#include <stdexcept>
#include <exception>
#include <typeinfo>
namespace AgcDriver::DriverDetail {
std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics) try {
namespace {
void TraceMaterializationFailure(std::uint64_t token, const Submission& submission, std::size_t packetOffset, std::size_t stage, const char* phase, const std::exception_ptr& failure) {
if (token == 0) return;
const char* type = "unknown";
std::string reason = "unknown exception";
try {
if (failure != nullptr) std::rethrow_exception(failure);
} catch (const std::exception& error) {
type = typeid(error).name();
reason = error.what();
} catch (...) {
}
try {
AgcDriver::CaptureTrace::Log("draw-materialization-failed token=%llu submission=%llu queue=%x offset=%zu stage=%zu phase=%s exception=%s reason=%.256s",
static_cast<unsigned long long>(token), static_cast<unsigned long long>(submission.serial), submission.queue, packetOffset, stage, phase, type, reason.c_str());
} catch (...) {
}
}
void TraceShaderRequestFailure(std::uint64_t token, const Submission& submission, std::size_t packetOffset, const DrawProgram& program, const std::exception_ptr& failure) noexcept {
if (token == 0) return;
try {
std::string reason = "unknown exception";
const char* type = "unknown";
try {
if (failure != nullptr) std::rethrow_exception(failure);
} catch (const std::exception& error) {
type = typeid(error).name();
reason = error.what();
} catch (...) {
}
AgcDriver::CaptureTrace::LogBestEffort("draw-shader-request-failed token=%llu submission=%llu queue=%x offset=%zu snapshot=0x%llx code=%llx codeOffset=%zu stage=%u type=%u reason=%.256s",
static_cast<unsigned long long>(token), static_cast<unsigned long long>(submission.serial), submission.queue, packetOffset,
static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(program.snapshot.get())),
static_cast<unsigned long long>(program.snapshot->codeAddress), program.codeOffset,
static_cast<unsigned>(program.binary.stage), static_cast<unsigned>(program.snapshot->type), reason.c_str());
} catch (...) {
}
}
}
std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, std::size_t packetOffset, std::uint64_t traceToken, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics) try {
PerformanceTimer timing("Shader.DrawStage");
using Stage = ShaderRecompiler::ShaderStage;
phaseTiming.Phase(DrawRowVectors);
@@ -28,7 +74,14 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt}
};
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request);
auto invocation = [&] {
try {
return InvocationFor(*program.snapshot, program.codeOffset, request);
} catch (...) {
TraceShaderRequestFailure(traceToken, submission, packetOffset, program, std::current_exception());
throw;
}
}();
timing.Mark("prepared_invocation");
auto& stageCapture = stageCaptures[i];
stageCapture.forgetSerial = GuestMemory::ForgetSerial();
@@ -73,16 +126,16 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
++captures;
phaseTiming.Phase(DrawRowCapture);
const auto waited = std::min(Graphics::Recorder::ThreadWaitedMs() - waitedBefore, phaseMs[DrawRowCapture]);
phaseMs[DrawRowCapture] -= waited;
phaseMs[DrawRowCaptureHookWaits] += waited;
}
if (dumpTarget != 0) {
const auto waited = std::min(Graphics::Recorder::ThreadWaitedMs() - waitedBefore, phaseMs[DrawRowCapture]);
phaseMs[DrawRowCapture] -= waited;
phaseMs[DrawRowCaptureHookWaits] += waited;
}
if (dumpTarget != 0) {
const auto slot0 = (static_cast<std::uint64_t>(readRegister(queue.context, 0x390)) << 40u) | (static_cast<std::uint64_t>(readRegister(queue.context, 0x318)) << 8u);
if ((graphics.hasColorTarget && graphics.color.address == dumpTarget) || slot0 == dumpTarget) static_cast<void>(dumpRequest(program.binary.codeAddress, request));
}
if (dumpSlot1 != 0) {
}
if (dumpSlot1 != 0) {
const auto value = [&](std::uint32_t offset) -> std::uint64_t { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; };
const auto slot1 = (value(0x391) << 40u) | (value(0x327) << 8u);
if (slot1 == dumpSlot1) {
@@ -94,17 +147,18 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
std::fclose(file);
}
}
}
}
phaseTiming.Phase(DrawRowCapture);
phaseTiming.Phase(DrawRowCapture);
timing.Mark("capture_resources");
try {
stageCapture.compiled = invocation.Materialize(*capture);
stageCapture.capture = capture;
} catch (const std::exception& error) {
TraceMaterializationFailure(traceToken, submission, packetOffset, i, "capture-or-materialize", std::current_exception());
throw named(error, request.context.memory);
}
stageCapture.capture = capture;
timing.Mark("materialize");
const auto walked = capture->userDataUse.asValue | capture->userDataUse.asBase;
auto mask = walked == 0 ? ~std::uint64_t{0} : walked | stageCapture.compiled->userDataRegisterReads;
@@ -179,6 +179,7 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
auto product = std::make_shared<DrawDecode>();
product->state = Graphics::DecodeState(queue, VertexWorkgroupRequired(queue, *submission.shaders));
DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true);
product->state.depthCopySource = std::ranges::any_of(product->programs, [](const DrawProgram& program) { return program.binary.codeAddress == DepthCopyPixelProgramAddress(true); });
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue));
Graphics::NormalizeUnusedViewportDepth(product->state, product->pixel);
Graphics::ApplyDepthRemap(context, product->state);
@@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include "prx/libc/include/HostThreadLocal.hpp"
#include <algorithm>
@@ -364,7 +365,7 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) {
std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held, %llu decode drains", static_cast<unsigned long long>(heldReadRanges.load(std::memory_order_relaxed)), static_cast<unsigned long long>(heldReadHits.exchange(0, std::memory_order_relaxed)), static_cast<unsigned long long>(decodeDrainCount - reportedDecodeDrains));
reportedDecodeDrains = decodeDrainCount;
drainText += reads;
std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room, %.2f us for the lock; %llu committer wakes;%s; drains:%s\n", Depth(), static_cast<unsigned long long>(commits), perCommit, static_cast<unsigned long long>(commitErrors), static_cast<unsigned long long>(enqueued), queued, perDraw, enqueued == 0 ? 0.0 : static_cast<double>(lockWaitNs) / 1e3 / static_cast<double>(enqueued), static_cast<unsigned long long>(wakes), reads, drainText.c_str());
AgcDriver::ProfilePrint_nid_no_patch("[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room, %.2f us for the lock; %llu committer wakes;%s; drains:%s\n", Depth(), static_cast<unsigned long long>(commits), perCommit, static_cast<unsigned long long>(commitErrors), static_cast<unsigned long long>(enqueued), queued, perDraw, enqueued == 0 ? 0.0 : static_cast<double>(lockWaitNs) / 1e3 / static_cast<double>(enqueued), static_cast<unsigned long long>(wakes), reads, drainText.c_str());
commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = wakes = lockWaitNs = 0;
drains = {};
drainWaitNs = {};
@@ -1,6 +1,8 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp"
@@ -8,12 +10,39 @@
namespace AgcDriver::DriverDetail {
namespace {
QueueState DepthCopyDraw(const QueueState& queue, const Graphics::DepthCopyPass& copy) {
const bool multisampled = copy.source.samples != 1u;
const auto address = DepthCopyPixelProgramAddress(multisampled);
const auto words = Graphics::DepthCopySourceWords(copy.source);
QueueState draw = queue;
draw.context.insert_or_assign(0x000u, 0u);
draw.shader.insert_or_assign(0x008u, static_cast<std::uint32_t>(address >> 8u));
draw.shader.insert_or_assign(0x009u, static_cast<std::uint32_t>(address >> 40u));
draw.shader.insert_or_assign(0x00au, 0u);
draw.shader.insert_or_assign(0x00bu, DepthCopyPixelUserWords(multisampled) << 1u);
for (std::uint32_t index = 0; index < words.size(); ++index) draw.shader.insert_or_assign(0x00cu + index, words[index]);
if (multisampled) draw.shader.insert_or_assign(0x014u, copy.sample);
draw.context.insert_or_assign(0x1b3u, 0x8002u);
draw.context.insert_or_assign(0x1b4u, 0x8002u);
draw.context.insert_or_assign(0x1b6u, 0u);
draw.context.insert_or_assign(0x1c4u, 0u);
draw.context.insert_or_assign(0x1c5u, 1u);
draw.context.insert_or_assign(0x08fu, 1u);
draw.context.insert_or_assign(0x203u, 0u);
if (const auto alpha = draw.context.find(0x2dcu); alpha != draw.context.end()) draw.context.insert_or_assign(0x2dcu, alpha->second & ~1u);
return draw;
}
}
bool Driver::drawPrecheck() {
static const bool precheck = std::getenv("APS5_NO_DRAW_PRECHECK") == nullptr;
return precheck;
}
std::optional<DrawVerdict> Driver::precheckDraw(const QueueState& queue, const Submission& submission, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect) {
std::optional<DrawVerdict> Driver::precheckDraw(const QueueState& queue, const Submission& submission, std::size_t packetOffset, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect) {
if (!drawParameters.indirect && !drawParameters.indexed && (drawParameters.indexCount == 0 || drawParameters.instanceCount == 0)) return DrawVerdict::Nothing;
if (const auto pass = Graphics::DecodeColorMetadataPass(queue)) {
require(!drawParameters.indirect, "indirect CB metadata passes are unsupported");
@@ -47,14 +76,8 @@ std::optional<DrawVerdict> Driver::precheckDraw(const QueueState& queue, const S
}
if (const auto copy = Graphics::DecodeDepthCopyPass(queue)) {
require(!drawParameters.indirect, "indirect DB depth copies are unsupported");
if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, (packet[0] >> 8u) & 0xffu);
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (device == nullptr) device = std::make_shared<VulkanDevice>();
const std::shared_ptr<VulkanDevice> localDevice = device;
recordLabelsForPacket(localDevice.get(), submission.queue);
localDevice->DepthCopyPass(*copy);
return DrawVerdict::Drawn;
auto copyDraw = DepthCopyDraw(queue, *copy);
return draw(copyDraw, packet, submission, packetOffset, rejected);
}
if (Graphics::DepthResummarizeDrawsNothing(queue)) return DrawVerdict::Nothing;
rejected = Graphics::DepthMaintenanceRejection(queue);
@@ -251,7 +251,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
for (std::size_t i = 0; i < s.blends.size(); ++i) {
if (!sameBlend(s.blends[i], t.blends[i])) return false;
}
if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.polygonMode != t.polygonMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false;
if (s.colorRop != t.colorRop || s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.polygonMode != t.polygonMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false;
if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false;
const auto& p = a.pixel;
const auto& q = b.pixel;
@@ -173,15 +173,21 @@ void Driver::learnPendingRelocation() {
}
if (pending.alias == 0) return;
if (drawAliases.size() >= MaxAliases) drawRelocationCounters.evicted += dropEveryOther(drawAliases);
drawAliases[pending.alias] = inserted->second;
drawAliases[pending.alias] = pending.key;
}
void Driver::relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std::shared_ptr<DrawEntry>& entry, std::shared_ptr<const DrawDecode>& decode, std::vector<DrawUserPatch>& patches) {
auto& counters = drawRelocationCounters;
++counters.lookups;
const auto found = drawAliases.find(alias);
if (found == drawAliases.end() || found->second->decode == nullptr) return;
const auto donor = found->second;
if (found == drawAliases.end()) return;
const auto cached = drawCache.find(found->second);
if (cached == drawCache.end()) {
drawAliases.erase(found);
return;
}
if (cached->second->decode == nullptr) return;
const auto donor = cached->second;
const auto& programs = donor->decode->programs;
if (donor->stages.size() != programs.size()) return;
struct Move {
@@ -29,7 +29,8 @@ std::optional<Graphics::IndirectDrawPath> Driver::ClassifyIndirectDraw(const Sha
using Path = Graphics::IndirectDrawPath;
const auto support = localDevice->DrawIndirectSupport();
const auto fail = [&](Path reason) { if (!indirectCpu) indirectCpu = reason; };
if (drawParameters.indexed && !GuestMemory::Accessible(reinterpret_cast<const void*>(drawParameters.indexAddress), static_cast<std::size_t>(drawParameters.indexCount) * drawParameters.indexSize)) fail(Path::IndexRange);
if (drawParameters.indexed && !indirect.indexLimitKnown) fail(Path::IndexLimitUnknown);
else if (drawParameters.indexed && !GuestMemory::Accessible(reinterpret_cast<const void*>(drawParameters.indexAddress), static_cast<std::size_t>(drawParameters.indexCount) * drawParameters.indexSize)) fail(Path::IndexRange);
constexpr std::uint32_t NoLocation = 0x280u;
const bool meshPath = graphics.stages.mesh.has_value() && !graphics.stages.tessellation && !graphics.rectList;
const bool meshGpuSide = meshPath && drawParameters.indexed && indirect.count == 1 && !indirect.countIndirect && indirect.baseVertexLocation == NoLocation && indirect.startInstanceLocation == NoLocation && (!indirect.drawIndexEnabled || indirect.drawIndexLocation == NoLocation);
@@ -1,15 +1,28 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp"
#include "prx/libkernel/Time/include/SleepWake.hpp"
#include <algorithm>
#include <mutex>
namespace AgcDriver {
void Submit(const Packet* packet, std::uint32_t queue) try {
KernelNoteGpuSubmit_nid_no_patch();
DriverDetail::Driver::Get().Submit(packet, queue);
auto& driver = DriverDetail::Driver::Get();
auto* const recorder = FrameRecorder::Active();
if (recorder == nullptr) {
driver.Submit(packet, queue);
return;
}
GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet));
recorder->Submit(*packet, queue);
const auto flips = driver.FlipSubmissions();
driver.Submit(packet, queue);
if (driver.FlipSubmissions() != flips) recorder->FrameBoundary();
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
}
@@ -23,18 +36,22 @@ void Shutdown() {
}
void RegisterShader(const Shader* shader) {
if (auto* const recorder = FrameRecorder::Active()) recorder->RegisterShader(shader);
DriverDetail::Driver::Get().RegisterShader(shader);
}
void SuspendPoint() {
if (auto* const recorder = FrameRecorder::Active()) recorder->Suspend();
DriverDetail::Driver::Get().SuspendPoint();
}
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
if (auto* const recorder = FrameRecorder::Active()) recorder->RegisterVideoOutput(handle);
DriverDetail::Driver::Get().RegisterVideoOutput(handle, output);
}
void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
if (auto* const recorder = FrameRecorder::Active()) recorder->UnregisterVideoOutput(handle);
DriverDetail::Driver::Get().UnregisterVideoOutput(handle, output);
}
@@ -47,6 +64,7 @@ void PresentClear(const PresentationWindow& window, bool opaque, void (*gpuReady
}
void PresentBuffer(const PresentationWindow& window, const DisplayBuffer& buffer, void (*gpuReady)(void*), void* context) {
if (auto* const recorder = FrameRecorder::Active()) recorder->PresentBuffer(buffer);
DriverDetail::Driver::Get().Present(window, &buffer, true, gpuReady, context);
}
@@ -104,11 +122,33 @@ extern "C" void AgcDriverSuspendPoint_nid_postfix() try {
}
extern "C" std::uint32_t AgcDriverReserveEopFlip_nid_postfix(const AgcDriver::FlipInfo& info) {
return AgcDriver::DriverDetail::Driver::Get().ReserveEopFlip(info);
const auto token = AgcDriver::DriverDetail::Driver::Get().ReserveEopFlip(info);
if (auto* const recorder = AgcDriver::FrameRecorder::Active(); recorder != nullptr && token != 0) recorder->ReserveEopFlip(info, token);
return token;
}
extern "C" std::uint32_t AgcDriverReserveEopFlipAt_nid_postfix(const AgcDriver::FlipInfo& info, std::uint32_t token) {
return AgcDriver::DriverDetail::Driver::Get().ReserveEopFlipAt(info, token);
}
extern "C" std::size_t AgcDriverAwaitedWaits_nid_postfix(AgcDriver::AwaitedWait* waits, std::size_t capacity) {
const auto awaited = AgcDriver::DriverDetail::Driver::Get().AwaitedWaits();
const auto count = std::min(capacity, awaited.size());
std::copy_n(awaited.begin(), count, waits);
return count;
}
extern "C" void AgcDriverSerialProgress_nid_postfix(std::uint64_t* accepted, std::uint64_t* completed) {
const auto [submitted, done] = AgcDriver::DriverDetail::Driver::Get().SerialProgress();
*accepted = submitted;
*completed = done;
}
extern "C" void AgcDriverSubmitFlip_nid_postfix(const AgcDriver::FlipInfo& info) try {
auto* const recorder = AgcDriver::FrameRecorder::Active();
if (recorder != nullptr) recorder->Flip(info);
AgcDriver::DriverDetail::Driver::Get().SubmitFlip(info);
if (recorder != nullptr) recorder->FrameBoundary();
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
}
@@ -146,17 +186,20 @@ extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) {
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) try {
if (auto* const recorder = AgcDriver::FrameRecorder::Active()) recorder->ResolveShaderAbi(shader, context, primitive);
AgcDriver::DriverDetail::Driver::Get().ResolveShaderAbi(shader, context, primitive);
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
}
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) try {
if (auto* const recorder = AgcDriver::FrameRecorder::Active()) recorder->ResolveGraphicsAbi(vertex, pixel, primitiveType);
AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsAbi(vertex, pixel, primitiveType);
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
}
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) try {
if (auto* const recorder = AgcDriver::FrameRecorder::Active()) recorder->ResolveGraphicsStagesAbi(stages, context, primitive);
AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsStagesAbi(stages, context, primitive);
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
@@ -247,6 +247,40 @@ void Driver::WaitIdle() {
if (const auto localDevice = device.Load()) localDevice->WaitIdle();
}
bool Driver::WaitIdleFor(std::chrono::milliseconds timeout) {
require(!onWorkerThread(), "worker cannot wait for itself");
std::unique_lock lock(mutex);
const auto target = accepted;
++idleWaiters;
const bool idle = changed.wait_for(lock, timeout, [&] { return failure != nullptr || stopping || completed >= target; });
--idleWaiters;
rethrowFailure();
checkStopping();
return idle;
}
std::vector<AwaitedWait> Driver::AwaitedWaits() {
std::vector<AwaitedWait> result;
for (const auto& slot : awaitedSlots) {
const auto address = slot.address.load(std::memory_order_acquire);
if (address == 0) continue;
AwaitedWait wait;
wait.address = address;
wait.reference = slot.reference.load(std::memory_order_relaxed);
wait.mask = slot.mask.load(std::memory_order_relaxed);
const auto shape = slot.shape.load(std::memory_order_relaxed);
wait.function = static_cast<std::uint32_t>(shape);
wait.bytes = static_cast<std::uint32_t>(shape >> 32u);
if (slot.address.load(std::memory_order_acquire) == address) result.push_back(wait);
}
return result;
}
std::pair<std::uint64_t, std::uint64_t> Driver::SerialProgress() {
std::lock_guard lock(mutex);
return {accepted, completed};
}
void Driver::CheckFailure() {
if (failed.load(std::memory_order_acquire)) {
std::lock_guard lock(mutex);
@@ -475,7 +475,7 @@ void Driver::execute(const Submission& submission) {
try {
std::string rejected;
const auto verdict = draw(queue, packet, submission, rejected);
const auto verdict = draw(queue, packet, submission, cursor, rejected);
drawn = verdict == DrawVerdict::Drawn;
CaptureTrace::Log("draw submission=%llu queue=%x offset=%zu target=%llx mask=%x verdict=%d reason=%.256s", static_cast<unsigned long long>(submission.serial), submission.queue, cursor, static_cast<unsigned long long>(color), readRegister(queue.context, 0x8e), static_cast<int>(verdict), rejected.c_str());
if (verdict == DrawVerdict::Rejected) {
@@ -569,8 +569,8 @@ void Driver::execute(const Submission& submission) {
return;
}
if ((std::atomic_ref<std::uint32_t>(*const_cast<std::uint32_t*>(submission.rewindTail - 1)).load(std::memory_order_acquire) & 0x80000000u) != 0) {
DrawPipeline::Queue0().Enqueue([this] {
submitOpenWork();
DrawPipeline::Queue0().Enqueue([this, progress = submission.progress] {
submitOpenWork(progress);
wakeQueueZeroForCompletions();
}, {});
endTiming.Finish();
@@ -594,7 +594,10 @@ void Driver::execute(const Submission& submission) {
if (localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
}
endTiming.Finish();
if (submission.rewindTail != nullptr) executeRewindTail(submission);
if (submission.rewindTail != nullptr) {
if (submission.progress) submitOpenWork(submission.progress);
executeRewindTail(submission);
}
}
}
@@ -12,19 +12,35 @@ namespace AgcDriver::DriverDetail {
std::uint32_t Driver::ReserveEopFlip(const FlipInfo& info) {
std::lock_guard lock(mutex);
require(nextEopToken <= EopFlipTokenLimit, "EOP flip token space exhausted");
return reserveEopFlip(info, nextEopToken);
}
std::uint32_t Driver::ReserveEopFlipAt(const FlipInfo& info, std::uint32_t token) {
std::lock_guard lock(mutex);
return reserveEopFlip(info, token);
}
std::uint32_t Driver::reserveEopFlip(const FlipInfo& info, std::uint32_t token) {
rethrowFailure();
checkStopping();
require(nextEopToken <= EopFlipTokenLimit, "EOP flip token space exhausted");
require(token >= EopFlipTokenBase && token <= EopFlipTokenLimit && !eopFlips.contains(token), "EOP flip token is not free");
const auto output = outputs.find(info.handle);
require(output != outputs.end(), "EOP flip references an unregistered video output");
auto request = output->second->TryReserve(info);
if (!request) return 0;
const auto token = nextEopToken;
eopFlips.emplace(token, EopFlip{info.handle, std::move(request)});
++nextEopToken;
nextEopToken = std::max(nextEopToken, token + 1);
return token;
}
std::vector<std::uint32_t> Driver::PendingEopFlipTokens() {
std::lock_guard lock(mutex);
std::vector<std::uint32_t> tokens;
for (const auto& [token, flip] : eopFlips) tokens.push_back(token);
return tokens;
}
void Driver::RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output) {
require(output != nullptr, "null video output");
std::lock_guard lock(mutex);
@@ -70,19 +70,23 @@ void Driver::deferCompletion(const Submission& submission) {
queue0Uncommitted.push_back(received);
}
completionDeferred() = true;
DrawPipeline::Queue0().Enqueue([this, serial = submission.serial, received, labelWrites = submission.labelWrites] {
completeSubmission(serial, received, labelWrites);
DrawPipeline::Queue0().Enqueue([this, serial = submission.serial, received, labelWrites = submission.labelWrites, progress = submission.progress] {
completeSubmission(serial, received, labelWrites, progress);
}, {});
}
void Driver::submitOpenWork() {
if (deferredLabels().labels.empty() && !Graphics::Recorder::PendingLabelSince().has_value() && Graphics::Recorder::RecordedWorkSinceSubmit() == 0) return;
void Driver::submitOpenWork(const std::shared_ptr<SubmissionProgress>& progress) {
const bool work = !deferredLabels().labels.empty() || Graphics::Recorder::PendingLabelSince().has_value() || Graphics::Recorder::RecordedWorkSinceSubmit() != 0;
if (!work && !progress) return;
++submissionCosts(0).endSubmits;
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::End);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
const auto localDevice = device.Load();
recordDeferredLabels(localDevice.get(), 0);
if (localDevice != nullptr) localDevice->SubmitRecorded(true);
if (work) {
recordDeferredLabels(localDevice.get(), 0);
if (localDevice != nullptr) localDevice->SubmitRecorded(true);
}
if (progress) progress->Publish(localDevice, localDevice ? localDevice->SubmittedEpoch() : 0);
}
void Driver::wakeQueueZeroForCompletions() {
@@ -91,7 +95,7 @@ void Driver::wakeQueueZeroForCompletions() {
changed.notify_all();
}
void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites) {
void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites, const std::shared_ptr<SubmissionProgress>& progress) {
struct Complete {
Driver& driver;
std::uint64_t serial, received;
@@ -109,7 +113,7 @@ void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, st
if (notify) driver.changed.notify_all();
}
} complete{*this, serial, received, labelWrites};
submitOpenWork();
submitOpenWork(progress);
}
const std::atomic<std::uint64_t>*& Driver::workerQueued() {
@@ -261,6 +265,7 @@ void Driver::run(std::uint32_t id) noexcept {
if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - submission.enqueuedAt).count());
}
execute(submission);
if (submission.progress && submission.rewindTail == nullptr && !completionDeferred()) submitOpenWork(submission.progress);
if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast<unsigned long long>(submission.serial), id);
const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
PerformanceContext completionContext(id == 0 ? frameTiming() : FrameTiming::Async());
@@ -1,3 +1,4 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp"
@@ -21,6 +22,61 @@ namespace AgcDriver::DriverDetail {
namespace {
bool& submitsQueue0() {
struct SubmitsQueue0Storage {};
return HostThreadLocal<bool, SubmitsQueue0Storage>();
}
std::uint64_t& flipSubmissionsOfThread() {
struct FlipSubmissionsStorage {};
return HostThreadLocal<std::uint64_t, FlipSubmissionsStorage>();
}
bool progressEligible(const Submission& submission) {
if (submission.queue != 0 || !submission.flips.empty()
|| !submission.renderingWaits.empty() || !submission.conditionalEnds.empty()) return false;
bool terminalRelease = false;
std::optional<Pm4::LabelWrite> knownWrite;
for (std::size_t at = 0; at < submission.commands.size();) {
const auto packet = std::span(submission.commands).subspan(at);
const auto count = Pm4::PacketWords(packet[0]);
if (count == 0 || count > packet.size()) return false;
const auto opcode = (packet[0] >> 8u) & 0xffu;
const bool type3 = (packet[0] >> 30u) == 3;
if (type3 && (opcode == 0x3c || opcode == 0x93)) {
if (!knownWrite || Pm4::Predicated(packet[0]) || ((packet[1] >> 4u) & 3u) != 1u) return false;
const auto wait = Pm4::DecodeWait(packet.first(count));
const auto bytes = knownWrite->Bytes();
if (wait.address < knownWrite->address) return false;
const auto offset = wait.address - knownWrite->address;
if (offset > bytes.size() || wait.bytes > bytes.size() - offset) return false;
std::uint64_t value = 0;
std::memcpy(&value, bytes.data() + offset, wait.bytes);
if (!Pm4::WaitComparesValue(packet.first(count), value)) return false;
terminalRelease = false;
at += count;
continue;
}
if (type3 && opcode == 0x59 && submission.rewindTail != nullptr && at + count == submission.commands.size()) {
at += count;
continue;
}
if (!Pm4::FillerPacket(packet[0]) && !(type3 && opcode == 0x10) && !Pm4::IsTagMarker(packet.first(count))) {
if (type3 && (opcode == 0x49 || opcode == 0x37)) {
const auto write = Pm4::DecodeLabelWrite(packet.first(count));
terminalRelease = opcode == 0x49 && write.has_value();
if (write && !Pm4::Predicated(packet[0]) && (opcode == 0x37 || (packet[2] >> 29u) < 3)) knownWrite = *write;
else if (opcode != 0x49 || (packet[2] >> 29u) != 0) knownWrite.reset();
} else {
terminalRelease = false;
if (!type3 || (opcode != 0x42 && opcode != 0x58)) knownWrite.reset();
}
}
at += count;
}
return terminalRelease;
}
std::uint64_t captureTime() {
return static_cast<std::uint64_t>(std::llround(TraceMs() * 1000.0));
}
@@ -38,11 +94,10 @@ bool EopRelease(std::span<const std::uint32_t> words) {
return words.size() >= 8 && (words[0] >> 30u) == 3u && Pm4::PacketWords(words[0]) == 8u && ((words[0] >> 8u) & 0xffu) == 0x49u && ((words[2] >> 24u) & 7u) == 2u;
}
bool& submitsQueue0() {
struct SubmitsQueue0Storage {};
return HostThreadLocal<bool, SubmitsQueue0Storage>();
}
std::uint64_t Driver::FlipSubmissions() const {
return flipSubmissionsOfThread();
}
std::deque<const std::uint32_t*>& Driver::releasedTails() {
@@ -367,6 +422,10 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) {
const auto validated = profile ? std::chrono::steady_clock::now() : start;
auto* const capture = SubmissionCapture::Active();
auto captured = capture != nullptr ? SubmissionCapture::Prepare(queue, SubmissionKind::Submit, submission.commands, submission.conditionalEnds) : CapturedSubmission{};
static const bool progressEnabled = std::getenv("APS5_SUBMIT_PROGRESS") != nullptr;
const auto progress = progressEnabled && progressEligible(submission) ? std::make_shared<SubmissionProgress>() : nullptr;
submission.progress = progress;
const auto progressDeadline = progress ? std::chrono::steady_clock::now() + std::chrono::milliseconds(2) : std::chrono::steady_clock::time_point{};
{
std::unique_lock lock(mutex);
rethrowFailure();
@@ -402,11 +461,31 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) {
submission.holdsFlip = true;
flipsAhead.fetch_add(1, std::memory_order_acq_rel);
}
if (!submission.flips.empty()) ++flipSubmissionsOfThread();
enqueue(std::move(submission));
++accepted;
}
changed.notify_all();
if (capture != nullptr) writeCaptured(*this, *capture, captured);
if (progress) {
PerformanceTimer timing("Driver.SubmitProgress");
const auto [localDevice, epoch] = progress->WaitUntil(progressDeadline);
const auto now = std::chrono::steady_clock::now();
bool gpuReady = false;
if (localDevice && epoch != 0 && now < progressDeadline) {
const auto remaining = std::chrono::duration_cast<std::chrono::nanoseconds>(progressDeadline - now).count();
gpuReady = localDevice->WaitRecordedFor(epoch, static_cast<std::uint64_t>(remaining));
}
if (profile) {
static std::atomic<std::uint64_t> eligible{0}, ready{0}, expired{0};
if (gpuReady) ready.fetch_add(1, std::memory_order_relaxed);
else expired.fetch_add(1, std::memory_order_relaxed);
const auto count = eligible.fetch_add(1, std::memory_order_relaxed) + 1;
if (count % 1000 == 0) AgcDriver::ProfilePrint_nid_no_patch("[submit-progress] %llu eligible terminal releases, %llu GPU-ready returns, %llu expired or unsupported\n",
static_cast<unsigned long long>(count), static_cast<unsigned long long>(ready.load()), static_cast<unsigned long long>(expired.load()));
}
CheckFailure();
}
}
void Driver::SuspendPoint() {
@@ -466,7 +545,18 @@ bool Driver::orderReleased(std::uint32_t queue, std::uint64_t received) const {
return runningWorkers.load(std::memory_order_acquire) == 0 && !completionsPending() && !Graphics::Recorder::SnapshotWriteOverlaps(awaited, 4);
}
void Driver::noteWaitFields(std::uint32_t queue, std::span<const std::uint32_t> packet) {
const auto fields = Pm4::DecodeWait(packet);
auto& slot = awaitedSlots[queue == 0 ? 0 : queue - 0x20 + 1];
slot.reference.store(fields.reference, std::memory_order_relaxed);
slot.mask.store(fields.mask, std::memory_order_relaxed);
slot.shape.store(static_cast<std::uint64_t>(fields.function) | static_cast<std::uint64_t>(fields.bytes) << 32u, std::memory_order_relaxed);
}
void Driver::noteWaitBlocked(std::uint32_t queue, std::uint64_t awaited, bool blocked) {
auto& slot = awaitedSlots[queue == 0 ? 0 : queue - 0x20 + 1];
if (!blocked) slot.shape.store(0, std::memory_order_relaxed);
slot.address.store(blocked ? awaited : 0, std::memory_order_release);
if (blocked) {
if (runningWorkers.fetch_sub(1, std::memory_order_acq_rel) == 1) {
idleEpoch.fetch_add(1, std::memory_order_release);
@@ -1702,7 +1702,7 @@ void PrepareRegisteredInBackground(std::shared_ptr<const ShaderSnapshot> snapsho
try {
entries = PrepareRegistered(*plan);
} catch (const std::exception& error) {
APS5_LOG_ERR("Shader 0x%llx could not be prepared after registration: %s", static_cast<unsigned long long>(snapshot->codeAddress), error.what());
APS5_LOG_ERR("Shader 0x%llx snapshot=0x%llx could not be prepared after registration: %s", static_cast<unsigned long long>(snapshot->codeAddress), static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(snapshot.get())), error.what());
failure = std::current_exception();
} catch (...) {
failure = std::current_exception();
@@ -2111,6 +2111,37 @@ std::uint64_t NullPixelProgramAddress() {
return reinterpret_cast<std::uintptr_t>(NullPixelCode);
}
alignas(256) static const std::uint32_t DepthCopyMultisamplePixelCode[64] = {0x360604ffu, 0x0000ffffu, 0x2c080490u, 0x7e0a0208u, 0xf0000130u, 0x00000603u, 0xbf8c3f70u, 0xf8001801u, 0x00000006u, 0xbf810000u};
alignas(256) static const std::uint32_t DepthCopySinglePixelCode[64] = {0x360604ffu, 0x0000ffffu, 0x2c080490u, 0xf0000108u, 0x00000603u, 0xbf8c3f70u, 0xf8001801u, 0x00000006u, 0xbf810000u};
static const Shader DepthCopyMultisamplePixelShader = [] {
Shader shader{};
shader.file_header = 0x34333231u;
shader.version = 0x18u;
shader.code = DepthCopyMultisamplePixelCode;
shader.header_size = sizeof(Shader);
shader.shader_size = sizeof(DepthCopyMultisamplePixelCode);
shader.type = 1;
return shader;
}();
static const Shader DepthCopySinglePixelShader = [] {
Shader shader{};
shader.file_header = 0x34333231u;
shader.version = 0x18u;
shader.code = DepthCopySinglePixelCode;
shader.header_size = sizeof(Shader);
shader.shader_size = sizeof(DepthCopySinglePixelCode);
shader.type = 1;
return shader;
}();
std::uint64_t DepthCopyPixelProgramAddress(bool multisampled) {
return reinterpret_cast<std::uintptr_t>(multisampled ? DepthCopyMultisamplePixelCode : DepthCopySinglePixelCode);
}
std::uint32_t DepthCopyPixelUserWords(bool multisampled) {
return multisampled ? 9u : 8u;
}
void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
BindTitleThread();
PerformanceContext timingContext(FrameTiming::Preparation());
@@ -2251,6 +2282,25 @@ ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device) {
return null;
}
ShaderSnapshot PrepareDepthCopyPixelProgram(bool multisampled) {
const auto& shader = multisampled ? DepthCopyMultisamplePixelShader : DepthCopySinglePixelShader;
const auto* code = multisampled ? DepthCopyMultisamplePixelCode : DepthCopySinglePixelCode;
ShaderSnapshot copy{DepthCopyPixelProgramAddress(multisampled), reinterpret_cast<std::uintptr_t>(&shader), shader.type, {}, {}};
copy.code.assign(code, code + 64);
copy.header.resize(sizeof(Shader));
std::memcpy(copy.header.data(), &shader, sizeof(Shader));
auto state = DecodeRegisteredState(copy);
state.shader.insert_or_assign(0x008u, static_cast<std::uint32_t>(copy.codeAddress >> 8u));
state.shader.insert_or_assign(0x009u, static_cast<std::uint32_t>(copy.codeAddress >> 40u));
state.shader.insert_or_assign(0x00bu, DepthCopyPixelUserWords(multisampled) << 1u);
state.context.insert_or_assign(0x1b3u, 0x8002u);
state.context.insert_or_assign(0x1b4u, 0x8002u);
state.context.insert_or_assign(0x1c5u, 1u);
copy.registeredState = std::make_shared<const RegisteredShaderState>(std::move(state));
copy.prepared->deferred = true;
return copy;
}
std::shared_ptr<const ShaderRegistry> Driver::RegisteredShaders() {
std::lock_guard lock(mutex);
return shaders;
@@ -2304,6 +2354,18 @@ void Driver::RegisterShader(const Shader* shader) {
localDevice = device;
}
snapshot.registeredState = std::make_shared<const RegisteredShaderState>(DecodeRegisteredState(snapshot));
{
std::lock_guard lock(mutex);
if (shaders != nullptr) {
if (const auto found = shaders->find(snapshot.codeAddress); found != shaders->end()) {
for (const auto& current : found->second) {
if (current->registeredState == nullptr || current->type != snapshot.type || current->code != snapshot.code || *current->registeredState != *snapshot.registeredState) continue;
snapshot.registeredState = current->registeredState;
break;
}
}
}
}
QueueState registered{};
registered.shader = snapshot.registeredState->shader;
registered.context = snapshot.registeredState->context;
@@ -2347,6 +2409,9 @@ void Driver::RegisterShader(const Shader* shader) {
if (shaders->find(NullPixelProgramAddress()) == shaders->end()) {
PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(PrepareNullPixelProgram(*localDevice)));
}
for (const bool multisampled : {false, true}) {
if (shaders->find(DepthCopyPixelProgramAddress(multisampled)) == shaders->end()) PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(PrepareDepthCopyPixelProgram(multisampled)));
}
transaction.Commit();
if (preparation != nullptr) PrepareRegisteredInBackground(published, std::move(preparation));
}
@@ -322,6 +322,7 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
if (entryTryFailed && pollService(false)) return;
noteWaitFields(queue, packet);
noteWaitBlocked(queue, awaited, true);
struct BlockedWait {
Driver& driver;
@@ -0,0 +1,455 @@
#include "prx/libSceAgcDriver/Execution/include/FrameCapture.hpp"
#include <zstd.h>
#include <algorithm>
#include <array>
#include <bit>
#include <cstring>
#include <future>
#include <iterator>
#include <stdexcept>
#include <system_error>
#include <thread>
namespace AgcDriver {
namespace {
static_assert(std::endian::native == std::endian::little, "frame captures are stored in little-endian order");
constexpr std::size_t ChunkHeaderBytes = 64;
void requireCapture(bool condition, const std::string& reason) {
if (!condition) throw std::runtime_error("frame capture: " + reason);
}
class Encoder {
public:
template <typename TValue>
void Put(TValue value) {
const auto at = bytes.size();
bytes.resize(at + sizeof(TValue));
std::memcpy(bytes.data() + at, &value, sizeof(TValue));
}
std::vector<std::byte> bytes;
};
class Decoder {
public:
explicit Decoder(std::span<const std::byte> bytes) : bytes(bytes) {}
template <typename TValue>
TValue Take(const char* what) {
requireCapture(sizeof(TValue) <= bytes.size() - at, std::string("record ends inside ") + what);
TValue value{};
std::memcpy(&value, bytes.data() + at, sizeof(TValue));
at += sizeof(TValue);
return value;
}
std::size_t Count(std::size_t elementBytes, const char* what) {
const auto count = Take<std::uint32_t>(what);
requireCapture(count <= (bytes.size() - at) / elementBytes, std::string("count of ") + what + " exceeds its record");
return count;
}
bool Done() const { return at == bytes.size(); }
private:
std::span<const std::byte> bytes;
std::size_t at = 0;
};
void putRegisters(Encoder& encoder, const std::vector<ShaderRegister>& registers) {
encoder.Put(static_cast<std::uint32_t>(registers.size()));
for (const auto& value : registers) {
encoder.Put(value.offset);
encoder.Put(value.value);
}
}
std::vector<ShaderRegister> takeRegisters(Decoder& decoder) {
std::vector<ShaderRegister> registers(decoder.Count(8, "registers"));
for (auto& value : registers) {
value.offset = decoder.Take<std::uint32_t>("register offset");
value.value = decoder.Take<std::uint32_t>("register value");
}
return registers;
}
std::vector<std::byte> encodeCall(const FrameCall& call) {
Encoder encoder;
encoder.Put(call.ordinal);
encoder.Put(call.timeNs);
encoder.Put(call.thread);
encoder.Put(call.accepted);
encoder.Put(call.completed);
encoder.Put(call.queue);
encoder.Put(call.flags);
encoder.Put(static_cast<std::uint32_t>(call.cpuFlip ? 1u : 0u));
encoder.Put(call.address);
encoder.Put(call.size);
encoder.Put(static_cast<std::uint32_t>(call.values.size()));
for (const auto value : call.values) encoder.Put(value);
putRegisters(encoder, call.context);
putRegisters(encoder, call.primitive);
return std::move(encoder.bytes);
}
FrameCall decodeCall(FrameCallKind kind, std::span<const std::byte> payload) {
Decoder decoder(payload);
FrameCall call;
call.kind = kind;
call.ordinal = decoder.Take<std::uint64_t>("ordinal");
call.timeNs = decoder.Take<std::uint64_t>("time");
call.thread = decoder.Take<std::uint64_t>("thread");
call.accepted = decoder.Take<std::uint64_t>("accepted serial");
call.completed = decoder.Take<std::uint64_t>("completed serial");
call.queue = decoder.Take<std::uint32_t>("queue");
call.flags = decoder.Take<std::uint32_t>("flags");
const auto cpuFlip = decoder.Take<std::uint32_t>("cpu flip");
requireCapture(cpuFlip <= 1, "cpu flip is not 0 or 1");
call.cpuFlip = cpuFlip == 1;
call.address = decoder.Take<std::uint64_t>("address");
call.size = decoder.Take<std::uint64_t>("size");
call.values.resize(decoder.Count(8, "values"));
for (auto& value : call.values) value = decoder.Take<std::uint64_t>("value");
call.context = takeRegisters(decoder);
call.primitive = takeRegisters(decoder);
requireCapture(decoder.Done(), "call record has trailing bytes");
return call;
}
bool knownCall(std::uint32_t kind) {
return kind >= static_cast<std::uint32_t>(FrameCallKind::RegisterShader) && kind <= static_cast<std::uint32_t>(FrameCallKind::UnregisterVideoOutput);
}
std::vector<std::byte> readFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
requireCapture(file.is_open(), "cannot open " + path.string());
std::vector<std::byte> bytes;
file.seekg(0, std::ios::end);
bytes.resize(static_cast<std::size_t>(file.tellg()));
file.seekg(0);
file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
requireCapture(file.good() || bytes.empty(), "cannot read " + path.string());
return bytes;
}
void requireMagic(std::span<const std::byte> bytes, std::string_view magic, const std::filesystem::path& path) {
requireCapture(bytes.size() >= magic.size() && std::memcmp(bytes.data(), magic.data(), magic.size()) == 0, path.string() + " is not a frame capture file");
}
struct Run {
std::uint64_t address;
std::span<const std::byte> bytes;
};
}
bool AllZero(std::span<const std::byte> bytes) {
constexpr std::size_t Word = sizeof(std::uint64_t);
std::size_t at = 0;
for (; at + Word <= bytes.size(); at += Word) {
std::uint64_t value = 0;
std::memcpy(&value, bytes.data() + at, Word);
if (value != 0) return false;
}
return std::all_of(bytes.begin() + static_cast<std::ptrdiff_t>(at), bytes.end(), [](std::byte value) { return value == std::byte{0}; });
}
std::string_view FrameCallName(FrameCallKind kind) {
switch (kind) {
case FrameCallKind::RegisterShader: return "register-shader";
case FrameCallKind::ResolveShaderAbi: return "resolve-shader-abi";
case FrameCallKind::ResolveGraphicsAbi: return "resolve-graphics-abi";
case FrameCallKind::ResolveGraphicsStagesAbi: return "resolve-graphics-stages-abi";
case FrameCallKind::RegisterVideoOutput: return "register-video-output";
case FrameCallKind::UnregisterVideoOutput: return "unregister-video-output";
case FrameCallKind::SetTessellationFactorRing: return "set-tf-ring";
case FrameCallKind::Submit: return "submit";
case FrameCallKind::Flip: return "flip";
case FrameCallKind::ReserveEopFlip: return "reserve-eop-flip";
case FrameCallKind::Suspend: return "suspend";
case FrameCallKind::Memory: return "memory";
case FrameCallKind::FrameStart: return "frame-start";
case FrameCallKind::End: return "end";
case FrameCallKind::DisplayBuffer: return "display-buffer";
}
throw std::runtime_error("frame capture: unknown call kind " + std::to_string(static_cast<std::uint32_t>(kind)));
}
FrameCaptureWriter::FrameCaptureWriter(const std::filesystem::path& directory, int level) : directory(directory), level(level) {
std::error_code error;
std::filesystem::create_directories(directory, error);
requireCapture(!error && std::filesystem::is_directory(directory), "cannot create the directory " + directory.string());
for (const auto name : {CallsName, MemoryName, HeaderName}) std::filesystem::remove(directory / name, error);
calls.open(directory / CallsName, std::ios::binary | std::ios::trunc);
memory.open(directory / MemoryName, std::ios::binary | std::ios::trunc);
requireCapture(calls.is_open() && memory.is_open(), "cannot create the capture files in " + directory.string());
calls.write(CallsMagic.data(), static_cast<std::streamsize>(CallsMagic.size()));
memory.write(MemoryMagic.data(), static_cast<std::streamsize>(MemoryMagic.size()));
requireCapture(calls.good() && memory.good(), "cannot write the capture files in " + directory.string());
}
void FrameCaptureWriter::WriteCall(const FrameCall& call) {
FrameCallName(call.kind);
const auto payload = encodeCall(call);
requireCapture(payload.size() <= UINT32_MAX, "call record too large");
std::lock_guard lock(mutex);
requireCapture(!finished, "call written after the capture finished");
const auto kind = static_cast<std::uint32_t>(call.kind);
const auto bytes = static_cast<std::uint32_t>(payload.size());
calls.write(reinterpret_cast<const char*>(&kind), sizeof(kind));
calls.write(reinterpret_cast<const char*>(&bytes), sizeof(bytes));
calls.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
requireCapture(calls.good(), "cannot write " + (directory / CallsName).string());
}
void FrameCaptureWriter::WriteRange(std::uint64_t address, std::uint64_t bytes, FrameRangeKind kind, std::uint32_t protection, std::uint32_t access) {
FrameMemoryChunk chunk;
chunk.kind = FrameMemoryKind::Range;
chunk.rangeKind = kind;
chunk.protection = protection;
chunk.access = access;
chunk.address = address;
chunk.bytes = bytes;
std::lock_guard lock(mutex);
writeChunk(chunk, {});
}
std::uint64_t FrameCaptureWriter::WriteSnapshot(std::uint64_t address, std::span<const std::byte> bytes) {
std::vector<Run> runs;
for (std::size_t at = 0; at < bytes.size();) {
const auto block = bytes.subspan(at, std::min(BlockBytes, bytes.size() - at));
if (AllZero(block)) {
at += block.size();
continue;
}
if (!runs.empty() && runs.back().address + runs.back().bytes.size() == address + at && runs.back().bytes.size() + block.size() <= ChunkBytes) {
runs.back().bytes = bytes.subspan(runs.back().address - address, runs.back().bytes.size() + block.size());
} else {
runs.push_back({address + at, block});
}
at += block.size();
}
const auto workers = std::max<std::size_t>(1, std::thread::hardware_concurrency() / 2);
std::uint64_t stored = 0;
for (std::size_t first = 0; first < runs.size(); first += workers) {
const auto last = std::min(runs.size(), first + workers);
std::vector<std::future<std::vector<std::byte>>> compressed;
for (auto index = first; index < last; ++index) {
compressed.push_back(std::async(std::launch::async, [this, run = runs[index]] { return compress(run.bytes); }));
}
std::lock_guard lock(mutex);
for (auto index = first; index < last; ++index) {
const auto payload = compressed[index - first].get();
FrameMemoryChunk chunk;
chunk.kind = FrameMemoryKind::Snapshot;
chunk.address = runs[index].address;
chunk.bytes = runs[index].bytes.size();
chunk.compressedBytes = payload.size();
writeChunk(chunk, payload);
stored += chunk.bytes;
}
}
return stored;
}
void FrameCaptureWriter::WriteDelta(std::uint64_t group, std::uint64_t completed, std::uint64_t address, std::span<const std::byte> bytes) {
for (std::size_t at = 0; at < bytes.size(); at += ChunkBytes) {
const auto part = bytes.subspan(at, std::min(ChunkBytes, bytes.size() - at));
const auto payload = compress(part);
FrameMemoryChunk chunk;
chunk.kind = FrameMemoryKind::Delta;
chunk.group = group;
chunk.completed = completed;
chunk.address = address + at;
chunk.bytes = part.size();
chunk.compressedBytes = payload.size();
std::lock_guard lock(mutex);
writeChunk(chunk, payload);
}
}
void FrameCaptureWriter::WriteReference(std::uint64_t index, std::uint64_t address, std::span<const std::byte> pixels) {
const auto payload = compress(pixels);
FrameMemoryChunk chunk;
chunk.kind = FrameMemoryKind::Reference;
chunk.group = index;
chunk.address = address;
chunk.bytes = pixels.size();
chunk.compressedBytes = payload.size();
std::lock_guard lock(mutex);
writeChunk(chunk, payload);
}
void FrameCaptureWriter::Finish(std::string_view header) {
std::lock_guard lock(mutex);
requireCapture(!finished, "capture finished twice");
finished = true;
calls.flush();
memory.flush();
requireCapture(calls.good() && memory.good(), "cannot flush the capture files in " + directory.string());
calls.close();
memory.close();
std::ofstream file(directory / HeaderName, std::ios::binary | std::ios::trunc);
file.write(header.data(), static_cast<std::streamsize>(header.size()));
file.flush();
requireCapture(file.good(), "cannot write " + (directory / HeaderName).string());
}
std::uint64_t FrameCaptureWriter::RawBytes() const {
std::lock_guard lock(mutex);
return rawBytes;
}
std::uint64_t FrameCaptureWriter::StoredBytes() const {
std::lock_guard lock(mutex);
return storedBytes;
}
const std::filesystem::path& FrameCaptureWriter::Directory() const {
return directory;
}
void FrameCaptureWriter::writeChunk(const FrameMemoryChunk& chunk, std::span<const std::byte> payload) {
requireCapture(!finished, "memory written after the capture finished");
Encoder encoder;
encoder.Put(static_cast<std::uint32_t>(chunk.kind));
encoder.Put(static_cast<std::uint32_t>(chunk.rangeKind));
encoder.Put(chunk.protection);
encoder.Put(chunk.access);
encoder.Put(chunk.group);
encoder.Put(chunk.completed);
encoder.Put(chunk.address);
encoder.Put(chunk.bytes);
encoder.Put(static_cast<std::uint64_t>(payload.size()));
encoder.Put(std::uint64_t{0});
memory.write(reinterpret_cast<const char*>(encoder.bytes.data()), static_cast<std::streamsize>(encoder.bytes.size()));
memory.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
requireCapture(memory.good(), "cannot write " + (directory / MemoryName).string());
if (chunk.kind != FrameMemoryKind::Range) rawBytes += chunk.bytes;
storedBytes += payload.size();
}
std::vector<std::byte> FrameCaptureWriter::compress(std::span<const std::byte> bytes) const {
std::vector<std::byte> output(ZSTD_compressBound(bytes.size()));
const auto written = ZSTD_compress(output.data(), output.size(), bytes.data(), bytes.size(), level);
requireCapture(ZSTD_isError(written) == 0, std::string("zstd: ") + ZSTD_getErrorName(written));
output.resize(written);
return output;
}
FrameCaptureReader::FrameCaptureReader(const std::filesystem::path& directory) : directory(directory) {
const auto callsPath = directory / FrameCaptureWriter::CallsName;
const auto callBytes = readFile(callsPath);
requireMagic(callBytes, FrameCaptureWriter::CallsMagic, callsPath);
std::size_t at = FrameCaptureWriter::CallsMagic.size();
while (at < callBytes.size()) {
if (callBytes.size() - at < 8) {
truncated = true;
break;
}
std::uint32_t kind = 0;
std::uint32_t bytes = 0;
std::memcpy(&kind, callBytes.data() + at, 4);
std::memcpy(&bytes, callBytes.data() + at + 4, 4);
requireCapture(knownCall(kind), callsPath.string() + ": unknown call kind " + std::to_string(kind) + " at byte " + std::to_string(at));
if (callBytes.size() - at - 8 < bytes) {
truncated = true;
break;
}
calls.push_back(decodeCall(static_cast<FrameCallKind>(kind), std::span(callBytes).subspan(at + 8, bytes)));
at += 8 + static_cast<std::size_t>(bytes);
}
const auto memoryPath = directory / FrameCaptureWriter::MemoryName;
std::ifstream memory(memoryPath, std::ios::binary);
requireCapture(memory.is_open(), "cannot open " + memoryPath.string());
memory.seekg(0, std::ios::end);
const auto size = static_cast<std::uint64_t>(memory.tellg());
std::string magic(FrameCaptureWriter::MemoryMagic.size(), '\0');
memory.seekg(0);
memory.read(magic.data(), static_cast<std::streamsize>(magic.size()));
requireCapture(memory.good() && magic == FrameCaptureWriter::MemoryMagic, memoryPath.string() + " is not a frame capture file");
std::uint64_t offset = magic.size();
while (offset < size) {
if (size - offset < ChunkHeaderBytes) {
truncated = true;
break;
}
std::array<std::byte, ChunkHeaderBytes> raw{};
memory.seekg(static_cast<std::streamoff>(offset));
memory.read(reinterpret_cast<char*>(raw.data()), static_cast<std::streamsize>(raw.size()));
requireCapture(memory.good(), "cannot read " + memoryPath.string());
Decoder decoder(raw);
FrameMemoryChunk chunk;
const auto kind = decoder.Take<std::uint32_t>("memory kind");
const auto rangeKind = decoder.Take<std::uint32_t>("range kind");
requireCapture(kind >= 1 && kind <= 4 && rangeKind >= 1 && rangeKind <= 6, memoryPath.string() + ": corrupt chunk header at byte " + std::to_string(offset));
chunk.kind = static_cast<FrameMemoryKind>(kind);
chunk.rangeKind = static_cast<FrameRangeKind>(rangeKind);
chunk.protection = decoder.Take<std::uint32_t>("protection");
chunk.access = decoder.Take<std::uint32_t>("access");
chunk.group = decoder.Take<std::uint64_t>("group");
chunk.completed = decoder.Take<std::uint64_t>("completed serial");
chunk.address = decoder.Take<std::uint64_t>("address");
chunk.bytes = decoder.Take<std::uint64_t>("bytes");
chunk.compressedBytes = decoder.Take<std::uint64_t>("compressed bytes");
requireCapture((chunk.kind == FrameMemoryKind::Range) == (chunk.compressedBytes == 0), memoryPath.string() + ": chunk payload does not match its kind at byte " + std::to_string(offset));
chunk.offset = offset + ChunkHeaderBytes;
if (size - chunk.offset < chunk.compressedBytes) {
truncated = true;
break;
}
chunks.push_back(chunk);
offset = chunk.offset + chunk.compressedBytes;
}
const auto headerPath = directory / FrameCaptureWriter::HeaderName;
if (std::filesystem::exists(headerPath)) {
const auto bytes = readFile(headerPath);
header.assign(reinterpret_cast<const char*>(bytes.data()), bytes.size());
} else {
truncated = true;
}
}
const std::vector<FrameCall>& FrameCaptureReader::Calls() const {
return calls;
}
const std::vector<FrameMemoryChunk>& FrameCaptureReader::Chunks() const {
return chunks;
}
const std::string& FrameCaptureReader::Header() const {
return header;
}
bool FrameCaptureReader::Truncated() const {
return truncated;
}
void FrameCaptureReader::Read(const FrameMemoryChunk& chunk, std::span<std::byte> destination) const {
requireCapture(chunk.kind != FrameMemoryKind::Range, "a range chunk has no bytes");
requireCapture(destination.size() == chunk.bytes, "destination size does not match the chunk");
const auto path = directory / FrameCaptureWriter::MemoryName;
std::ifstream memory(path, std::ios::binary);
requireCapture(memory.is_open(), "cannot open " + path.string());
std::vector<std::byte> compressed(chunk.compressedBytes);
memory.seekg(static_cast<std::streamoff>(chunk.offset));
memory.read(reinterpret_cast<char*>(compressed.data()), static_cast<std::streamsize>(compressed.size()));
requireCapture(memory.good(), "cannot read " + path.string());
const auto written = ZSTD_decompress(destination.data(), destination.size(), compressed.data(), compressed.size());
requireCapture(ZSTD_isError(written) == 0, std::string("zstd: ") + ZSTD_getErrorName(written));
requireCapture(written == chunk.bytes, "chunk at " + std::to_string(chunk.address) + " decompressed to the wrong size");
}
std::vector<std::byte> FrameCaptureReader::Read(const FrameMemoryChunk& chunk) const {
std::vector<std::byte> bytes(chunk.bytes);
Read(chunk, bytes);
return bytes;
}
}
@@ -0,0 +1,587 @@
#include "prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/TraceRevision.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/General.hpp"
#include <algorithm>
#include <array>
#include <charconv>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <functional>
#include <limits>
#include <set>
#include <stdexcept>
#include <thread>
#include <tuple>
#include <utility>
#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif
extern "C" int APS5_VABI sceAgcDriverGetTFRing(uintptr_t* base, uint32_t* size);
namespace AgcDriver {
namespace {
std::uint64_t readNumber(const char* text, std::uint64_t fallback) {
if (text == nullptr || *text == '\0') return fallback;
std::uint64_t value = 0;
const std::string_view view(text);
const auto [last, error] = std::from_chars(view.data(), view.data() + view.size(), value);
if (error != std::errc{} || last != view.data() + view.size()) throw std::runtime_error("frame capture: '" + std::string(view) + "' is not a number");
return value;
}
std::string jsonString(std::string_view text) {
std::string result = "\"";
for (const char character : text) {
if (character == '"' || character == '\\') {
result += '\\';
result += character;
} else if (static_cast<unsigned char>(character) < 0x20) {
char escaped[8];
std::snprintf(escaped, sizeof(escaped), "\\u%04x", static_cast<unsigned>(static_cast<unsigned char>(character)));
result += escaped;
} else {
result += character;
}
}
return result + "\"";
}
std::vector<std::pair<std::string, std::string>> captureEnvironment() {
#ifdef _WIN32
char** entries = _environ;
#else
char** entries = environ;
#endif
std::vector<std::pair<std::string, std::string>> result;
for (; entries != nullptr && *entries != nullptr; ++entries) {
const std::string_view entry = *entries;
const auto equals = entry.find('=');
if (!entry.starts_with("APS5_") || equals == std::string_view::npos) continue;
result.emplace_back(std::string(entry.substr(0, equals)), std::string(entry.substr(equals + 1)));
}
std::sort(result.begin(), result.end());
return result;
}
std::uint64_t threadId() {
return static_cast<std::uint64_t>(std::hash<std::thread::id>{}(std::this_thread::get_id()));
}
std::vector<ShaderRegister> copyRegisters(std::span<const ShaderRegister> registers) {
return {registers.begin(), registers.end()};
}
std::uint64_t pointer(const void* value) {
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(value));
}
std::uint64_t flipValue(std::int64_t value) {
return static_cast<std::uint64_t>(value);
}
std::uint64_t contentHash(std::span<const std::byte> bytes) {
std::uint64_t hash = 0xcbf29ce484222325ull;
for (const auto value : bytes) hash = (hash ^ static_cast<std::uint64_t>(value)) * 0x100000001b3ull;
return hash == 0 ? 1 : hash;
}
}
FrameRecorder* FrameRecorder::Active() {
static FrameRecorder* const recorder = []() -> FrameRecorder* {
const char* directory = std::getenv("APS5_FRAME_CAPTURE");
if (directory == nullptr || *directory == '\0') return nullptr;
const char* trigger = std::getenv("APS5_FRAME_CAPTURE_TRIGGER");
const auto atBoundary = readNumber(std::getenv("APS5_FRAME_CAPTURE_AT_FLIP"), 0);
const auto frames = readNumber(std::getenv("APS5_FRAME_CAPTURE_FRAMES"), DefaultFrames);
if (frames == 0) throw std::runtime_error("frame capture: APS5_FRAME_CAPTURE_FRAMES must be at least 1");
return new FrameRecorder(directory, trigger != nullptr ? trigger : "", atBoundary, frames);
}();
return recorder;
}
FrameRecorder::FrameRecorder(std::filesystem::path directory, std::filesystem::path trigger, std::uint64_t atBoundary, std::uint64_t frames)
: directory(std::move(directory)), trigger(std::move(trigger)), atBoundary(atBoundary), frames(frames), origin(std::chrono::steady_clock::now()) {}
FrameCall FrameRecorder::make(FrameCallKind kind) {
FrameCall call;
call.kind = kind;
call.timeNs = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - origin).count());
call.thread = threadId();
return call;
}
void FrameRecorder::RegisterShader(const Shader* shader) {
auto call = make(FrameCallKind::RegisterShader);
call.address = pointer(shader);
const std::array<const Shader*, 1> shaders{shader};
record(std::move(call), true, shaders);
}
void FrameRecorder::ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
auto call = make(FrameCallKind::ResolveShaderAbi);
call.address = pointer(shader);
call.context = copyRegisters(context);
call.primitive = copyRegisters(primitive);
const std::array<const Shader*, 1> shaders{shader};
record(std::move(call), true, shaders);
}
void FrameRecorder::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
auto call = make(FrameCallKind::ResolveGraphicsStagesAbi);
for (const auto* stage : stages) call.values.push_back(pointer(stage));
call.context = copyRegisters(context);
call.primitive = copyRegisters(primitive);
record(std::move(call), true, stages);
}
void FrameRecorder::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
auto call = make(FrameCallKind::ResolveGraphicsAbi);
call.values = {pointer(vertex), pointer(pixel), primitiveType};
const std::array<const Shader*, 2> shaders{vertex, pixel};
record(std::move(call), true, shaders);
}
void FrameRecorder::RegisterVideoOutput(std::uint32_t handle) {
auto call = make(FrameCallKind::RegisterVideoOutput);
call.values = {handle};
record(std::move(call), true);
}
void FrameRecorder::UnregisterVideoOutput(std::uint32_t handle) {
auto call = make(FrameCallKind::UnregisterVideoOutput);
call.values = {handle};
record(std::move(call), true);
}
void FrameRecorder::Submit(const Packet& packet, std::uint32_t queue) {
auto call = make(FrameCallKind::Submit);
call.queue = queue;
call.address = pointer(packet.addr);
call.size = packet.dw_num;
call.flags = packet.flags;
record(std::move(call), false);
}
void FrameRecorder::Flip(const FlipInfo& info) {
auto call = make(FrameCallKind::Flip);
call.cpuFlip = true;
call.values = {info.handle, flipValue(info.index), info.mode, flipValue(info.argument)};
record(std::move(call), false);
}
void FrameRecorder::ReserveEopFlip(const FlipInfo& info, std::uint32_t token) {
auto call = make(FrameCallKind::ReserveEopFlip);
call.values = {info.handle, flipValue(info.index), info.mode, flipValue(info.argument), token};
std::lock_guard lock(mutex);
if (state == State::Waiting) {
call.ordinal = ++ordinal;
reservations.push_back(std::move(call));
if (reservations.size() > ReservationHistory) reservations.pop_front();
return;
}
if (state != State::Recording) return;
emitDelta();
if (state != State::Recording) return;
call.ordinal = ++ordinal;
std::tie(call.accepted, call.completed) = DriverDetail::Driver::Get().SerialProgress();
writer->WriteCall(call);
++recordedCalls;
}
void FrameRecorder::Suspend() {
record(make(FrameCallKind::Suspend), false);
}
void FrameRecorder::PresentBuffer(const DisplayBuffer& buffer) {
std::lock_guard lock(displayMutex);
displayBuffers[buffer.address] = buffer;
}
void FrameRecorder::writeReferenceFrames() {
std::map<std::uint64_t, DisplayBuffer> presented;
{
std::lock_guard lock(displayMutex);
presented = displayBuffers;
}
if (presented.empty()) return;
if (!DriverDetail::Driver::Get().WaitIdleFor(std::chrono::seconds(10))) {
warnings.push_back("no reference frames: the driver did not go idle after the last frame");
return;
}
std::uint64_t index = 0;
for (const auto& [address, buffer] : presented) {
try {
const auto pixels = ReadDisplayBuffer(buffer);
writer->WriteReference(index, address, pixels);
} catch (const std::exception& failure) {
warnings.push_back(std::string("no reference frame for a display buffer: ") + failure.what());
continue;
}
auto call = make(FrameCallKind::DisplayBuffer);
call.ordinal = ++ordinal;
call.address = buffer.address;
call.values = {index, buffer.pixelFormat, buffer.width, buffer.height, buffer.tilingMode, buffer.pitchInPixel, buffer.dccAddress, buffer.dccClearColor, buffer.alignment};
writer->WriteCall(call);
++index;
}
}
void FrameRecorder::record(FrameCall call, bool prologueCall, std::span<const Shader* const> shaders) {
std::lock_guard lock(mutex);
if (state == State::Waiting) {
if (!prologueCall) return;
call.ordinal = ++ordinal;
prologue.push_back(std::move(call));
return;
}
if (state != State::Recording) return;
if (prologueCall) emitShaderDelta(shaders);
else emitDelta();
if (state != State::Recording) return;
call.ordinal = ++ordinal;
std::tie(call.accepted, call.completed) = DriverDetail::Driver::Get().SerialProgress();
writer->WriteCall(call);
++recordedCalls;
}
void FrameRecorder::FrameBoundary() {
std::lock_guard lock(mutex);
++boundaries;
if (state == State::Recording) {
if (++recordedFrames >= frames) finish();
return;
}
if (state == State::Waiting && triggered()) start();
}
bool FrameRecorder::triggered() {
if (!trigger.empty()) {
std::error_code error;
if (!std::filesystem::exists(trigger, error)) return false;
std::ifstream file(trigger);
std::uint64_t requested = 0;
if (file >> requested && requested != 0) frames = requested;
return true;
}
return boundaries >= std::max<std::uint64_t>(atBoundary, 1);
}
void FrameRecorder::emitDelta() {
std::vector<Span> spans;
for (const auto& range : ranges) {
if (range.kind == FrameRangeKind::Watched || range.kind == FrameRangeKind::Unwatched) spans.push_back({range.address, range.bytes, generation});
}
const auto mark = GuestMemory::WriteGeneration();
const auto added = newRanges();
writeDelta(spans, {}, added);
generation = mark;
shaderMarks.clear();
}
std::vector<FrameRecorder::Range> FrameRecorder::newRanges() {
const auto current = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
if (current == allocationsGeneration) return {};
allocationsGeneration = current;
std::map<std::uint64_t, std::uint64_t> covered;
for (const auto& range : ranges) {
auto& end = covered[range.address];
end = std::max(end, range.address + range.bytes);
}
std::vector<Range> added;
for (const auto& range : committedRanges()) {
const auto end = range.address + range.bytes;
for (auto at = range.address; at < end;) {
const auto next = covered.upper_bound(at);
if (next != covered.begin() && std::prev(next)->second > at) {
at = std::prev(next)->second;
continue;
}
auto piece = range;
piece.address = at;
piece.bytes = (next == covered.end() ? end : std::min(end, next->first)) - at;
added.push_back(piece);
at += piece.bytes;
}
}
return added;
}
void FrameRecorder::emitShaderDelta(std::span<const Shader* const> shaders) {
std::set<std::uint64_t> blocks;
std::set<std::uint64_t> hostPages;
std::uintptr_t arenaBase = 0;
std::size_t arenaBytes = 0;
GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes);
const auto note = [&](const void* address, std::uint64_t bytes = 0) {
if (address == nullptr) return;
const auto at = pointer(address);
const bool arena = at >= arenaBase && at - arenaBase < arenaBytes;
const auto blockBytes = arena ? FrameCaptureWriter::BlockBytes : HostPageBytes;
const auto begin = at & ~std::uint64_t{blockBytes - 1};
const auto count = bytes != 0 ? (at - begin + bytes + blockBytes - 1) / blockBytes : 2;
auto& pages = arena ? blocks : hostPages;
for (std::uint64_t index = 0; index < count; ++index) pages.insert(begin + index * blockBytes);
};
for (const auto* shader : shaders) {
if (shader == nullptr || !GuestMemory::Accessible(shader, sizeof(Shader))) continue;
note(shader);
note(const_cast<const void*>(shader->code), shader->shader_size);
note(shader->user_data);
note(shader->cx_registers);
note(shader->sh_registers);
note(shader->specials);
note(shader->input_semantics);
note(shader->output_semantics);
if (shader->user_data == nullptr || !GuestMemory::Accessible(shader->user_data, sizeof(ShaderUserData))) continue;
note(shader->user_data->direct_resource_offset);
for (const auto* sharps : shader->user_data->sharp_resource_offset) note(sharps);
}
if (blocks.empty() && hostPages.empty()) return;
const auto mark = GuestMemory::WriteGeneration();
std::vector<Span> spans;
for (const auto block : blocks) {
const auto found = shaderMarks.find(block);
spans.push_back({block, FrameCaptureWriter::BlockBytes, found != shaderMarks.end() ? std::max(found->second, generation) : generation});
}
const std::vector<std::uint64_t> pages(hostPages.begin(), hostPages.end());
writeDelta(spans, pages);
for (const auto block : blocks) shaderMarks[block] = mark;
}
void FrameRecorder::writeDelta(std::span<const Span> spans, std::span<const std::uint64_t> hostPages, std::span<const Range> added) {
if (abandonIfDiskLow()) return;
const auto [accepted, completed] = DriverDetail::Driver::Get().SerialProgress();
const auto group = deltaGroups + 1;
bool any = false;
std::vector<std::pair<std::uint64_t, std::uint64_t>> written;
std::vector<std::byte> bytes;
for (const auto& range : added) {
writer->WriteRange(range.address, range.bytes, range.kind, range.protection, range.access);
ranges.push_back(range);
++lateRanges;
if (!range.readable) {
unreadableBytes += range.bytes;
continue;
}
for (std::uint64_t at = 0; at < range.bytes;) {
bytes.resize(static_cast<std::size_t>(std::min<std::uint64_t>(FrameCaptureWriter::BlockBytes, range.bytes - at)));
GuestMemory::ReadCommittedUnsynced(range.address + at, bytes);
const bool zero = std::all_of(bytes.begin(), bytes.end(), [](std::byte value) { return value == std::byte{0}; });
if (!zero) {
writer->WriteDelta(group, completed, range.address + at, bytes);
deltaBytes += bytes.size();
any = true;
}
at += bytes.size();
}
}
for (const auto page : hostPages) {
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(static_cast<std::uintptr_t>(page)), HostPageBytes)) continue;
bytes.resize(HostPageBytes);
std::memcpy(bytes.data(), reinterpret_cast<const void*>(static_cast<std::uintptr_t>(page)), HostPageBytes);
const auto hash = contentHash(bytes);
auto& known = hostPageHashes[page];
if (known == hash) continue;
known = hash;
writer->WriteDelta(group, completed, page, bytes);
deltaBytes += bytes.size();
any = true;
}
for (const auto& span : spans) {
GuestMemory::CollectWrittenBlocks(span.address, static_cast<std::size_t>(span.bytes), span.since, written);
for (const auto& [begin, end] : written) {
bytes.resize(static_cast<std::size_t>(end - begin));
GuestMemory::ReadCommittedUnsynced(begin, bytes);
writer->WriteDelta(group, completed, begin, bytes);
deltaBytes += bytes.size();
any = true;
}
}
if (!any) return;
deltaGroups = group;
auto call = make(FrameCallKind::Memory);
call.ordinal = ++ordinal;
call.accepted = accepted;
call.completed = completed;
call.values = {group};
writer->WriteCall(call);
}
void FrameRecorder::snapshot(const Range& range) {
writer->WriteRange(range.address, range.bytes, range.kind, range.protection, range.access);
if (!range.readable) {
unreadableBytes += range.bytes;
return;
}
std::vector<std::byte> bytes;
for (std::uint64_t at = 0; at < range.bytes; at += SnapshotPieceBytes) {
if (abandonIfDiskLow()) return;
bytes.resize(static_cast<std::size_t>(std::min<std::uint64_t>(SnapshotPieceBytes, range.bytes - at)));
GuestMemory::ReadCommitted(range.address + at, bytes);
snapshotBytes += writer->WriteSnapshot(range.address + at, bytes);
}
}
bool FrameRecorder::abandonIfDiskLow() {
std::error_code error;
const auto space = std::filesystem::space(directory, error);
if (error || space.available >= MinimumFreeBytes) return false;
warnings.push_back("stopped: less than 4 GiB free on the capture volume");
std::fprintf(stderr, "[frame-capture] stopped: less than 4 GiB free on the capture volume\n");
try {
writer->Finish(header());
} catch (const std::exception& failure) {
std::fprintf(stderr, "[frame-capture] %s\n", failure.what());
}
writer.reset();
state = State::Done;
return true;
}
void FrameRecorder::start() {
++startAttempts;
auto& driver = DriverDetail::Driver::Get();
if (!driver.WaitIdleFor(std::chrono::seconds(10))) {
std::fprintf(stderr, "[frame-capture] the driver did not go idle within 10 s at frame boundary %llu (attempt %llu)\n", static_cast<unsigned long long>(boundaries), static_cast<unsigned long long>(startAttempts));
if (startAttempts >= 5) {
std::fprintf(stderr, "[frame-capture] giving up: no idle frame boundary\n");
state = State::Done;
}
return;
}
startedAt = std::chrono::steady_clock::now();
startBoundary = boundaries;
allocationsGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
ranges = committedRanges();
writer = std::make_unique<FrameCaptureWriter>(directory);
std::vector<std::pair<std::uint64_t, std::uint64_t>> written;
for (const auto& range : ranges) {
if (range.kind == FrameRangeKind::Watched || range.kind == FrameRangeKind::Unwatched) GuestMemory::CollectWrittenBlocks(range.address, static_cast<std::size_t>(range.bytes), std::numeric_limits<std::uint64_t>::max(), written);
}
generation = GuestMemory::WriteGeneration();
for (const auto& range : ranges) {
snapshot(range);
if (!writer) return;
}
for (const auto& call : prologue) writer->WriteCall(call);
std::uintptr_t ringBase = 0;
std::uint32_t ringSize = 0;
sceAgcDriverGetTFRing(&ringBase, &ringSize);
auto ring = make(FrameCallKind::SetTessellationFactorRing);
ring.ordinal = ++ordinal;
ring.address = ringBase;
ring.size = ringSize;
writer->WriteCall(ring);
const auto pending = driver.PendingEopFlipTokens();
const std::set<std::uint64_t> open(pending.begin(), pending.end());
for (const auto& call : reservations) {
if (open.contains(call.values.back())) writer->WriteCall(call);
}
auto begin = make(FrameCallKind::FrameStart);
begin.ordinal = ++ordinal;
std::tie(begin.accepted, begin.completed) = driver.SerialProgress();
begin.values = {boundaries};
writer->WriteCall(begin);
state = State::Recording;
recordedFrames = 0;
std::fprintf(stderr, "[frame-capture] started at frame boundary %llu: %zu ranges, %.1f MiB snapshot in %.1f s\n", static_cast<unsigned long long>(boundaries), ranges.size(),
static_cast<double>(snapshotBytes) / (1024.0 * 1024.0), std::chrono::duration<double>(std::chrono::steady_clock::now() - startedAt).count());
}
void FrameRecorder::finish() {
emitDelta();
if (!writer) return;
auto end = make(FrameCallKind::End);
end.ordinal = ++ordinal;
std::tie(end.accepted, end.completed) = DriverDetail::Driver::Get().SerialProgress();
writer->WriteCall(end);
writeReferenceFrames();
writer->Finish(header());
std::fprintf(stderr, "[frame-capture] %llu frames, %llu calls, %llu memory deltas (%.1f MiB), %.1f MiB raw, %.1f MiB stored in %s\n", static_cast<unsigned long long>(recordedFrames),
static_cast<unsigned long long>(recordedCalls), static_cast<unsigned long long>(deltaGroups), static_cast<double>(deltaBytes) / (1024.0 * 1024.0),
static_cast<double>(writer->RawBytes()) / (1024.0 * 1024.0), static_cast<double>(writer->StoredBytes()) / (1024.0 * 1024.0), directory.string().c_str());
writer.reset();
state = State::Done;
}
std::string FrameRecorder::header() const {
std::uintptr_t arenaBase = 0;
std::size_t arenaBytes = 0;
GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes);
std::string text = "{\"magic\":\"aps5-frame-capture 1\"";
text += ",\"commit\":" + jsonString(APS5_TRACE_COMMIT);
text += std::string(",\"dirty\":") + (APS5_TRACE_DIRTY < 0 ? "null" : APS5_TRACE_DIRTY != 0 ? "true" : "false");
text += ",\"arena_base\":" + std::to_string(arenaBase) + ",\"arena_bytes\":" + std::to_string(arenaBytes);
text += ",\"start_boundary\":" + std::to_string(startBoundary) + ",\"frames\":" + std::to_string(recordedFrames);
text += ",\"calls\":" + std::to_string(recordedCalls) + ",\"prologue_calls\":" + std::to_string(prologue.size());
text += ",\"ranges\":" + std::to_string(ranges.size()) + ",\"late_ranges\":" + std::to_string(lateRanges) + ",\"snapshot_bytes\":" + std::to_string(snapshotBytes) + ",\"unreadable_bytes\":" + std::to_string(unreadableBytes);
text += ",\"memory_groups\":" + std::to_string(deltaGroups) + ",\"delta_bytes\":" + std::to_string(deltaBytes);
text += ",\"capture_seconds\":" + std::to_string(std::chrono::duration<double>(std::chrono::steady_clock::now() - startedAt).count());
text += ",\"env\":{";
bool first = true;
for (const auto& [name, value] : captureEnvironment()) {
text += (first ? "" : ",") + jsonString(name) + ":" + jsonString(value);
first = false;
}
text += "},\"warnings\":[";
first = true;
for (const auto& warning : warnings) {
text += (first ? "" : ",") + jsonString(warning);
first = false;
}
return text + "]}\n";
}
std::vector<FrameRecorder::Range> FrameRecorder::committedRanges() {
#ifdef _WIN32
std::uintptr_t base = 0;
std::size_t size = 0;
GuestArena::GuestArenaRange_nid_postfix(&base, &size);
if (size == 0) throw std::runtime_error("frame capture: the guest arena is not reserved");
std::vector<Range> result;
for (const auto& allocation : GuestAllocations::GuestAllocationsAcquireAll_nid_postfix()) {
if (!allocation->readable) continue;
const auto first = allocation->address;
const auto end = first + allocation->bytes;
const bool host = first < base || end > base + size;
const auto access = FrameRangeAccess::Readable | (allocation->writable ? FrameRangeAccess::Writable : 0u) | (allocation->gpu ? FrameRangeAccess::Gpu : 0u);
for (auto address = first; address < end;) {
MEMORY_BASIC_INFORMATION info{};
if (VirtualQuery(reinterpret_cast<const void*>(static_cast<std::uintptr_t>(address)), &info, sizeof(info)) == 0) throw std::runtime_error("frame capture: VirtualQuery failed inside the guest arena");
const auto regionEnd = std::min<std::uint64_t>(end, reinterpret_cast<std::uintptr_t>(info.BaseAddress) + info.RegionSize);
if (info.State == MEM_COMMIT) {
Range range;
range.address = address;
range.bytes = regionEnd - address;
range.protection = info.Protect;
range.access = access;
const auto page = info.Protect & 0xffu;
range.readable = (info.Protect & PAGE_GUARD) == 0 && (page == PAGE_READONLY || page == PAGE_READWRITE || page == PAGE_WRITECOPY || page == PAGE_EXECUTE_READ || page == PAGE_EXECUTE_READWRITE || page == PAGE_EXECUTE_WRITECOPY);
if (host) range.kind = FrameRangeKind::Host;
else if (info.Type == MEM_PRIVATE) range.kind = GuestMemory::Watched(range.address, static_cast<std::size_t>(range.bytes)) ? FrameRangeKind::Watched : FrameRangeKind::Unwatched;
else range.kind = FrameRangeKind::Section;
auto& last = result.empty() ? range : result.back();
if (!result.empty() && last.address + last.bytes == range.address && last.kind == range.kind && last.protection == range.protection && last.readable == range.readable && last.access == range.access) last.bytes += range.bytes;
else result.push_back(range);
}
address = regionEnd;
}
}
return result;
#else
throw std::runtime_error("frame capture: guest memory capture is implemented on Windows only");
#endif
}
}
@@ -21,6 +21,7 @@
#include <cstdlib>
#include <cstring>
#include <limits>
#include <map>
#include <memory>
#include <new>
#include <stdexcept>
@@ -1041,6 +1042,7 @@ struct WriteTracker {
};
std::unordered_map<std::uint64_t, CpuPages> cpuPages;
std::unordered_map<std::uint64_t, CpuPages> walkPages;
std::map<std::uint64_t, std::uint32_t> refused;
void noteCpuStore(std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) {
notePages(cpuPages, begin, end, stampGeneration);
@@ -1245,7 +1247,16 @@ struct WriteTracker {
return false;
}
bool refusedSince(std::uint64_t address, std::uint64_t end, std::uint64_t since) const {
const auto last = blockOf(end - 1);
for (auto it = refused.lower_bound(blockOf(address)); it != refused.end() && it->first <= last; ++it) {
if (it->second > since) return true;
}
return false;
}
bool rangeUnchanged(std::uint64_t address, std::size_t bytes, std::uint64_t since) const {
if (refusedSince(address, address + bytes, since)) return false;
return unchanged(blockOf(address), blockOf(address + bytes - 1), since) || !storedOver(address, address + bytes, since, true);
}
@@ -1450,13 +1461,32 @@ bool collectWindowsWrites(StampRuns& runs, std::uint64_t first, std::uint64_t st
if (runs.stamped != 0) collectDirty.fetch_add(1, std::memory_order_relaxed);
return fresh || singlePass || runs.stamped == 0 || walk(true);
}
void noteRefusedWalk(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop) {
const auto pagesOf = [&](auto list, auto&& each) {
auto cursor = first;
while (cursor < stop) {
std::size_t count = tracker.pages.size();
list(static_cast<std::uintptr_t>(cursor), static_cast<std::size_t>(stop - cursor), tracker.pages.data(), &count);
for (std::size_t i = 0; i < count; ++i) each(reinterpret_cast<std::uintptr_t>(tracker.pages[i]));
if (count < tracker.pages.size()) break;
cursor = reinterpret_cast<std::uintptr_t>(tracker.pages[count - 1]) + WritePageBytes;
}
};
StampRuns runs{tracker, StampKind::Cpu};
pagesOf(&GuestArena::GuestArenaTakeUnreportedPages_nid_no_patch, [&](std::uintptr_t page) { stampWrittenRun(&runs, page, page + WritePageBytes); });
const auto generation = tracker.generation.load(std::memory_order_relaxed);
pagesOf(&GuestArena::GuestArenaComparedPages_nid_no_patch, [&](std::uintptr_t page) { tracker.refused[tracker.blockOf(page)] = generation; });
}
#endif
bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, StampKind kind, bool armedOnly = false, const WindowCopy* window = nullptr) {
++tracker.generation;
StampRuns runs{tracker, kind, 0, window};
#ifdef _WIN32
return collectWindowsWrites(runs, first, stop, armedOnly);
if (collectWindowsWrites(runs, first, stop, armedOnly)) return true;
noteRefusedWalk(tracker, first, stop);
return false;
#else
const auto dirtyRuns = collectDirtyRuns.load(std::memory_order_relaxed);
const auto collect = armedOnly ? &GuestWriteWatch::GuestWriteWatchCollectArmed_nid_postfix : &GuestWriteWatch::GuestWriteWatchCollect_nid_postfix;
@@ -1549,6 +1579,71 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes) {
return collectWrites(address, bytes, false);
}
std::uint64_t WriteGeneration() {
return Tracker().generation.load(std::memory_order_acquire);
}
std::uint64_t CollectWrittenBlocks(std::uint64_t address, std::size_t bytes, std::uint64_t since, std::vector<std::pair<std::uint64_t, std::uint64_t>>& written) {
written.clear();
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return 0;
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
tracker.initialize();
if (!tracker.watched) return 0;
#ifdef _WIN32
if (address < tracker.base || bytes > tracker.size || address - tracker.base > tracker.size - bytes) return 0;
#endif
const auto end = address + bytes;
const auto first = tracker.blockOf(address);
const auto last = tracker.blockOf(end - 1);
const auto clip = [&](std::uint64_t block) {
const auto begin = tracker.blockBegin(block);
return std::pair{std::max(address, begin), std::min<std::uint64_t>(end, begin + WriteBlockBytes)};
};
std::uint64_t spanBegin = 0;
std::uint64_t spanEnd = 0;
const auto walkSpan = [&] {
if (spanEnd > spanBegin) walkWrites(tracker, spanBegin & ~std::uint64_t{WritePageBytes - 1}, (spanEnd + WritePageBytes - 1) & ~std::uint64_t{WritePageBytes - 1}, StampKind::Cpu);
spanBegin = spanEnd = 0;
};
for (auto block = first; block <= last; ++block) {
const auto [begin, stop] = clip(block);
if (!tracker.covers(begin, stop - begin)) {
walkSpan();
continue;
}
if (spanEnd != begin) {
walkSpan();
spanBegin = begin;
}
spanEnd = stop;
}
walkSpan();
const auto append = [&](std::uint64_t begin, std::uint64_t stop) {
if (!written.empty() && written.back().second == begin) written.back().second = stop;
else written.emplace_back(begin, stop);
};
for (auto block = first; block <= last; ++block) {
if (tracker.cpuStampOf(block) <= since) continue;
const auto [begin, stop] = clip(block);
const auto pages = tracker.cpuPages.find(block);
bool any = false;
if (pages != tracker.cpuPages.end()) {
const auto blockStart = tracker.blockBegin(block);
for (std::size_t page = 0; page < WritePagesPerBlock; ++page) {
if (pages->second.pages[page] <= since) continue;
const auto pageBegin = std::max<std::uint64_t>(begin, blockStart + page * WritePageBytes);
const auto pageEnd = std::min<std::uint64_t>(stop, blockStart + (page + 1) * WritePageBytes);
if (pageBegin >= pageEnd) continue;
append(pageBegin, pageEnd);
any = true;
}
}
if (!any) append(begin, stop);
}
return tracker.generation.load(std::memory_order_relaxed);
}
void BumpCollectEpoch() {
const auto epoch = nextCollectEpoch.fetch_add(1, std::memory_order_relaxed) + 1;
HostThreadSlot<std::uint64_t, CollectEpochSlot>::Set(epoch);
@@ -1658,7 +1753,9 @@ void Rewatch(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges) {
for (const auto& [begin, end] : ranges) {
if (end <= begin || begin < tracker.base || end - tracker.base > tracker.size || (begin - tracker.base) % WriteBlockBytes != 0 || (end - tracker.base) % WriteBlockBytes != 0) continue;
const auto bytes = static_cast<std::size_t>(end - begin);
if (tracker.coverage.Covers(begin, bytes) || !walkWrites(tracker, begin, end, StampKind::Cpu) || !tracker.coverage.Restore(begin, bytes)) continue;
if (tracker.coverage.Covers(begin, bytes)) continue;
GuestArena::GuestArenaCompareWhenShared_nid_no_patch(static_cast<std::uintptr_t>(begin), bytes);
if (!walkWrites(tracker, begin, end, StampKind::Cpu) || !tracker.coverage.Restore(begin, bytes)) continue;
++tracker.generation;
tracker.noteCpuStore(begin, end, tracker.generation);
for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) {
@@ -1729,6 +1826,9 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
auto unwatchedFrom = first;
for (const auto& [begin, end] : spans) {
if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue;
#ifdef _WIN32
GuestArena::GuestArenaCompareResidentWhenShared_nid_no_patch(static_cast<std::uintptr_t>(begin), static_cast<std::size_t>(end - begin));
#endif
if (unwatchedFrom < begin) unwatchLocked(tracker, unwatchedFrom, static_cast<std::size_t>(begin - unwatchedFrom));
unwatchedFrom = end;
}
@@ -1791,7 +1891,7 @@ bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint6
tracker.initialize();
if (!tracker.watched || generation == 0) return false;
for (const auto& [begin, end] : ranges) {
if (end <= begin || !tracker.covers(begin, static_cast<std::size_t>(end - begin)) || tracker.newestStamp(begin, end) > generation) return false;
if (end <= begin || !tracker.covers(begin, static_cast<std::size_t>(end - begin)) || tracker.newestStamp(begin, end) > generation || tracker.refusedSince(begin, end, generation)) return false;
}
return true;
}
@@ -24,6 +24,41 @@ struct Cache {
std::uint16_t counts[Classes];
};
constexpr std::uint16_t KeptLists = 8;
struct Depot {
SRWLOCK lock = SRWLOCK_INIT;
void* lists = nullptr;
std::uint16_t count = 0;
};
Depot depots[Classes];
bool KeepList(std::size_t sizeClass, void* list) noexcept {
auto& depot = depots[sizeClass];
AcquireSRWLockExclusive(&depot.lock);
const bool kept = depot.count < KeptLists;
if (kept) {
static_cast<void**>(list)[1] = depot.lists;
depot.lists = list;
++depot.count;
}
ReleaseSRWLockExclusive(&depot.lock);
return kept;
}
void* TakeList(std::size_t sizeClass) noexcept {
auto& depot = depots[sizeClass];
AcquireSRWLockExclusive(&depot.lock);
void* list = depot.lists;
if (list != nullptr) {
depot.lists = static_cast<void**>(list)[1];
--depot.count;
}
ReleaseSRWLockExclusive(&depot.lock);
return list;
}
Cache* const Closed = reinterpret_cast<Cache*>(std::uintptr_t{1});
constexpr unsigned long TlsSlotsOffset = 0x1480;
@@ -114,6 +149,12 @@ void* TryAllocate(std::size_t bytes) noexcept {
if (bytes <= LargestCached) {
const auto sizeClass = (bytes - 1) / Granule;
if (auto* cache = CurrentCache()) {
if (cache->heads[sizeClass] == nullptr) {
if (void* list = TakeList(sizeClass)) {
cache->heads[sizeClass] = list;
cache->counts[sizeClass] = Depth(sizeClass);
}
}
if (void* block = cache->heads[sizeClass]) {
cache->heads[sizeClass] = *static_cast<void**>(block);
--cache->counts[sizeClass];
@@ -140,6 +181,10 @@ void Release(void* block, std::size_t bytes = 0) noexcept {
const auto usable = bytes != 0 && bytes % Granule == 0 ? bytes : _msize(block);
if (usable >= Granule && usable < LargestCached + Granule) {
const auto sizeClass = usable / Granule - 1;
if (cache->counts[sizeClass] == Depth(sizeClass) && KeepList(sizeClass, cache->heads[sizeClass])) {
cache->heads[sizeClass] = nullptr;
cache->counts[sizeClass] = 0;
}
if (cache->counts[sizeClass] < Depth(sizeClass)) {
*static_cast<void**>(block) = cache->heads[sizeClass];
cache->heads[sizeClass] = block;
@@ -3,6 +3,7 @@
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/PreciseWait.hpp"
#include <algorithm>
#include <atomic>
#include <thread>
#include <chrono>
#include <cstdio>
@@ -226,8 +227,7 @@ std::string_view UnsupportedReason(std::uint32_t header) {
return "graphics draw, shader stages and guest render-target materialization 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:
return "cooperative command-queue waits are not implemented";
case 0x39: return {};
case 0x59: return {};
case 0x84: case 0x85: case 0x86: case 0x88:
return "separate CE/DE execution and counter synchronization are not implemented";
@@ -458,6 +458,12 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
require((packet[1] & 7u) <= 6u, "invalid WAIT_REG_MEM compare function");
break;
}
case 0x39:
size(4);
require((packet[3] >> 29u) == 6u || (packet[3] >> 29u) == 7u, "invalid MEM_SEMAPHORE operation");
require(address(packet[1], packet[2]) != 0 && (packet[1] & 7u) == 0, "null or misaligned MEM_SEMAPHORE address");
require((packet[3] & 0x10000u) == 0, "MEM_SEMAPHORE mailbox waits are not implemented");
break;
case 0x45: {
size(9);
require((packet[1] & ~0x317u) == 0, "COND_WRITE reserved fields are not implemented");
@@ -716,6 +722,7 @@ std::vector<PacketStore> DecodeStores(std::span<const std::uint32_t> packet) {
if (!label.has_value()) return {};
return {known(label->address, label->Bytes())};
}
case 0x39: return {runTime(address(packet[1], packet[2]), 8)};
case 0x40: {
require(packet.size() == 6, "invalid COPY_DATA size");
if (CopyDataToRegister(packet)) return {};
@@ -771,7 +778,7 @@ bool UsesGpuCacheBarrier(std::span<const std::uint32_t> packet) {
bool AccessesMemory(std::uint32_t header) {
switch ((header >> 8u) & 0xffu) {
case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x91: case 0x9f: return true;
case 0x22: case 0x27: case 0x39: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x91: case 0x9f: return true;
default: return false;
}
}
@@ -856,12 +863,17 @@ DrawParameters resolveIndirectDraw(std::span<const std::uint32_t> packet, const
indirect.indxOffset = offset->second;
} else {
const auto indexSize = indexSizeOf(queue);
require(queue.indexBufferSize != 0, "INDEX_BUFFER_SIZE has not been set");
const auto bytes = static_cast<std::uint64_t>(queue.indexBufferSize) * indexSize;
require(bytes <= std::numeric_limits<std::size_t>::max() && bytes <= std::numeric_limits<std::uint64_t>::max() - queue.indexBase, "index range size overflow");
draw.indexAddress = queue.indexBase;
draw.indexCount = queue.indexBufferSize;
draw.indexSize = indexSize;
if (queue.indexBufferSize) {
require(*queue.indexBufferSize != 0, "INDEX_BUFFER_SIZE is zero");
const auto bytes = static_cast<std::uint64_t>(*queue.indexBufferSize) * indexSize;
require(bytes <= std::numeric_limits<std::size_t>::max() && bytes <= std::numeric_limits<std::uint64_t>::max() - queue.indexBase, "index range size overflow");
draw.indexCount = *queue.indexBufferSize;
} else {
draw.indexCount = 0;
indirect.indexLimitKnown = false;
}
}
draw.indirect = indirect;
return draw;
@@ -1072,6 +1084,47 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
}
return;
}
case 0x39: {
const auto semaphore = address(packet[1], packet[2]);
auto* word = reinterpret_cast<std::uint64_t*>(semaphore);
std::atomic_ref<std::uint64_t> shared(*word);
const auto prepare = [&] {
GuestMemory::GuardRead(semaphore, sizeof(*word));
GuestMemory::FlushGpuWrites(semaphore, sizeof(*word));
GuestMemory::CheckRange(word, sizeof(*word), std::atomic_ref<std::uint64_t>::required_alignment, true);
};
if ((packet[3] >> 29u) == 6u) {
prepare();
const bool set = (packet[3] & 0x100000u) != 0;
GuestMemory::StoreOwnBytes(semaphore, sizeof(*word), [&] {
if (set) shared.store(1, std::memory_order_release);
else shared.fetch_add(1, std::memory_order_acq_rel);
});
return;
}
const auto start = std::chrono::steady_clock::now();
bool warned = false;
for (;;) {
{
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait);
prepare();
}
auto value = shared.load(std::memory_order_acquire);
if (value != 0) {
bool taken = false;
GuestMemory::StoreOwnBytes(semaphore, sizeof(*word), [&] {
taken = shared.compare_exchange_strong(value, value - 1, std::memory_order_acq_rel);
});
if (taken) return;
continue;
}
PreciseSleepUs(50);
if (!warned && std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) {
warned = true;
std::fprintf(stderr, "[gpu] MEM_SEMAPHORE wait at 0x%llx still waiting after 5s\n", static_cast<unsigned long long>(semaphore));
}
}
}
case 0x49: {
// Earlier work has drained by the time the driver reaches this packet, so the end-of-pipe
// write can happen immediately.
@@ -392,7 +392,7 @@ bool ShaderMemory::readWords(void* context, std::uint64_t address, std::uint32_t
}
auto last = first + BlockWords;
while (last < end && !page->valid.TestBlock(last)) last += BlockWords;
std::memcpy(&page->words[first], reinterpret_cast<const void*>(base + first * sizeof(std::uint32_t)), (last - first) * sizeof(std::uint32_t));
GuestMemory::CopyGuestBytes(&page->words[first], reinterpret_cast<const void*>(base + first * sizeof(std::uint32_t)), (last - first) * sizeof(std::uint32_t));
page->valid.SetRun(first, last - first);
first = last;
}
@@ -495,8 +495,8 @@ bool ShaderMemory::place(std::uint64_t address, const std::uint32_t* values, std
if (!page.valid.Test(index + word)) page.words[index + word] = values[word];
}
}
if (headOpen && head < index) std::memcpy(&page.words[head], reinterpret_cast<const void*>(base + head * sizeof(std::uint32_t)), (index - head) * sizeof(std::uint32_t));
if (tailOpen && index + words < tail) std::memcpy(&page.words[index + words], reinterpret_cast<const void*>(base + (index + words) * sizeof(std::uint32_t)), (tail - index - words) * sizeof(std::uint32_t));
if (headOpen && head < index) GuestMemory::CopyGuestBytes(&page.words[head], reinterpret_cast<const void*>(base + head * sizeof(std::uint32_t)), (index - head) * sizeof(std::uint32_t));
if (tailOpen && index + words < tail) GuestMemory::CopyGuestBytes(&page.words[index + words], reinterpret_cast<const void*>(base + (index + words) * sizeof(std::uint32_t)), (tail - index - words) * sizeof(std::uint32_t));
page.valid.SetRun(page.lazy ? head : index, page.lazy ? tail - head : words);
page.read.SetRun(index, words);
if (recent) page.recent.SetRun(index, words);
@@ -422,6 +422,7 @@ struct VulkanDevice::State {
bool depthBounds = false;
bool depthBiasClamp = false;
bool dualSrcBlend = false;
bool logicOp = false;
bool occlusionQueryPrecise = false;
VkDeviceSize hostImportAlignment = 0;
bool drawInputsInPlace = true;
@@ -1775,6 +1776,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
enabled.depthBiasClamp = available.depthBiasClamp;
state->depthBiasClamp = enabled.depthBiasClamp == VK_TRUE;
enabled.dualSrcBlend = available.dualSrcBlend;
enabled.logicOp = available.logicOp;
state->logicOp = enabled.logicOp == VK_TRUE;
state->dualSrcBlend = enabled.dualSrcBlend == VK_TRUE;
enabled.occlusionQueryPrecise = available.occlusionQueryPrecise;
state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE;
@@ -2033,6 +2036,29 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
deviceInfo.enabledExtensionCount = static_cast<std::uint32_t>(deviceExtensions.size());
deviceInfo.ppEnabledExtensionNames = deviceExtensions.data();
}
const bool diagnosticsRequested = std::getenv("APS5_NV_DIAGNOSTICS") != nullptr;
VkPhysicalDeviceDiagnosticsConfigFeaturesNV diagnosticsFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DIAGNOSTICS_CONFIG_FEATURES_NV};
if (diagnosticsRequested && hasExtension(VK_NV_DEVICE_DIAGNOSTICS_CONFIG_EXTENSION_NAME)) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &diagnosticsFeatures};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
}
const auto diagnostics = ChooseDeviceDiagnostics(diagnosticsRequested, diagnosticsFeatures.diagnosticsConfig == VK_TRUE, hasExtension(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME));
VkDeviceDiagnosticsConfigCreateInfoNV diagnosticsConfig{VK_STRUCTURE_TYPE_DEVICE_DIAGNOSTICS_CONFIG_CREATE_INFO_NV};
if (diagnostics.config) {
diagnosticsFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DIAGNOSTICS_CONFIG_FEATURES_NV};
diagnosticsFeatures.diagnosticsConfig = VK_TRUE;
diagnosticsConfig.flags = diagnostics.flags;
deviceExtensions.push_back(VK_NV_DEVICE_DIAGNOSTICS_CONFIG_EXTENSION_NAME);
if (diagnostics.checkpoints && !checkpoints) deviceExtensions.push_back(VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME);
deviceInfo.enabledExtensionCount = static_cast<std::uint32_t>(deviceExtensions.size());
deviceInfo.ppEnabledExtensionNames = deviceExtensions.data();
diagnosticsConfig.pNext = byteFeatures.pNext;
diagnosticsFeatures.pNext = &diagnosticsConfig;
byteFeatures.pNext = &diagnosticsFeatures;
std::fprintf(stderr, "[gpu] APS5_NV_DIAGNOSTICS: VK_NV_device_diagnostics_config enabled (shader debug info, resource tracking, automatic checkpoints, shader error reporting)\n");
} else if (diagnosticsRequested) {
std::fprintf(stderr, "[gpu] APS5_NV_DIAGNOSTICS: the device does not support VK_NV_device_diagnostics_config\n");
}
bdaFeatures.pNext = &byteFeatures;
VkPhysicalDeviceRobustness2FeaturesEXT robustness2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
if (hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) {
@@ -2354,6 +2380,14 @@ void VulkanDevice::WaitRecorded(std::uint64_t serial) {
if (state->recorder) state->recorder->WaitSerial(serial);
}
std::uint64_t VulkanDevice::SubmittedEpoch() const {
return state->recorder ? state->recorder->NewestSubmitted() : 0;
}
bool VulkanDevice::WaitRecordedFor(std::uint64_t serial, std::uint64_t timeoutNanos) {
return state->recorder && state->recorder->WaitSerialFor(serial, timeoutNanos);
}
void VulkanDevice::ReapRecorded(std::uint64_t serial) {
if (!state->recorder) return;
// The drain's site in the [recorder] table is this call's caller (Driver::execute's drain,
@@ -3940,6 +3974,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.drawIndirectFirstInstance = state->drawIndirectFirstInstance;
context.multiDrawIndirect = state->multiDrawIndirect;
context.dualSrcBlend = state->dualSrcBlend;
context.logicOp = state->logicOp;
context.depthBounds = state->depthBounds;
context.depthBiasClamp = state->depthBiasClamp;
context.samplerFilterMinmax = state->samplerFilterMinmax;
@@ -4011,10 +4046,6 @@ std::optional<std::string> VulkanDevice::KnownDrawRejection(const Graphics::Stat
return Graphics::KnownValidationFailure(graphicsContext(), shaders, graphics);
}
void VulkanDevice::DepthCopyPass(const Graphics::DepthCopyPass& pass) {
Graphics::RunDepthCopyPass(graphicsContext(), pass);
}
void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) {
Graphics::RunColorMetadataPass(graphicsContext(), pass);
}
@@ -4057,7 +4088,7 @@ bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stage
return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), group);
}
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe) {
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe, const DrawTraceContext* trace) {
startJournalReplay();
for (const auto& shader : shaders) {
require(shader.program != nullptr, "missing compiled shader");
@@ -4065,11 +4096,11 @@ void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParamete
}
// Two stdout lines per draw cost ~1.3 ms per frame of the queue-0 worker (part of it under the
// GPU mutex); APS5_TRACE_DRAWS=1 restores them.
static const bool trace = std::getenv("APS5_TRACE_DRAWS") != nullptr;
if (trace) APS5_LOG_OUT("VulkanDevice::Draw indices=%u instances=%u indexSize=%u address=0x%llx shaders=%zu colorTarget=%u", draw.indexCount, draw.instanceCount, draw.indexSize, static_cast<unsigned long long>(draw.indexAddress), shaders.size(), static_cast<unsigned>(graphics.hasColorTarget));
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
if (traceDraws) APS5_LOG_OUT("VulkanDevice::Draw indices=%u instances=%u indexSize=%u address=0x%llx shaders=%zu colorTarget=%u", draw.indexCount, draw.instanceCount, draw.indexSize, static_cast<unsigned long long>(draw.indexAddress), shaders.size(), static_cast<unsigned>(graphics.hasColorTarget));
const auto context = graphicsContext();
Graphics::Draw(context, graphics, draw, shaders, snapshots, recipe);
if (trace) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete");
Graphics::Draw(context, graphics, draw, shaders, snapshots, recipe, trace);
if (traceDraws) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete");
}
bool VulkanDevice::DrawInputsInPlace() const {
@@ -4618,7 +4649,7 @@ void VulkanDevice::NoteDrawRecipeMiss(DrawRecipePrecheck miss) {
counters.precheckMisses[miss == DrawRecipePrecheck::NoRecipe ? MissNoRecipe : MissDevice].fetch_add(1, std::memory_order_relaxed);
}
RecipeOutcome VulkanDevice::DrawFromRecipe(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe) {
RecipeOutcome VulkanDevice::DrawFromRecipe(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const DrawTraceContext* trace) {
PerformanceTimer timing("Vulkan.DrawFromRecipe");
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
auto& counters = Recipes().kinds[static_cast<std::size_t>(RecipeKind::Draw)];
@@ -4629,7 +4660,7 @@ RecipeOutcome VulkanDevice::DrawFromRecipe(const Graphics::State& graphics, cons
return RecipeOutcome::Rebuild;
}
const auto context = graphicsContext();
const auto outcome = Graphics::DrawWithRecipe(context, graphics, draw, shaders, snapshots, *recipe);
const auto outcome = Graphics::DrawWithRecipe(context, graphics, draw, shaders, snapshots, *recipe, trace);
if (!outcome.recorded) {
switch (outcome.miss) {
case Graphics::DrawRecipeMiss::NotRecordable: counters.precheckMisses[MissNotRecordable].fetch_add(1, std::memory_order_relaxed); break;
@@ -213,6 +213,7 @@ struct Context {
bool depthBounds = false;
bool depthBiasClamp = false;
bool dualSrcBlend = false;
bool logicOp = false;
bool samplerFilterMinmax = false;
bool nonSeamlessCubeMap = false;
VkSampleCountFlags mergedCoverageSampleCounts = 0;
@@ -72,11 +72,22 @@ VkImageAspectFlags HtileFillClears(std::uint32_t pattern, bool stencilInHtile);
std::uint64_t HtileMetadataBytes(VkExtent2D extent);
bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes);
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D);
std::size_t DepthPlaneCopyCount();
VkFormat HostDepthFormat(const Context& context, VkFormat format);
float HostDepthBiasConstant(const Context& context, VkFormat format, float constant);
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip);
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage);
void CopyDepthSample(const Context& context, std::uint64_t depthAddress, std::uint32_t samples, std::uint32_t sample, StorageTexture& destination, VkImageView destinationView, VkExtent2D extent);
std::array<std::uint32_t, 8> DepthCopySourceWords(const DepthTarget& target);
class DepthCopySourceScope {
public:
explicit DepthCopySourceScope(bool active);
~DepthCopySourceScope();
DepthCopySourceScope(const DepthCopySourceScope&) = delete;
DepthCopySourceScope& operator=(const DepthCopySourceScope&) = delete;
private:
bool previous;
};
struct StorageImageClear {
std::array<std::uint32_t, 8> image{};
@@ -14,10 +14,18 @@ namespace AgcDriver::Graphics {
struct HostImport;
struct DrawTraceContext {
std::uint64_t token = 0;
std::uint64_t submission = 0;
std::uint32_t queue = 0;
std::size_t offset = 0;
std::uint64_t target = 0;
};
// `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, non-indirect draw
// built for its draw-cache entry (design_cpu_final M8; null otherwise, and always under
// APS5_NO_DRAW_RECIPE=1).
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr);
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr);
struct PixelFunctionLdsLayouts {
VkDescriptorSetLayout empty = VK_NULL_HANDLE;
@@ -92,7 +100,7 @@ struct DrawRecipeOutcome {
// the template's proof (ProveCurrent) with the alias checks repeated, the pipeline and framebuffer
// from the recipe, then the record as Draw's (pass continued or begun, Kept, MarkGpuWrites). A
// miss records nothing.
DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, const DrawRecipe& recipe);
DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, const DrawRecipe& recipe, const DrawTraceContext* trace = nullptr);
// Whether recorded draws build recipes and draw-cache hits use them (APS5_NO_DRAW_RECIPE unset).
bool DrawRecipes();
@@ -102,7 +110,7 @@ bool DrawRecipes();
// draw index, the vertex ranges, the device features, APS5_NO_GPU_INDIRECT_DRAW) or under the device
// lock in Draw (the memory state of the records). `readMs` is the CPU read's time (its sync);
// `rewritten` counts a GPU-side draw whose records were copied and patched with a constant.
enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, IndexRange, Count };
enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, IndexRange, IndexLimitUnknown, Count };
void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten = false);
std::array<std::uint64_t, static_cast<std::size_t>(IndirectDrawPath::Count)> IndirectDrawCounts();
void CountEmptyIndirectDraw(double readMs);
@@ -140,7 +148,6 @@ void CountDrawSkip(DrawSkip kind, double us);
std::shared_ptr<std::vector<std::shared_ptr<ShaderResources>>> DrawCopiedWriters();
void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass);
void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass);
std::vector<std::pair<std::uint64_t, std::uint64_t>> PipelinedColorMetadataPassWrites(const ColorMetadataPass& pass);
bool DrawPipelineEnabled();
bool CommitTrims();
@@ -81,6 +81,7 @@ std::shared_ptr<Texture> MultisampleTargetFmaskTexture_nid_no_patch(const Contex
void ClearMultisampleTargets(VkDevice device);
void InstallResidentImageReadGuard_nid_no_patch();
void UpdateResidentImageUnmapObserver_nid_no_patch();
void GuardOneFragmentFmask(const Context& context, const ColorTarget& target);
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access);
void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access);
@@ -61,11 +61,14 @@ struct PipelineJournalCounters {
std::uint64_t replayFailed = 0;
double replayMs = 0;
std::uint32_t replayThreads = 0;
std::uint64_t replayHeldPeakBytes = 0;
std::uint64_t recordedGraphics = 0;
std::uint64_t recordedCompute = 0;
std::uint64_t recordedModules = 0;
std::uint64_t writtenBytes = 0;
std::uint64_t writeFailures = 0;
std::uint64_t evictedRecords = 0;
std::uint64_t evictedBytes = 0;
};
// A decimal switch of the pipeline store or journal, or its default when unset; anything else throws.
@@ -107,7 +107,8 @@ public:
static constexpr std::size_t DrawInputEntries = 16384;
std::shared_ptr<Buffer> UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp, VkDeviceSize* offset = nullptr);
void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array<std::uint64_t, 3>& stamp, std::shared_ptr<Buffer> buffer, VkDeviceSize offset = 0);
std::shared_ptr<Buffer> ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0);
std::shared_ptr<Buffer> ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0, VkDeviceSize* offset = nullptr);
static constexpr std::size_t ContainingSnapshotProbes = 16;
void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr<Buffer> buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0);
static constexpr std::size_t DrawUploadChunk = std::size_t{1} << 20u;
struct UploadSlice {
@@ -202,12 +203,14 @@ public:
// Submits the open batch and returns the serial of the newest submitted batch (0 when nothing was
// ever submitted); WaitSerial(serial) then covers all recorded work.
std::uint64_t SubmitAndEpoch();
std::uint64_t OpenBatchSerial();
bool HasTimeline() const { return timeline != VK_NULL_HANDLE; }
bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array<std::uint32_t, 3>* threadGroup = nullptr);
// Waits until every batch up to `serial` completed on the GPU. Called WITHOUT GuestMemory::GpuMutex:
// it touches only the immutable timeline semaphore (the caller keeps the device alive). Requires
// HasTimeline(); the debug switch APS5_NO_TIMELINE makes the device create the recorder without one.
void WaitSerial(std::uint64_t serial);
bool WaitSerialFor(std::uint64_t serial, std::uint64_t timeoutNanos);
// Under the mutex: finishes (completions, release) the in-flight batches up to `serial`, from the
// front only; tolerates batches another thread finished meanwhile. Later batches stay in flight.
void FinishUpTo(std::uint64_t serial);
@@ -219,6 +222,7 @@ public:
std::uint64_t SamplesTotal();
bool DumpSamples(VkDeviceAddress target, std::uint64_t address = 0);
void NoteSampledDraw(VkCommandBuffer commands);
void NoteDrawTraceToken(std::uint64_t token) noexcept;
bool QueuesSampleDumps() const;
void PrepareSampleSlot();
void EndPassSamples();
@@ -677,6 +681,8 @@ private:
std::uint64_t end;
};
std::vector<KeyStore> keyStores;
std::vector<std::uint64_t> drawTraceTokens;
std::unordered_set<std::uint64_t> drawTraceTokenSet;
};
struct LabelEntry {
std::uint32_t value;
@@ -887,6 +893,7 @@ private:
std::map<DrawSnapshotKey, IndexRange> indexRanges;
std::list<DrawSnapshotKey> indexRangeRecency;
bool refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes);
std::shared_ptr<Buffer> containingDrawSnapshot(std::uint64_t address, std::size_t bytes, VkDeviceSize* offset);
mutable std::weak_ptr<const WriteRanges> publishedWriteRanges;
mutable bool publishedWriteRangesValid = false;
};
@@ -33,7 +33,7 @@ void ClearCachedTextures(VkDevice device);
std::uint64_t ReleaseTextureCaches(std::uint64_t bytes, bool newImage = false);
std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory);
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes);
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes);
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes, std::uint64_t storageBytes = 0);
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport);
std::optional<std::uint32_t> LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory);
std::uint64_t StorageTextureCacheBudget(const Context& context);
@@ -67,6 +67,7 @@ struct ColorTarget {
bool uintExport = false;
std::uint32_t pipeBankXor = 0;
std::uint32_t samples = 1;
std::uint32_t coverageSamples = 1;
std::uint32_t dccControl = 0;
std::uint64_t fmaskAddress = 0;
std::uint32_t fmaskPipeBankXor = 0;
@@ -98,6 +99,7 @@ struct State {
std::optional<DepthTarget> depth;
bool depthTest = false;
bool depthWrite = false;
bool depthCopySource = false;
VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS;
bool depthBoundsTest = false;
float minDepthBounds = 0.0f;
@@ -152,6 +154,7 @@ struct State {
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
VkPipelineColorBlendAttachmentState blend;
std::array<float, 4> blendConstants;
std::uint8_t colorRop = 0xccu;
std::uint32_t samples = 1;
std::array<std::uint8_t, 32> sampleLocations{};
bool customSampleLocations = false;
@@ -170,6 +173,9 @@ struct State {
// distance vectors); a vertex program that exports pos1..pos3 depends on them.
inline constexpr std::uint32_t PositionExportLayoutBits = 0xffffu | (1u << 21u) | (1u << 22u) | (1u << 23u);
inline constexpr std::uint8_t CopyRop = 0xccu;
VkLogicOp LogicOpFromRop(std::uint8_t rop);
ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false);
void ApplyPointSize(const State& state, ShaderRecompiler::ShaderVertexStageInfo& vertex);
State DecodeState(const QueueState& queue, bool vertexWorkgroup = false);
@@ -217,9 +223,7 @@ struct ColorMetadataPass {
};
std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue);
struct DepthCopyPass {
std::uint64_t depthAddress;
VkExtent2D extent;
std::uint32_t samples;
DepthTarget source;
std::uint32_t sample;
ColorTarget color;
};
@@ -293,7 +297,7 @@ inline constexpr std::array<DrawKeyRange, 50> DrawKeyRegisters{{
// PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL.
{RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2af, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1},
// CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3.
{RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18},
{RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 0x10}, {RegisterBank::Context, 0x3a8, 0x18},
// The pixel program address, RSRC2 and user words; the geometry-back user pointer and program
// address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address;
// the hull user pointer, program address, RSRC2 and user words; the local program address.
@@ -56,7 +56,6 @@ namespace AgcDriver::Graphics {
void DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask, std::uint64_t cmaskOffset, std::size_t cmaskBytes, VkImageView view, std::uint32_t width, std::uint32_t height, std::uint32_t elementBytes, const std::array<std::uint32_t, 2>& clearWords, bool write);
std::uint32_t CmaskErrors();
void DispatchMultisampleReadback(VkCommandBuffer commands, VkFormat format, VkBuffer destination, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements);
void DispatchDepthSample(VkCommandBuffer commands, VkImageView depth, bool multisampled, VkImageView target, std::uint32_t width, std::uint32_t height, std::uint32_t sample);
void DispatchMultisampleImage(VkCommandBuffer commands, std::uint32_t elementBytes, VkBuffer source, std::uint64_t offset, std::uint64_t bytes, VkImageView arrayView, std::uint32_t width, std::uint32_t height, std::uint32_t layers, std::uint32_t fragments, std::uint32_t firstElement, std::uint32_t elements);
private:
@@ -85,11 +84,6 @@ namespace AgcDriver::Graphics {
VkPipelineLayout cmaskPipelineLayout = VK_NULL_HANDLE;
VkPipeline cmaskPipeline = VK_NULL_HANDLE;
DescriptorRing cmaskDescriptorSets;
VkDescriptorSetLayout depthSampleLayout = VK_NULL_HANDLE;
VkPipelineLayout depthSamplePipelineLayout = VK_NULL_HANDLE;
std::array<VkPipeline, 2> depthSamplePipelines{};
VkSampler depthSampler = VK_NULL_HANDLE;
DescriptorRing depthSampleSets;
std::unique_ptr<Buffer> cmaskErrors;
VkDescriptorSetLayout imageDescriptorLayout = VK_NULL_HANDLE;
VkPipelineLayout imagePipelineLayout = VK_NULL_HANDLE;
@@ -8,6 +8,7 @@
#include <mutex>
#include <unordered_map>
#include <cstring>
#include <cstdio>
#include <limits>
#include <optional>
#include <set>
@@ -29,13 +30,13 @@ inline bool NullVertexDescriptor(const ShaderRecompiler::VertexAttribute& attrib
return fields[0] == 0 && fields[1] == 0 && fields[2] == 0 && fields[3] == 0;
}
inline VertexFormat DecodeVertexFormat(const ShaderRecompiler::VertexAttribute& attribute) {
inline VertexFormat DecodeVertexFormat(const ShaderRecompiler::VertexAttribute& attribute, std::uint32_t minimumComponents = 0u) {
Require(attribute.components >= 1 && attribute.components <= 4, "invalid vertex attribute component count");
if (NullVertexDescriptor(attribute)) {
const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT};
return {formats[attribute.components - 1], attribute.components * 4u, 4u, "f32"};
}
const auto components = attribute.formatComponents == 0u ? attribute.components : attribute.formatComponents;
const auto components = std::max(attribute.formatComponents == 0u ? attribute.components : attribute.formatComponents, minimumComponents);
Require(components >= 1u && components <= 4u, "invalid vertex format component count");
const auto format = (attribute.resource.fields[3] >> 12u) & 0x7fu;
switch (format) {
@@ -106,11 +107,27 @@ inline std::string VertexAttributeSignature(const ShaderRecompiler::VertexAttrib
return std::string(format.scalar) + (attribute.components == 1 ? "" : "x" + std::to_string(attribute.components));
}
inline std::uint32_t VertexReadBytes(const ShaderRecompiler::VertexAttribute& attribute, VkFormat host) {
for (auto components = 1u; components <= 4u; ++components) {
const auto format = DecodeVertexFormat(attribute, components);
if (format.format == host) return format.bytes;
}
throw std::runtime_error("AGC graphics: vertex format " + std::to_string(host) + " is not a form of the attribute's buffer format");
}
struct VertexInputLayout {
std::vector<VkVertexInputBindingDescription> bindings;
std::vector<VkVertexInputAttributeDescription> attributes;
};
inline std::array<char, 160> VertexDescriptorFlagsMessage(const ShaderRecompiler::VertexAttribute& attribute) {
std::array<char, 160> message{};
const auto& fields = attribute.resource.fields;
std::snprintf(message.data(), message.size(), "unsupported vertex buffer descriptor flags at location %u (V#=[%08x %08x %08x %08x])",
attribute.location, fields[0], fields[1], fields[2], fields[3]);
return message;
}
inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::span<const ShaderRecompiler::VertexAttribute> attributes) {
Require(attributes.size() <= context.limits.maxVertexInputBindings && attributes.size() <= context.limits.maxVertexInputAttributes, "vertex input count exceeds device limits");
VertexInputLayout result;
@@ -120,18 +137,29 @@ inline VertexInputLayout BuildVertexInputLayout(const Context& context, std::spa
const auto format = DecodeVertexFormat(attribute);
Require(attribute.location < context.limits.maxVertexInputAttributes && locations.insert(attribute.location).second, "invalid or duplicate vertex attribute location");
Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index");
Require((fields[1] & 0x80000000u) == 0 && (fields[3] & 0x00800000u) == 0 && (fields[3] >> 30u) == 0, "unsupported vertex buffer descriptor flags");
if ((fields[1] & 0x80000000u) != 0 || (fields[3] & 0x00800000u) != 0 || (fields[3] >> 30u) != 0) {
Require(false, VertexDescriptorFlagsMessage(attribute).data());
}
const auto stride = (fields[1] >> 16u) & 0x3fffu;
const auto address = fields[0] | (static_cast<std::uint64_t>(fields[1] & 0xffffu) << 32u);
Require(NullVertexDescriptor(attribute) || (address != 0 && address % format.alignment == 0 && stride % format.alignment == 0), "unaligned vertex buffer");
Require(stride <= context.limits.maxVertexInputBindingStride, "vertex stride exceeds device limits");
Require(context.formatProperties != nullptr, "missing vertex format property query");
VkFormatProperties properties{};
context.formatProperties(context.physical, format.format, &properties);
Require((properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0, "device does not support vertex format " + std::to_string(format.format));
const auto supported = [&](VkFormat candidate) {
VkFormatProperties properties{};
context.formatProperties(context.physical, candidate, &properties);
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
};
auto host = format.format;
auto hostSupported = supported(host);
for (auto components = 1u; !hostSupported && components <= 4u; ++components) {
host = DecodeVertexFormat(attribute, components).format;
hostSupported = supported(host);
}
Require(hostSupported, "device does not support vertex format " + std::to_string(format.format));
const auto binding = static_cast<std::uint32_t>(result.bindings.size());
result.bindings.push_back({binding, stride, attribute.fetchIndex == 0 ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE});
result.attributes.push_back({attribute.location, binding, format.format, 0});
result.attributes.push_back({attribute.location, binding, host, 0});
}
return result;
}
@@ -1,27 +0,0 @@
#version 450
layout(local_size_x = 8, local_size_y = 8) in;
#if MULTISAMPLED
layout(set = 0, binding = 0) uniform sampler2DMS depth;
#else
layout(set = 0, binding = 0) uniform sampler2D depth;
#endif
layout(set = 0, binding = 1, r32f) writeonly uniform image2D target;
layout(push_constant) uniform Push {
uint width;
uint height;
int sampleIndex;
} push;
void main() {
uvec2 pixel = gl_GlobalInvocationID.xy;
if (pixel.x >= push.width || pixel.y >= push.height) return;
#if MULTISAMPLED
float value = texelFetch(depth, ivec2(pixel), push.sampleIndex).r;
#else
float value = texelFetch(depth, ivec2(pixel), 0).r;
#endif
imageStore(target, ivec2(pixel), vec4(value, 0.0, 0.0, 0.0));
}
@@ -427,8 +427,8 @@ public:
return (ResolveTextureFormat(resource.format) == expected || depthBits || UnormStencilView(resource)) && oneSlice && resource.width == target.extent.width && resource.height == target.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && resource.baseArray == 0;
}
std::shared_ptr<Texture> Sampled(std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, bool drawn = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) {
CheckCurrent();
std::shared_ptr<Texture> Sampled(std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, bool drawn = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D, bool copySource = false) {
if (!copySource) CheckCurrent();
CheckResource(resource);
std::array<std::uint32_t, 13> key{};
std::copy_n(words.begin(), std::min<std::size_t>(words.size(), 8), key.begin());
@@ -468,7 +468,7 @@ public:
const auto sampledImage = useSampleShadow ? sampleShadow : owned;
auto texture = std::make_shared<Texture>(context, useSampleShadow ? sampleShadow->image : image, hostFormat, stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, components, viewType, 0, sampledImage);
if (multisampled) {
Require(samplingActive, "native multisample depth sampling requires its creation-time owner");
Require(copySource || samplingActive, "native multisample depth sampling requires its creation-time owner");
texture->MarkRefreshedPerUse();
}
textures.emplace(key, texture);
@@ -511,30 +511,6 @@ public:
else Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
}
void CopySample(std::uint32_t sample, StorageTexture& destination, VkImageView destinationView) {
auto* recorder = Recorder::Active();
Require(recorder != nullptr, "depth sample copies need an active recorder");
Require(sampleable, "DB depth copy from a depth surface the device cannot sample at its sample count");
ApplyFastClear();
if (useSampleShadow) {
if (sampleShadow == nullptr) createSampleShadow();
refreshSampleShadow(false);
recorder->Keep(sampleShadow);
}
if (depthView == VK_NULL_HANDLE) {
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = useSampleShadow ? sampleShadow->image : image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = hostFormat;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &depthView), "vkCreateImageView depth sample copy");
}
Require(context.detiler != nullptr, "depth sample copies need the texture detiler");
context.detiler->DispatchDepthSample(recorder->Commands(), depthView, target.samples > 1, destinationView, target.extent.width, target.extent.height, sample);
Recorder::CountBarriers(Recorder::CommandClass::Copy, 2);
destination.MarkDirty();
}
bool Holds(const Texture* texture) const {
const auto held = [&](const auto& entry) { return entry.second.get() == texture; };
return std::any_of(textures.begin(), textures.end(), held) || std::any_of(stencilCopy.textures.begin(), stencilCopy.textures.end(), held);
@@ -915,7 +891,6 @@ public:
const VkFormat hostFormat;
VkImage image = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkImageView depthView = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
std::shared_ptr<std::atomic<bool>> active = std::make_shared<std::atomic<bool>>(true);
bool sampleable = true;
@@ -1175,7 +1150,6 @@ private:
releaseStencilCopy();
mirrorPlane = {};
textures.clear();
if (depthView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, depthView, nullptr);
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (owned != nullptr) owned.reset();
else {
@@ -1218,8 +1192,8 @@ public:
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth plane copy");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy");
stagingSize = static_cast<std::size_t>(sliceBytes()) * layers;
staging = std::make_unique<DeviceBuffer>(context, stagingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
imageBytes = requirements.size;
owned = std::make_shared<OwnedImage>(context, image, memory);
} catch (...) {
release();
throw;
@@ -1238,12 +1212,7 @@ public:
std::snprintf(text, sizeof(text), "AGC graphics: depth plane copy geometry does not match its layers (copy %ux%u, %u layers, %llu bytes a slice; texture %ux%u guest format %u, dimension %d, tile mode %u, %u layers, %llu bytes a slice, %zu mips)", extent.width, extent.height, layers, static_cast<unsigned long long>(sliceBytes()), resource.width, resource.height, resource.format, static_cast<int>(resource.dimension), resource.tileMode, geometry.layers, static_cast<unsigned long long>(geometry.sliceLinearBytes), geometry.mips.size());
throw std::runtime_error(text);
}
const auto stagingBytes = static_cast<std::size_t>(geometry.sliceLinearBytes) * layers;
if (stagingBytes > stagingSize) {
retiredStaging.push_back(std::move(staging));
staging = std::make_unique<DeviceBuffer>(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
stagingSize = stagingBytes;
}
const auto stagingBytes = std::max<std::size_t>(static_cast<std::size_t>(geometry.sliceLinearBytes), static_cast<std::size_t>(sliceBytes())) * layers;
std::vector<std::shared_ptr<Buffer>> uploads;
std::vector<std::uint32_t> uploadLayers;
std::vector<std::uint32_t> copiedLayers;
@@ -1269,11 +1238,14 @@ public:
uploadLayers.push_back(layer);
held = current;
}
if (!copiedLayers.empty() || !uploads.empty()) record(slices, resource, geometry, copiedLayers, uploads, uploadLayers);
if (!copiedLayers.empty() || !uploads.empty()) {
if (staging == nullptr || stagingBytes > staging->Size()) staging = std::make_shared<DeviceBuffer>(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
record(slices, resource, geometry, copiedLayers, uploads, uploadLayers, staging);
}
const std::array<std::uint32_t, 6> key{static_cast<std::uint32_t>(components.r), static_cast<std::uint32_t>(components.g), static_cast<std::uint32_t>(components.b), static_cast<std::uint32_t>(components.a), static_cast<std::uint32_t>(viewType), baseLayer};
auto& texture = textures[key];
if (texture == nullptr) {
texture = std::make_shared<Texture>(context, image, format, aspect(), components, viewType, baseLayer);
texture = std::make_shared<Texture>(context, image, format, aspect(), components, viewType, baseLayer, owned);
texture->MarkRefreshedPerUse();
}
return texture;
@@ -1283,6 +1255,9 @@ public:
const VkExtent2D extent;
const VkFormat format;
const std::uint32_t layers;
VkDeviceSize imageBytes = 0;
std::uint64_t lastUse = 0;
VkDeviceSize HeldBytes() const { return imageBytes + (staging != nullptr ? staging->Size() : 0u); }
private:
struct GuestLayer {
@@ -1295,15 +1270,14 @@ private:
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
std::unique_ptr<DeviceBuffer> staging;
std::size_t stagingSize = 0;
std::vector<std::unique_ptr<DeviceBuffer>> retiredStaging;
std::shared_ptr<OwnedImage> owned;
std::shared_ptr<DeviceBuffer> staging;
std::map<std::array<std::uint32_t, 6>, std::shared_ptr<Texture>> textures;
std::vector<GuestLayer> guestLayers;
std::vector<DepthPlaneSlice> surfaceLayers;
bool filled = false;
void record(std::span<const DepthPlaneSlice> slices, const GuestTextureResource& resource, const SurfaceGeometry& geometry, std::span<const std::uint32_t> copiedLayers, std::span<const std::shared_ptr<Buffer>> uploads, std::span<const std::uint32_t> uploadLayers) {
void record(std::span<const DepthPlaneSlice> slices, const GuestTextureResource& resource, const SurfaceGeometry& geometry, std::span<const std::uint32_t> copiedLayers, std::span<const std::shared_ptr<Buffer>> uploads, std::span<const std::uint32_t> uploadLayers, const std::shared_ptr<DeviceBuffer>& staging) {
if (!uploads.empty()) {
Require(context.detiler != nullptr, "depth plane copy requires a texture detiler");
context.detiler->BeginBatch();
@@ -1360,7 +1334,11 @@ private:
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
filled = true;
if (batch) batch->SubmitAndWait();
else Recorder::CountBarriers(Recorder::CommandClass::Draw, 3);
else {
Recorder::CountBarriers(Recorder::CommandClass::Draw, 3);
recorder->Keep(staging, staging->Size());
if (owned != nullptr) recorder->Keep(owned);
}
}
VkDeviceSize sliceBytes() const {
@@ -1374,8 +1352,11 @@ private:
void release() noexcept {
textures.clear();
staging.reset();
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
if (owned != nullptr) owned.reset();
else {
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
}
image = VK_NULL_HANDLE;
memory = VK_NULL_HANDLE;
}
@@ -1386,6 +1367,32 @@ std::map<std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, VkFor
return *copies;
}
std::uint64_t& planeCopyClock() {
static std::uint64_t clock = 0;
return clock;
}
void evictPlaneCopies(const std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, VkFormat, std::uint32_t>& keep) {
static const VkDeviceSize budget = [] {
const char* text = std::getenv("APS5_DEPTH_PLANE_COPY_MIB");
return static_cast<VkDeviceSize>(text != nullptr ? std::strtoull(text, nullptr, 10) : 256ull) << 20u;
}();
if (budget == 0) return;
auto& copies = planeCopies();
VkDeviceSize total = 0;
for (const auto& entry : copies) total += entry.second->HeldBytes();
while (total > budget) {
auto oldest = copies.end();
for (auto it = copies.begin(); it != copies.end(); ++it) {
if (it->first == keep) continue;
if (oldest == copies.end() || it->second->lastUse < oldest->second->lastUse) oldest = it;
}
if (oldest == copies.end()) break;
total -= oldest->second->HeldBytes();
copies.erase(oldest);
}
}
struct LayeredSlice {
DepthSurface* surface;
std::uint32_t layer;
@@ -2098,6 +2105,12 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint6
const bool overlapped = !surface->syncing && plane < end && address < plane + planeBytes;
if (overlapped && (surface->pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) surface->MarkAllStale(planeBytes);
if (overlapped && (scope == DepthPlaneScope::Whole || anyUnit(surface->StaleUnitsIn(planeBytes, address, end)))) {
if (GuestMemory::CommittedRanges(plane, static_cast<std::size_t>(planeBytes)).empty()) {
static std::atomic<bool> reported{false};
if (!reported.exchange(true)) std::fprintf(stderr, "[gpu] depth plane 0x%llx+0x%llx was unmapped with GPU results pending; they are dropped\n", static_cast<unsigned long long>(plane), static_cast<unsigned long long>(planeBytes));
surface->ClearStale();
continue;
}
surface->SyncFromMemory(resource);
auto units = scope == DepthPlaneScope::Whole ? surface->staleUnits : surface->StaleUnitsIn(planeBytes, address, end);
if (anyUnit(units)) {
@@ -2311,6 +2324,16 @@ void ClearDepthSurfaces(VkDevice device) {
UpdateResidentImageUnmapObserver_nid_no_patch();
}
std::size_t DepthPlaneCopyCount() {
return planeCopies().size();
}
namespace {
thread_local bool depthCopySourceActive = false;
}
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType) {
const auto held = [&](const auto& surface) {
return surface->active->load(std::memory_order_acquire) && surface->context.device == context.device && !surface->retired && (surface->target.address == resource.baseAddress || (surface->target.stencilAddress != 0 && surface->target.stencilAddress == resource.baseAddress));
@@ -2331,12 +2354,17 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
Require(base.samples > 1u, "multisample reader overlaps a single-sample depth attachment");
auto* recorder = RecorderFor(context);
Require(recorder != nullptr, "a native multisampled depth target is sampled only by recorded draws and dispatches");
found->CheckCurrent();
found->CheckResource(resource);
Require(found->pendingClear == 0u, "native multisample depth fast-clear transition is not implemented");
if (depthCopySourceActive) {
found->CheckResource(resource);
found->ApplyFastClear();
} else {
found->CheckCurrent();
found->CheckResource(resource);
Require(found->pendingClear == 0u, "native multisample depth fast-clear transition is not implemented");
}
RecordMemoryBarrier(context, recorder->Commands(), VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
Recorder::CountBarriers(Recorder::CommandClass::Draw, 1);
return found->Sampled(words, resource, components, found == drawnDepth, multisampleViewType);
return found->Sampled(words, resource, components, found == drawnDepth, multisampleViewType, depthCopySourceActive);
}
if (resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0) return nullptr;
const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT;
@@ -2372,11 +2400,14 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
else slices.push_back({slice->image, slice == drawnDepth ? 0u : slice->written});
}
const auto imageFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT;
auto& copy = planeCopies()[{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers}];
if (copy == nullptr) {
copy = std::make_unique<DepthPlaneCopy>(context, base.extent, imageFormat, layers);
}
return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u);
auto& copies = planeCopies();
const std::tuple<VkDevice, std::uint64_t, std::uint32_t, std::uint32_t, VkFormat, std::uint32_t> copyKey{context.device, base.address, base.extent.width, base.extent.height, imageFormat, layers};
auto& copy = copies[copyKey];
if (copy == nullptr) copy = std::make_unique<DepthPlaneCopy>(context, base.extent, imageFormat, layers);
copy->lastUse = ++planeCopyClock();
auto texture = copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u);
evictPlaneCopies(copyKey);
return texture;
}
bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture) {
@@ -2387,7 +2418,7 @@ bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resou
std::lock_guard lock(surfacesMutex());
auto* found = planeSurface(context.device, address);
if (found == nullptr || !found->Holds(texture)) return false;
found->CheckCurrent();
if (!depthCopySourceActive) found->CheckCurrent();
found->CheckResource(resource);
return true;
}
@@ -2522,15 +2553,23 @@ std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std:
return layeredDepthStorage(context, surface, resource, mip);
}
void CopyDepthSample(const Context& context, std::uint64_t depthAddress, std::uint32_t samples, std::uint32_t sample, StorageTexture& destination, VkImageView destinationView, VkExtent2D extent) {
std::lock_guard lock(surfacesMutex());
auto* found = planeSurface(context.device, depthAddress);
Require(found != nullptr && found->target.address == depthAddress, "DB depth copy from a depth plane no draw rendered");
Require(found->target.format == VK_FORMAT_D32_SFLOAT || found->target.format == VK_FORMAT_D32_SFLOAT_S8_UINT, "DB depth copy from a depth surface that is not 32-bit float");
Require(found->target.samples == samples, "DB depth copy from a depth surface of another sample count");
Require(found->target.extent.width == extent.width && found->target.extent.height == extent.height, "DB depth copy from a depth surface of another size");
Require(sample < found->target.samples, "DB depth copy of a sample the depth surface does not have");
found->CopySample(sample, destination, destinationView);
DepthCopySourceScope::DepthCopySourceScope(bool active) : previous(depthCopySourceActive) {
depthCopySourceActive = active;
}
DepthCopySourceScope::~DepthCopySourceScope() {
depthCopySourceActive = previous;
}
std::array<std::uint32_t, 8> DepthCopySourceWords(const DepthTarget& target) {
const bool multisampled = target.samples != 1u;
const auto base = (target.address | target.pipeBankXor) >> 8u;
const auto metadata = multisampled ? target.htileAddress >> 8u : 0u;
const auto count = static_cast<std::uint32_t>(std::countr_zero(target.samples));
const auto width = target.extent.width - 1u, height = target.extent.height - 1u;
const auto type = multisampled ? 14u : 9u;
const auto word6 = (multisampled && target.iterate256 ? 1u << 10u : 0u) | (multisampled && target.htilePipeAligned ? 1u << 19u : 0u) | (metadata != 0 ? 1u << 21u : 0u) | (static_cast<std::uint32_t>(metadata & 255u) << 24u);
return {static_cast<std::uint32_t>(base), static_cast<std::uint32_t>((base >> 32u) & 255u) | (22u << 20u) | ((width & 3u) << 30u), (width >> 2u) | (height << 14u), (type << 28u) | (target.depthSwizzle << 20u) | (count << 16u) | 0x204u, 0u, count << 4u, word6, static_cast<std::uint32_t>(metadata >> 8u)};
}
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage) {
@@ -16,6 +16,7 @@
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Mutex.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "ThreadOwned.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
@@ -408,6 +409,19 @@ CmaskState readCmask(const ColorTarget& color, const CmaskLayout& layout, std::v
std::vector<std::byte> mask(bytes);
GuestMemory::Read(address, mask, CmaskLayout::Alignment);
const auto uniform = [&](std::byte value) { return std::all_of(mask.begin(), mask.end(), [&](std::byte entry) { return entry == value; }); };
if (color.coverageSamples > color.samples) {
for (std::uint32_t tileY = 0; tileY < layout.TilesY(); ++tileY) {
for (std::uint32_t tileX = 0; tileX < layout.TilesX(); ++tileX) {
const auto nibble = layout.Nibble(tileX, tileY);
const auto code = (std::to_integer<std::uint32_t>(mask[nibble / 2u]) >> ((nibble % 2u) * 4u)) & 0xfu;
if (code != 0u && (code >> 2u) != 3u) {
char text[128];
std::snprintf(text, sizeof(text), "AGC graphics: CMASK code 0x%x of a one-fragment color target is not implemented", code);
throw std::runtime_error(text);
}
}
}
}
if (uniform(std::byte{0xff})) return CmaskState::Expanded;
if (uniform(std::byte{0})) return CmaskState::Cleared;
for (std::uint32_t tileY = 0; tileY < layout.TilesY(); ++tileY) {
@@ -464,6 +478,10 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, con
const auto bytes = layout.Bytes();
Require(color.cmaskBytes == bytes, "CMASK size differs from the color target's CMASK layout");
if (cmaskProved(color.cmaskAddress, bytes)) return false;
if (color.coverageSamples > color.samples) {
std::vector<std::uint32_t> codes;
static_cast<void>(readCmask(color, layout, codes));
}
auto* recorder = Recorder::Active();
if (resident != nullptr && context.detiler != nullptr && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread()) {
const auto* import = HostImportFor(context, color.cmaskAddress, bytes);
@@ -536,7 +554,7 @@ constexpr std::array<const char*, PhaseCount> DrawPhaseNames{"validate", "vertex
enum SyncReason : std::size_t { SyncNone, SyncNotResident, SyncNoRecorder, SyncDisabled, SyncCopiedWrites, SyncLease, SyncCount };
constexpr std::array<const char*, SyncCount> SyncReasonNames{"none", "non-resident target", "no recorder", "disabled", "copied writes", "lease"};
constexpr std::size_t IndirectPathCount = static_cast<std::size_t>(IndirectDrawPath::Count);
constexpr std::array<const char*, IndirectPathCount> IndirectDrawPathNames{"gpu-side", "patched SGPR not folded", "fetch offset unknown", "non-vertex path", "GE_INDX_OFFSET", "draw index", "vertex range too large", "feature gap", "pending image results", "pending label or copied write", "not imported", "disabled", "index range not fully readable"};
constexpr std::array<const char*, IndirectPathCount> IndirectDrawPathNames{"gpu-side", "patched SGPR not folded", "fetch offset unknown", "non-vertex path", "GE_INDX_OFFSET", "draw index", "vertex range too large", "feature gap", "pending image results", "pending label or copied write", "not imported", "disabled", "index range not fully readable", "index limit unknown"};
// What became of one draw, for the totals.
struct DrawOutcome {
@@ -763,8 +781,8 @@ void reportDraw(const std::array<double, PhaseCount>& us, const ShaderResources:
for (std::size_t i = 1; i < profile.recipeMisses.size() && room(); ++i) {
n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), " %s %llu", DrawRecipeMissName(static_cast<DrawRecipeMiss>(i)), static_cast<unsigned long long>(profile.recipeMisses[i]));
}
std::fprintf(stderr, "%s\n", line);
std::fprintf(stderr, "[rescache] draws: %llu hits, %llu misses, %llu invalidated, %llu uncacheable; validation memo %llu hits / %llu misses (which key words the misses differ in: the miss churn line)\n", static_cast<unsigned long long>(profile.cacheHits), static_cast<unsigned long long>(profile.cacheMisses), static_cast<unsigned long long>(profile.cacheInvalidated), static_cast<unsigned long long>(profile.uncacheable), static_cast<unsigned long long>(profile.validateHits), static_cast<unsigned long long>(profile.validateMisses));
AgcDriver::ProfilePrint_nid_no_patch("%s\n", line);
AgcDriver::ProfilePrint_nid_no_patch("[rescache] draws: %llu hits, %llu misses, %llu invalidated, %llu uncacheable; validation memo %llu hits / %llu misses (which key words the misses differ in: the miss churn line)\n", static_cast<unsigned long long>(profile.cacheHits), static_cast<unsigned long long>(profile.cacheMisses), static_cast<unsigned long long>(profile.cacheInvalidated), static_cast<unsigned long long>(profile.uncacheable), static_cast<unsigned long long>(profile.validateHits), static_cast<unsigned long long>(profile.validateMisses));
profile.totalsUs.fill(0);
profile.maxUs.fill(0);
profile.maxDrawUs = profile.hookWaitUs = profile.maxHookWaitUs = profile.ownSyncUs = 0;
@@ -1278,8 +1296,8 @@ bool StripPatches(const State& state, std::span<const CompiledShader> shaders) {
return state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP && BarycentricPatches(state, shaders);
}
std::shared_ptr<Buffer> zeroVertexBuffer(const Context& context, const ShaderRecompiler::VertexAttribute& attribute) {
auto zero = std::make_shared<Buffer>(context, DecodeVertexFormat(attribute).bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
std::shared_ptr<Buffer> zeroVertexBuffer(const Context& context, std::size_t bytes) {
auto zero = std::make_shared<Buffer>(context, bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);
std::fill(zero->Bytes().begin(), zero->Bytes().end(), std::byte{0});
return zero;
}
@@ -1524,14 +1542,19 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
fetchValid.reserve(attributes.size());
std::vector<std::size_t> fetchOf(attributes.size(), 0);
std::vector<std::shared_ptr<Buffer>> zeroed(attributes.size());
const auto& layout = inputs.VertexLayout();
for (std::size_t i = 0; i < attributes.size(); ++i) {
const auto& attribute = attributes[i];
const auto described = std::find_if(layout.attributes.begin(), layout.attributes.end(), [&](const auto& entry) { return entry.location == attribute.location; });
Require(described != layout.attributes.end(), "vertex attribute missing from its input layout");
const auto narrow = DecodeVertexFormat(attribute);
const std::size_t widened = described->format == narrow.format ? 0u : VertexReadBytes(attribute, described->format) - narrow.bytes;
if (NullVertexDescriptor(attribute) || VertexFetchOutOfRange(attribute)) {
zeroed[i] = zeroVertexBuffer(context, attribute);
zeroed[i] = zeroVertexBuffer(context, narrow.bytes + widened);
continue;
}
// An indirect draw's counts are unknown here: the descriptor's whole range is copied.
const auto bytes = args != nullptr ? VertexBufferExtent(attribute) : VertexBufferReadSize(attribute, inputs.maxIndex, draw.instanceCount, draw.firstInstance);
const auto bytes = (args != nullptr ? VertexBufferExtent(attribute) : VertexBufferReadSize(attribute, inputs.maxIndex, draw.instanceCount, draw.firstInstance)) + widened;
const auto& fields = attribute.resource.fields;
const auto address = fields[0] | (static_cast<std::uint64_t>(fields[1] & 0xffffu) << 32u);
const auto firstByte = args == nullptr ? VertexBufferReadOffset(attribute, draw.firstVertex, draw.firstInstance) : 0;
@@ -1712,16 +1735,24 @@ struct IndirectRecord {
// The draw commands of one draw: the vertex and index buffer binds, then the direct draw, the
// GPU-side indirect draw from `argumentBuffer` or the CPU-read records with the driver's rules.
void recordDrawCommands(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, const IndirectRecord* indirect, VkBuffer argumentBuffer, VkDeviceSize argumentOffset, std::uint32_t firstIndex = 0) {
void recordDrawCommands(const Context& context, VkCommandBuffer commands, const State& state, const Pm4::DrawParameters& draw, const DrawInputs& inputs, const IndirectRecord* indirect, VkBuffer argumentBuffer, VkDeviceSize argumentOffset, const DrawTraceContext* trace, std::uint32_t& subdraw, std::uint32_t firstIndex = 0) {
if (context.recorder != nullptr) context.recorder->NoteSampledDraw(commands);
const auto recorded = [&](std::uint32_t index) {
if (trace != nullptr && context.recorder != nullptr) {
context.recorder->NoteDrawTraceToken(trace->token);
CaptureTrace::Log("draw-recorded token=%llu batch=%llu submission=%llu queue=%u offset=%zu target=%llx subdraw=%u", static_cast<unsigned long long>(trace->token), static_cast<unsigned long long>(context.recorder->OpenBatchSerial()), static_cast<unsigned long long>(trace->submission), trace->queue, trace->offset, static_cast<unsigned long long>(trace->target), index);
}
};
const auto* args = indirect != nullptr ? indirect->args : nullptr;
if (state.stages.mesh && args != nullptr) {
context.Function<PFN_vkCmdDrawMeshTasksIndirectEXT>("vkCmdDrawMeshTasksIndirectEXT")(commands, argumentBuffer, argumentOffset, 1, sizeof(VkDrawMeshTasksIndirectCommandEXT));
recorded(subdraw++);
return;
}
if (state.stages.mesh) {
APS5_LOG_OUT_DEBUG("vkCmdDrawMeshTasksEXT groups=%u instances=%u", inputs.meshGroups, draw.instanceCount);
context.Function<PFN_vkCmdDrawMeshTasksEXT>("vkCmdDrawMeshTasksEXT")(commands, inputs.meshGroups, draw.instanceCount, 1);
recorded(subdraw++);
return;
}
if (!inputs.vertexHandles.empty()) context.Resolved(&DeviceFunctions::cmdBindVertexBuffers, "vkCmdBindVertexBuffers")(commands, 0, static_cast<std::uint32_t>(inputs.vertexHandles.size()), inputs.vertexHandles.data(), inputs.vertexOffsets.data());
@@ -1732,8 +1763,13 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const
}
if (draw.indexed) context.Resolved(&DeviceFunctions::cmdBindIndexBuffer, "vkCmdBindIndexBuffer")(commands, inputs.indexHandle, inputs.indexOffset, draw.indexSize == 2 ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32);
if (args == nullptr) {
if (draw.indexed) context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, draw.indexCount, draw.instanceCount, firstIndex, static_cast<std::int32_t>(draw.firstVertex), draw.firstInstance);
else context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, draw.indexCount, draw.instanceCount, draw.firstVertex, draw.firstInstance);
if (draw.indexed) {
context.Resolved(&DeviceFunctions::cmdDrawIndexed, "vkCmdDrawIndexed")(commands, draw.indexCount, draw.instanceCount, firstIndex, static_cast<std::int32_t>(draw.firstVertex), draw.firstInstance);
recorded(subdraw++);
} else {
context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, draw.indexCount, draw.instanceCount, draw.firstVertex, draw.firstInstance);
recorded(subdraw++);
}
} else if (indirect->path == IndirectDrawPath::Gpu) {
if (args->countIndirect) {
if (draw.indexed) context.Function<PFN_vkCmdDrawIndexedIndirectCountKHR>("vkCmdDrawIndexedIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, HostImportOffset(*indirect->countImport, args->countAddress), args->count, args->stride);
@@ -1986,6 +2022,7 @@ void recordStencilReplay(const Context& context, VkCommandBuffer commands, const
const auto referenceOffset = fragment->pushConstantOffset + fragment->program->stencilReferenceDword * sizeof(std::uint32_t);
Require(referenceOffset <= block.size() - sizeof(std::uint32_t), "stencil replay reference exceeds the push block");
const auto run = [&](const Pm4::DrawParameters& piece, std::uint32_t firstIndex) {
std::uint32_t unusedSubdraw = 0;
for (const auto& [reference, pipeline] : replay) {
selected.stencilReplayReference = reference;
selected.stencilFront.reference = reference;
@@ -1993,7 +2030,7 @@ void recordStencilReplay(const Context& context, VkCommandBuffer commands, const
context.Resolved(&DeviceFunctions::cmdSetStencilReference, "vkCmdSetStencilReference")(commands, VK_STENCIL_FACE_FRONT_AND_BACK, reference);
std::memcpy(block.data() + referenceOffset, &reference, sizeof(reference));
pipeline->PushConstants(commands, stages, block);
recordDrawCommands(context, commands, selected, piece, inputs, nullptr, VK_NULL_HANDLE, 0, firstIndex);
recordDrawCommands(context, commands, selected, piece, inputs, nullptr, VK_NULL_HANDLE, 0, nullptr, unusedSubdraw, firstIndex);
}
};
if (replay.size() == 1u) {
@@ -2126,7 +2163,8 @@ bool WriteOnlyPasses() {
return enabled;
}
void recordDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, DrawInputs& inputs, RecordedDraw& record, DrawOutcome& outcome, DrawTimer& timer, double& ownWaitedMs) {
void recordDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, DrawInputs& inputs, RecordedDraw& record, DrawOutcome& outcome, DrawTimer& timer, double& ownWaitedMs, const DrawTraceContext* trace = nullptr) {
std::uint32_t subdraw = 0;
auto* recorder = record.recorder;
auto& resources = *record.resources;
const auto* args = record.indirect != nullptr ? record.indirect->args : nullptr;
@@ -2285,18 +2323,18 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
APS5_LOG_CHARS_OUT_DEBUG("Resources bound");
pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty());
APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded");
if (record.stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset);
if (record.stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset, trace, subdraw);
else {
recordStencilReplay(context, commands, state, draw, inputs, record.stencilReplay, shaders, resources, record.pushBytes, record.pushStages);
for (const auto& [reference, pipeline] : record.stencilReplay) recorder->Keep(pipeline);
}
if (record.pipeline->SplitsFaces()) {
record.pipeline->ContinueBackFaces(commands, state);
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset);
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset, trace, subdraw);
}
if (record.pipeline->SplitsSecondChannel()) {
record.pipeline->ContinueSecondChannel(commands);
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset);
recordDrawCommands(context, commands, state, draw, inputs, record.indirect, argumentBuffer, argumentOffset, trace, subdraw);
}
if (meshArguments != nullptr) recorder->Keep(meshArguments);
if (args != nullptr) CountIndirectDraw(record.indirect->path, record.indirect->readMs, rewritten);
@@ -2411,6 +2449,7 @@ struct PreparedDraw {
PreparedDraw(const PreparedDraw&) = delete;
PreparedDraw& operator=(const PreparedDraw&) = delete;
std::shared_ptr<const DrawRecipe>* recipeOut = nullptr;
const DrawTraceContext* trace = nullptr;
std::vector<std::uint32_t> stencilReferences;
std::optional<PerformanceTimer> timing;
DrawTimer timer;
@@ -2519,6 +2558,7 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara
const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes);
static_cast<void>(FlushDepthPlanes(color.address, colorLayout.Bytes(), DepthPlaneScope::Whole));
RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes());
if (color.fmaskAddress != 0) GuardOneFragmentFmask(context, color);
constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
timer.phase(PhaseSetup);
// Debug aid: APS5_NO_RESIDENT_TARGETS=1 copies every target in and out again.
@@ -2881,7 +2921,7 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
record.reordered = reordered;
record.reorderPass = reorderPass;
record.depthLocations = depthLocations;
recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs);
recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs, prepared.trace);
// The recipe for the caller's draw-cache entry (design_cpu_final M8, rule R3 for draws):
// 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
@@ -3017,18 +3057,19 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
APS5_LOG_CHARS_OUT_DEBUG("Resources bound");
pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty());
APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded");
if (stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset);
std::uint32_t subdraw = 0;
if (stencilReplay.empty()) recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset, prepared.trace, subdraw);
else {
recordStencilReplay(context, commands, state, draw, inputs, stencilReplay, shaders, *resources);
if (recorded) for (const auto& [reference, selectedPipeline] : stencilReplay) recorder->Keep(selectedPipeline);
}
if (pipeline->SplitsFaces()) {
pipeline->ContinueBackFaces(commands, state);
recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset);
recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset, prepared.trace, subdraw);
}
if (pipeline->SplitsSecondChannel()) {
pipeline->ContinueSecondChannel(commands);
recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset);
recordDrawCommands(context, commands, state, draw, inputs, args != nullptr ? &indirect : nullptr, argumentBuffer, argumentOffset, prepared.trace, subdraw);
}
if (meshArguments != nullptr && recorded) recorder->Keep(meshArguments);
if (args != nullptr) CountIndirectDraw(indirect.path, indirect.readMs, rewritten);
@@ -3185,14 +3226,17 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D
}
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut) {
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut, const DrawTraceContext* trace) {
const DepthCopySourceScope depthCopySource(state.depthCopySource);
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
PreparedDraw prepared(profile);
prepared.recipeOut = recipeOut;
prepared.trace = trace;
if (prepareDraw(context, state, draw, shaders, snapshots, prepared)) recordPreparedDraw(context, state, draw, shaders, prepared);
}
DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, const DrawRecipe& recipe) {
DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, const DrawRecipe& recipe, const DrawTraceContext* trace) {
const DepthCopySourceScope depthCopySource(state.depthCopySource);
if (context.recorder != nullptr) context.recorder->BoundKeptBytes();
static_cast<void>(snapshots);
PerformanceTimer timing("Graphics.DrawWithRecipe");
@@ -3233,6 +3277,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
targets.reserve(recipe.targets.size());
for (std::size_t index = 0; index < recipe.targets.size(); ++index) {
timer.phase(PhaseSetup);
if (state.colors[index].fmaskAddress != 0) GuardOneFragmentFmask(context, state.colors[index]);
auto stored = recipe.targets[index].lock();
if (stored == nullptr || !StorageImageCached(context, stored.get()) || !StorageImageServesKeys(*stored, state.colors[index].dccAddress)) return miss(DrawRecipeMiss::TargetGone);
auto resident = refreshResidentTarget(context, state, state.colors[index], outcome, profile, [&] {
@@ -3300,7 +3345,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
record.pushBytes = &recipe.pushBytes;
record.pushStages = recipe.pushStages;
record.depthLocations = passDepthLocations(context, state, *record.pipeline);
recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs);
recordDraw(context, state, draw, shaders, inputs, record, outcome, timer, ownWaitedMs, trace);
if (profile) {
result.recordUs = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - recordStart).count();
auto& stats = Profile();
@@ -3323,12 +3368,6 @@ std::shared_ptr<StorageTexture> metadataPassResident(const Context& context, con
}
void RunDepthCopyPass(const Context& context, const DepthCopyPass& pass) {
Require(pass.color.tileMode != ColorTileMode::Linear && context.detiler != nullptr, "DB depth copy into a target without a resident image is not implemented");
const auto resident = CachedStorageSurface(context, SurfaceForTarget(pass.color));
CopyDepthSample(context, pass.depthAddress, pass.samples, pass.sample, *resident, resident->StorageView(0, false), pass.extent);
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> PipelinedColorMetadataPassWrites(const ColorMetadataPass& pass) {
std::vector<std::pair<std::uint64_t, std::uint64_t>> writes;
if (pass.mode == ColorMetadataPass::Mode::Resolve) {
@@ -1365,7 +1365,7 @@ ImportRun joinedRun(const GuestAllocations::Lease& lease, std::uint64_t begin, s
auto run = crossedRun(lease, begin, end);
if (run.ranges.empty()) return run;
if (std::all_of(run.ranges.begin(), run.ranges.end(), [window](const auto& range) { return range->bytes > window; })) return run;
if (begin == run.base && end == run.base + run.bytes) return run;
if (begin == run.base) return run;
return {};
}
@@ -1531,16 +1531,24 @@ std::atomic<std::uint64_t> importMaxBytes{0};
// The import of an access to a registered range larger than the range window: the cells the access touches, grown to
// the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of
// replacing each other's.
ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) {
std::optional<std::pair<std::uint64_t, std::uint64_t>> importWindowBounds(const GuestAllocations::Range& range, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) {
const auto window = rangeImportWindow.load(std::memory_order_relaxed);
if (run.ranges.size() != 1 || window == 0 || alignment == 0 || window % alignment != 0) return run;
const auto& range = *run.ranges.front();
if (range.bytes <= window || begin < range.address || end > range.address + range.bytes || end <= begin) return run;
if (window == 0 || alignment == 0 || window % alignment != 0 || range.bytes <= window || begin < range.address || end > range.address + range.bytes || end <= begin) return std::nullopt;
const auto lastCell = range.bytes / window - 1;
const auto cell = [&](std::uint64_t address) { return std::min((address - range.address) / window, lastCell); };
auto windowBegin = range.address + cell(begin) * window;
const auto windowBegin = range.address + cell(begin) * window;
const auto endCell = cell(end - 1);
auto windowEnd = endCell == lastCell ? range.address + range.bytes : range.address + (endCell + 1) * window;
const auto windowEnd = endCell == lastCell ? range.address + range.bytes : range.address + (endCell + 1) * window;
if (windowBegin % alignment != 0 || (windowEnd - windowBegin) % alignment != 0) return std::nullopt;
return std::pair{windowBegin, windowEnd};
}
ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin, std::uint64_t end, std::uint64_t alignment) {
if (run.ranges.size() != 1) return run;
const auto& range = *run.ranges.front();
const auto bounds = importWindowBounds(range, begin, end, alignment);
if (!bounds) return run;
auto [windowBegin, windowEnd] = *bounds;
for (bool grown = true; grown;) {
grown = false;
auto it = state.imports.upper_bound(windowBegin);
@@ -1615,7 +1623,10 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
}
}
const auto budget = evict ? 0 : effectiveBudget(context, state);
if (const auto index = leasedIndexContaining(lease, address); budget != 0 && index != lease.size() && stillRefused(state, rememberedRun(state.refusals, lease, index, index), budget) && findImport(state, address, address + 1) == nullptr) {
const auto index = leasedIndexContaining(lease, address);
const auto windowBounds = index != lease.size() && end != 0 ? importWindowBounds(*lease[index], address, end, context.hostImportAlignment) : std::nullopt;
const bool complete = !windowBounds || (windowBounds->first == lease[index]->address && windowBounds->second == lease[index]->address + lease[index]->bytes);
if (budget != 0 && index != lease.size() && complete && stillRefused(state, rememberedRun(state.refusals, lease, index, index), budget) && findImport(state, address, address + 1) == nullptr) {
auto& counts = state.counts[static_cast<std::size_t>(requester)];
++counts.refused;
counts.refusedBytes += lease[index]->bytes;
@@ -1660,6 +1671,40 @@ std::vector<ImportPiece> importPieces(const Context& context, HostImports& state
return pieces;
}
std::vector<ImportPiece> budgetedImportPieces(const Context& context, HostImports& state, const GuestAllocations::Range& range, const GuestAllocations::Lease& lease) {
const auto window = rangeImportWindow.load(std::memory_order_relaxed);
const auto alignment = context.hostImportAlignment;
if (!range.releasable || !GuestMemory::Watched(range.address, range.bytes) || window == 0 || alignment == 0 || window % alignment != 0 || range.address % alignment != 0 || range.bytes <= window) return {};
const auto end = range.address + range.bytes;
std::vector<ImportPiece> pieces;
for (auto cursor = range.address; cursor < end;) {
const HostImport* entry = nullptr;
if (const auto* previous = peekImport(state, cursor, cursor + 1); previous != nullptr && previous->base >= range.address && previous->base + previous->bytes <= end) {
entry = importAllocation(context, state, cursor, lease, ImportRequester::Build, std::min(end, previous->base + previous->bytes), false);
}
if (entry == nullptr) {
const auto budget = effectiveBudget(context, state);
auto room = budget - std::min(budget, state.liveBytes);
if (const auto limit = importMaxBytes.load(std::memory_order_relaxed); limit != 0) room = std::min(room, limit);
if (room < window) break;
auto wanted = cursor + std::min(end - cursor, window);
if (const auto bounds = importWindowBounds(range, cursor, wanted, alignment); bounds && bounds->second - bounds->first > room) {
wanted = range.address + (range.bytes / window - 1) * window;
if (wanted <= cursor) break;
}
entry = importAllocation(context, state, cursor, lease, ImportRequester::Build, wanted, false);
}
if (entry == nullptr || entry->base > cursor) break;
const auto to = std::min(end, entry->base + entry->bytes);
if (to <= cursor) break;
pieces.push_back({cursor, to, entry});
cursor = to;
}
if (pieces.empty()) return {};
if (pieces.back().end != end) pieces.push_back({pieces.back().end, end, nullptr});
return pieces;
}
const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
if (const auto* low = peekImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base;
if (const auto* high = peekImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes;
@@ -2666,7 +2711,13 @@ bool importsNow(const Context& context, HostImports& state, const GuestAllocatio
// 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
std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::shared_ptr<const GuestAllocations::Range>& range, std::vector<RefreshBlock>& blocks, bool heap) {
std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::shared_ptr<const GuestAllocations::Range>& range, std::vector<RefreshBlock>& blocks, bool heap, std::uint64_t begin = 0, std::uint64_t bytes = 0) {
auto selected = *range;
if (bytes != 0) {
Require(begin >= range->address && bytes <= range->bytes && begin - range->address <= range->bytes - bytes, "heap mirror selection exceeds its registered range");
selected.address = begin;
selected.bytes = static_cast<std::size_t>(bytes);
}
auto& state = Mirrors();
std::shared_ptr<ImageMirror> mirror;
{
@@ -2679,28 +2730,28 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
state.device = context.device;
++state.version;
}
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->reserved == range->bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second;
if (state.failed.contains(selected.address)) return nullptr;
const auto found = state.entries.find(selected.address);
if (found != state.entries.end() && sameRange(found->second->range, range) && found->second->reserved == selected.bytes && found->second->writable == range->writable && found->second->heap == heap) mirror = found->second;
}
if (mirror != nullptr) {
if (!mirror->sparse) {
if (!heap && prepareRefresh(*mirror, range->address, range->bytes)) appendBlocks(blocks, *mirror, range->address, range->bytes);
if (!heap && prepareRefresh(*mirror, selected.address, selected.bytes)) appendBlocks(blocks, *mirror, selected.address, selected.bytes);
return mirror;
}
if (refreshSparseMirror(*mirror, blocks)) return mirror;
}
Pieces pieces;
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(range->address), range->bytes, range->writable && !heap)) {
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(selected.address), selected.bytes, range->writable && !heap)) {
if (!heap || !sparseMirrorsEnabled()) return nullptr;
pieces = GuestMemory::DescribeCommitted(range->address, range->bytes).ranges;
pieces = GuestMemory::DescribeCommitted(selected.address, selected.bytes).ranges;
if (pieces.empty()) return nullptr;
}
const bool sparse = !pieces.empty();
mirror = std::make_shared<ImageMirror>();
mirror->base = range->address;
mirror->bytes = sparse ? sparseExtent(range->address, range->bytes, pieces) : range->bytes;
mirror->reserved = range->bytes;
mirror->base = selected.address;
mirror->bytes = sparse ? sparseExtent(selected.address, selected.bytes, pieces) : selected.bytes;
mirror->reserved = selected.bytes;
mirror->sparse = sparse;
mirror->writable = range->writable;
mirror->heap = heap;
@@ -2713,8 +2764,8 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
syncRange(mirror->base, mirror->bytes);
if (GuestMemory::Watched(mirror->base, static_cast<std::size_t>(mirror->bytes))) generation = collectPieces(pieces);
} else {
prepareRefresh(*mirror, range->address, range->bytes);
generation = GuestMemory::CollectWrites(range->address, range->bytes);
prepareRefresh(*mirror, selected.address, selected.bytes);
generation = GuestMemory::CollectWrites(selected.address, selected.bytes);
}
constexpr std::uint64_t block = 65536;
mirror->generations.assign(static_cast<std::size_t>(((mirror->base + mirror->bytes + block - 1) / block) - mirror->base / block), generation);
@@ -2722,7 +2773,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
std::shared_ptr<ImageMirror> stale;
std::lock_guard lock(state.mutex);
++state.heapUnwatched;
if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) {
if (const auto found = state.entries.find(selected.address); found != state.entries.end() && found->second->heap) {
unlistMirror(state, *found->second);
stale = std::move(found->second);
state.entries.erase(found);
@@ -2733,7 +2784,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
mirror->watchedSerial.store(serial, std::memory_order_relaxed);
if (context.sparseBinding && context.sparseResidencyBuffer && context.sparseResidencyAliased) backed = GuestNonzeroBlocks(sparse ? clipPieces(pieces, mirror->base, mirror->base + mirror->bytes) : GuestRuns{{mirror->base, mirror->base + mirror->bytes}});
auto need = blockBytes(backed);
reserveHeapBytes(range->address, range->bytes, need);
reserveHeapBytes(selected.address, selected.bytes, need);
try {
mirror->resident = ResidentBuffer::Create(context, mirror->base, mirror->bytes, backed);
} catch (const std::runtime_error& error) {
@@ -2743,7 +2794,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
state.heapBytes -= need;
held = state.heapBytes;
}
throw std::runtime_error(heapMirrorError(range->address, range->bytes, held, error.what()));
throw std::runtime_error(heapMirrorError(selected.address, selected.bytes, held, error.what()));
}
if (mirror->resident == nullptr) {
{
@@ -2751,7 +2802,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
state.heapBytes -= need;
}
need = mirror->bytes;
reserveHeapBytes(range->address, range->bytes, need);
reserveHeapBytes(selected.address, selected.bytes, need);
}
mirror->accounted = need;
}
@@ -2771,9 +2822,9 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
std::lock_guard lock(state.mutex);
++state.rebuilds;
mirror->serial = (1ull << 63u) | ++state.serials;
if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) unlistMirror(state, *found->second);
if (const auto found = state.entries.find(selected.address); found != state.entries.end() && found->second->heap) unlistMirror(state, *found->second);
mirror->listed = true;
state.entries[range->address] = mirror;
state.entries[selected.address] = mirror;
++state.version;
return mirror;
}
@@ -2787,11 +2838,11 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
state.heapBytes -= mirror->accounted;
held = state.heapBytes;
}
throw std::runtime_error(heapMirrorError(range->address, range->bytes, held, error.what()));
throw std::runtime_error(heapMirrorError(selected.address, selected.bytes, held, 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::fprintf(stderr, "[gpu] image mirror of 0x%llx+0x%llx failed: %s; falling back to copies\n", static_cast<unsigned long long>(selected.address), static_cast<unsigned long long>(selected.bytes), error.what());
std::lock_guard lock(state.mutex);
state.failed.insert(range->address);
state.failed.insert(selected.address);
return nullptr;
}
// Read stores GPU results pending in the range first (write-backs of earlier copies of it);
@@ -2799,7 +2850,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
{
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::MirrorRefresh);
const auto target = mirror->buffer->Bytes().first(static_cast<std::size_t>(mirror->bytes));
if (!sparse) GuestMemory::Read(range->address, target);
if (!sparse) GuestMemory::Read(selected.address, target);
std::size_t cursor = 0;
for (const auto& [first, last] : pieces) {
const auto at = static_cast<std::size_t>(first - mirror->base);
@@ -2815,9 +2866,9 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
++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) unlistMirror(state, *found->second);
if (const auto found = state.entries.find(selected.address); found != state.entries.end() && found->second->heap) unlistMirror(state, *found->second);
mirror->listed = heap;
state.entries[range->address] = mirror;
state.entries[selected.address] = mirror;
++state.version;
return mirror;
}
@@ -2860,20 +2911,28 @@ void sweepMirrors() {
state.pinnedVersion = state.version;
}
void releaseImportedMirrors(const Context& context, const std::vector<std::uint64_t>& imported) {
void releaseImportedMirrors(const Context& context, std::span<const std::pair<std::uint64_t, std::uint64_t>> imported) {
if (imported.empty()) return;
std::vector<std::shared_ptr<ImageMirror>> released;
auto& state = Mirrors();
std::lock_guard lock(state.mutex);
if (state.device != context.device || state.entries.empty()) return;
for (const auto base : imported) {
const auto found = state.entries.find(base);
if (found == state.entries.end() || !found->second->heap) continue;
state.heapImportedBytes += found->second->accounted;
unlistMirror(state, *found->second);
++state.heapImported;
released.push_back(std::move(found->second));
state.entries.erase(found);
for (const auto& [begin, end] : imported) {
auto it = state.entries.upper_bound(begin);
if (it != state.entries.begin()) --it;
while (it != state.entries.end() && it->first < end) {
const auto& mirror = it->second;
if (!mirror->heap || mirror->base + mirror->bytes <= begin) {
++it;
continue;
}
++state.heapImported;
state.heapImportedBytes += mirror->accounted;
unlistMirror(state, *mirror);
released.push_back(std::move(it->second));
it = state.entries.erase(it);
++state.version;
}
}
}
@@ -3671,10 +3730,11 @@ bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped&
} else if (state.refreshedGeneration > generation) {
return true;
}
if (!adopting) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
for (const auto& mapped : ranges) {
const auto range = mapped.lock();
if (range == nullptr || leasedRangeOwner(current, range->address) != range) continue;
if (range == nullptr || range->bytes <= 0x10000) continue;
if (current.empty()) current = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
if (leasedRangeOwner(current, range->address) != range) continue;
try {
if (!GuestMemory::GpuMutex().HeldByThisThread()) {
const auto run = importRun(current, range->address, std::numeric_limits<std::uint64_t>::max(), effectiveBudget(context, state));
@@ -3866,7 +3926,7 @@ void GuestBufferMemory::AcquireRegistered() {
// must not happen under it.
regions.reserve(lease.size());
std::vector<AddressCopy> copies;
std::vector<std::uint64_t> imported;
std::vector<std::pair<std::uint64_t, std::uint64_t>> imported;
const HostImport* covering = nullptr;
std::uint64_t coveredBegin = 0;
std::uint64_t coveredEnd = 0;
@@ -3893,15 +3953,33 @@ void GuestBufferMemory::AcquireRegistered() {
coveredEnd = entry->base + entry->bytes;
}
}
if (importable && mirrorsEnabled() && entry == nullptr && pieces.empty()) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
pieces = budgetedImportPieces(context, state, *range, lease);
}
lap(timing.importsUs, at);
if (!pieces.empty()) {
const auto firstImported = imported.size();
for (const auto& piece : pieces) if (piece.entry != nullptr) imported.emplace_back(piece.begin, piece.end);
releaseImportedMirrors(context, std::span(imported).subspan(firstImported));
for (const auto& piece : pieces) {
Region part{piece.begin, piece.end, range->writable, {}, nullptr};
part.hostBacked = true;
part.direct = piece.entry;
if (piece.entry != nullptr) {
part.hostBacked = true;
part.direct = piece.entry;
} else {
part.mirror = acquireMirror(context, range, blocks, true, piece.begin, piece.end - piece.begin);
lap(timing.mirrorsUs, at);
Require(part.mirror != nullptr, "a partial heap import has no mirror for its remaining bytes");
mirrored = true;
if (part.mirror->sparse) {
part.sparse = true;
part.backed = part.mirror->committed;
} else heaps.push_back(part.mirror.get());
}
regions.push_back(std::move(part));
}
imported.push_back(range->address);
continue;
}
if (entry != nullptr) {
@@ -3909,7 +3987,7 @@ void GuestBufferMemory::AcquireRegistered() {
// Reused by Upload while no import was dropped since (see importsEpoch).
region.direct = entry;
regions.push_back(std::move(region));
imported.push_back(range->address);
imported.emplace_back(range->address, range->address + range->bytes);
continue;
}
if (mirrorsEnabled()) {
@@ -228,7 +228,7 @@ public:
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VkImageViewUsageCreateInfo viewUsage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
viewUsage.usage = imageUsage & ~VK_IMAGE_USAGE_STORAGE_BIT;
if (orderFormat != VK_FORMAT_UNDEFINED) viewInfo.pNext = &viewUsage;
if ((imageUsage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) viewInfo.pNext = &viewUsage;
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created.view), "vkCreateImageView multisampled target");
} catch (...) {
destroy(created);
@@ -660,6 +660,7 @@ struct GuardedRange {
std::uint32_t samples;
std::shared_ptr<std::atomic<bool>> active;
std::shared_ptr<std::atomic<bool>> metadataValid;
bool fmask = false;
};
class PrimedSharedMutex : public std::shared_mutex {
@@ -1152,6 +1153,12 @@ VkComponentMapping swapRedBlue(VkComponentMapping components) {
return {swapped(components.r, VK_COMPONENT_SWIZZLE_R), swapped(components.g, VK_COMPONENT_SWIZZLE_G), swapped(components.b, VK_COMPONENT_SWIZZLE_B), swapped(components.a, VK_COMPONENT_SWIZZLE_A)};
}
bool sameStorageForm(const Context& context, VkFormat rendered, VkFormat requested) {
if (rendered == requested) return true;
const auto storage = HostStorageFormat(context, rendered);
return storage != VK_FORMAT_UNDEFINED && storage == HostStorageFormat(context, requested);
}
}
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) {
@@ -1214,7 +1221,7 @@ VkImageView MultisampleTargetStorageView(const Context& context, const GuestText
if (!target->active->load(std::memory_order_acquire)) continue;
if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue;
const auto& rendered = target->target;
if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format));
if (sameStorageForm(context, rendered.format, format) && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format));
other = target.get();
}
if (other == nullptr) {
@@ -1315,6 +1322,17 @@ void ClearMultisampleTargets(VkDevice device) {
UpdateResidentImageUnmapObserver_nid_no_patch();
}
void GuardOneFragmentFmask(const Context& context, const ColorTarget& target) {
const auto bits = static_cast<std::uint64_t>(target.extent.width) * target.extent.height * target.coverageSamples;
const auto bytes = std::max<std::uint64_t>((bits / 8u + 0xffffu) & ~0xffffull, 0x10000u);
std::unique_lock lock(guardedMutex());
const bool known = std::ranges::any_of(guarded(), [&](const auto& range) { return range.fmask && range.device == context.device && range.address == target.fmaskAddress && range.bytes == bytes; });
if (!known) guarded().push_back({context.device, target.fmaskAddress, bytes, target.coverageSamples, std::make_shared<std::atomic<bool>>(true), nullptr, true});
anyGuarded().store(true, std::memory_order_release);
lock.unlock();
InstallResidentImageReadGuard_nid_no_patch();
}
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) {
RequireNoResidentDepthRead_nid_no_patch(address, bytes);
RequireNoMultisampleColorTarget(address, bytes, access);
@@ -1326,6 +1344,10 @@ void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, c
for (const auto& range : guarded()) {
if (address >= range.address + range.bytes || range.address >= address + bytes) continue;
char text[256];
if (range.fmask) {
std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps the FMASK 0x%llx+0x%llx of a one-fragment color target of %u samples drawn on the host, which does not write it: guest access to it is not implemented", access, static_cast<unsigned long long>(address), bytes, static_cast<unsigned long long>(range.address), static_cast<unsigned long long>(range.bytes), range.samples);
throw std::runtime_error(text);
}
std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast<unsigned long long>(address), bytes, static_cast<unsigned long long>(range.address), static_cast<unsigned long long>(range.bytes), range.samples);
throw std::runtime_error(text);
}
@@ -20,6 +20,7 @@
#include <cstdlib>
#include <list>
#include <mutex>
#include <spirv/unified1/spirv.hpp>
#include <string_view>
#include <type_traits>
#include <unordered_map>
@@ -27,6 +28,34 @@
namespace AgcDriver::Graphics {
namespace {
bool LogicOpFormat(VkFormat format) {
switch (format) {
case VK_FORMAT_R8_UNORM:
case VK_FORMAT_R8G8_UNORM:
case VK_FORMAT_R8G8B8A8_UNORM:
case VK_FORMAT_B8G8R8A8_UNORM:
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
case VK_FORMAT_A2R10G10B10_UNORM_PACK32:
case VK_FORMAT_R16_UNORM:
case VK_FORMAT_R16G16_UNORM:
case VK_FORMAT_R16G16B16A16_UNORM:
case VK_FORMAT_R8_UINT:
case VK_FORMAT_R8G8_UINT:
case VK_FORMAT_R8G8B8A8_UINT:
case VK_FORMAT_R16_UINT:
case VK_FORMAT_R16G16_UINT:
case VK_FORMAT_R16G16B16A16_UINT:
case VK_FORMAT_R32_UINT:
case VK_FORMAT_R32G32_UINT:
case VK_FORMAT_R32G32B32A32_UINT: return true;
default: return false;
}
}
}
void LogPipelineStatistics_nid_no_patch(const Context& context, VkPipeline pipeline) {
if (!context.pipelineExecutableInfo) return;
const auto getProperties = context.Function<PFN_vkGetPipelineExecutablePropertiesKHR>("vkGetPipelineExecutablePropertiesKHR");
@@ -105,6 +134,18 @@ bool BarycentricPatches(const State& state, std::span<const CompiledShader> shad
return !state.rectList && state.stages.path == ShaderPath::Vertex && std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::TessellationControl; });
}
bool FragmentInterlock(std::span<const CompiledShader> shaders) {
return std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) {
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) return false;
const auto words = shader.program->spirv.Words();
for (std::size_t cursor = 5; cursor + 1 < words.size() && (words[cursor] & 0xffffu) == spv::OpCapability && (words[cursor] >> 16u) == 2u; cursor += 2) {
const auto capability = words[cursor + 1];
if (capability == spv::CapabilityFragmentShaderPixelInterlockEXT || capability == spv::CapabilityFragmentShaderSampleInterlockEXT || capability == spv::CapabilityFragmentShaderShadingRateInterlockEXT) return true;
}
return false;
});
}
void ValidateProvokingVertex(const Context& context, const State& state, std::span<const CompiledShader> shaders) {
Require(state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT || state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, "invalid provoking vertex mode");
if (state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
@@ -280,7 +321,7 @@ void appendKey(std::vector<std::byte>& key, const TValue& value) {
bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders) {
using Stage = ShaderRecompiler::ShaderStage;
if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state) || std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; })) return false;
if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state) || std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; }) || FragmentInterlock(shaders)) return false;
keys = {};
for (const auto& shader : shaders) {
const auto identity = shader.program->Identity();
@@ -350,6 +391,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State&
}
}
for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend);
appendKey(keys.fragmentOutput, state.colorRop);
for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value);
appendKey(keys.renderPass, state.blends.size());
for (const auto& color : state.colors) {
@@ -416,6 +458,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping");
Require(!state.dualSourceBlend || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "device does not support dual-source blending into this many attachments");
Require(state.reversedBlend == 0 || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "blending into a color target with a reversed component order through a second source color needs dual-source blending, which the device does not support");
Require(LogicOpFromRop(state.colorRop) == VK_LOGIC_OP_COPY || context.logicOp, "device does not support color logic operations");
Require(LogicOpFromRop(state.colorRop) == VK_LOGIC_OP_COPY || std::ranges::all_of(state.colors, [](const ColorTarget& color) { return LogicOpFormat(color.format); }), "color logic operations on a color target that is not a normalized or integer format are not implemented");
Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled");
Require(!state.perSampleShading || context.sampleRateShading, "per-sample pixel shading requires the sampleRateShading device feature");
Require(!state.mergedCoverage || (context.mergedCoverageSampleCounts & state.samples) != 0, "an overrasterized draw (a multisampled rasterizer into one-sample color targets) requires VK_NV_framebuffer_mixed_samples with merged coverage reduction");
@@ -639,6 +683,10 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
secondChannels[index].colorBlendOp = state.blends[index].alphaBlendOp;
}
}
if (const auto logicOp = LogicOpFromRop(state.colorRop); logicOp != VK_LOGIC_OP_COPY) {
blend.logicOpEnable = VK_TRUE;
blend.logicOp = logicOp;
}
blend.attachmentCount = static_cast<std::uint32_t>(firstChannels.size());
blend.pAttachments = firstChannels.empty() ? nullptr : firstChannels.data();
std::copy(state.blendConstants.begin(), state.blendConstants.end(), blend.blendConstants);
@@ -982,6 +1030,7 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, state.conservativeRasterization);
append(key, state.blends.size());
for (const auto& blend : state.blends) append(key, blend);
append(key, state.colorRop);
for (const auto value : state.blendConstants) append(key, value);
append(key, state.samples);
append(key, state.perSampleShading);
@@ -197,7 +197,7 @@ void PipelineCache::save(bool final, bool waitForPipelines) {
settledBytes = liveBytes;
settledFile = file;
};
const auto disk = fileUnchanged ? File{} : readFile();
auto disk = fileUnchanged ? File{} : readFile();
if (disk.state == FileState::Unreadable) {
std::fprintf(stderr, "[pipeline-cache] cannot read %s; not saving over it\n", path.string().c_str());
settle(readHeader());
@@ -206,12 +206,16 @@ void PipelineCache::save(bool final, bool waitForPipelines) {
if (disk.state == FileState::Invalid) std::fprintf(stderr, "[pipeline-cache] replacing %s: %s\n", path.string().c_str(), disk.invalidReason);
std::size_t floor = fileUnchanged ? std::max<std::size_t>(loadedBytes, settledFile->dataBytes) : 0;
VkPipelineCache merged = VK_NULL_HANDLE;
bool mergedWithFile = false;
struct DestroyMerged {
const Context& context;
const VkPipelineCache& cache;
~DestroyMerged() {
if (cache != VK_NULL_HANDLE) context.Function<PFN_vkDestroyPipelineCache>("vkDestroyPipelineCache")(context.device, cache, nullptr);
VkPipelineCache& cache;
void Reset() {
if (cache == VK_NULL_HANDLE) return;
context.Function<PFN_vkDestroyPipelineCache>("vkDestroyPipelineCache")(context.device, cache, nullptr);
cache = VK_NULL_HANDLE;
}
~DestroyMerged() { Reset(); }
} destroyMerged{context, merged};
if (disk.state == FileState::Valid) {
const auto data = disk.Payload();
@@ -221,11 +225,13 @@ void PipelineCache::save(bool final, bool waitForPipelines) {
const auto result = context.Function<PFN_vkCreatePipelineCache>("vkCreatePipelineCache")(context.device, &info, nullptr, &merged);
if (result == VK_SUCCESS) {
floor = std::max(loadedBytes, data.size());
mergedWithFile = true;
} else {
merged = VK_NULL_HANDLE;
std::fprintf(stderr, "[pipeline-cache] replacing %s: the driver refused it (Vulkan result %d)\n", path.string().c_str(), static_cast<int>(result));
}
}
std::vector<std::byte>().swap(disk.contents);
if (merged != VK_NULL_HANDLE) {
auto synchronizationLock = snapshot();
if (!synchronizationLock.owns_lock()) return;
@@ -251,6 +257,7 @@ void PipelineCache::save(bool final, bool waitForPipelines) {
if (result != VK_SUCCESS && result != VK_INCOMPLETE) return;
file.resize(sizeof(FileHeader) + size);
}
destroyMerged.Reset();
if (size < floor) {
std::fprintf(stderr, "[pipeline-cache] keeping %s: the merged cache has %.1f KiB, less than the %.1f KiB on disk or loaded at start\n", path.string().c_str(), static_cast<double>(size) / 1024.0, static_cast<double>(floor) / 1024.0);
settle(fileUnchanged ? *settledFile : disk.header);
@@ -263,7 +270,7 @@ void PipelineCache::save(bool final, bool waitForPipelines) {
return;
}
settle(header);
if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s%s\n", static_cast<double>(size) / 1024.0, path.string().c_str(), std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count(), merged != VK_NULL_HANDLE ? " (merged with the file)" : "", final ? " (teardown)" : "");
if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s%s\n", static_cast<double>(size) / 1024.0, path.string().c_str(), std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count(), mergedWithFile ? " (merged with the file)" : "", final ? " (teardown)" : "");
}
void PipelineCache::SaveNow() {
@@ -1,11 +1,13 @@
#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libc/include/HostThreadLocal.hpp"
#include "ShaderCacheDirectory.hpp"
#include "ShaderDiskCache.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <condition_variable>
#include <cstdlib>
@@ -23,6 +25,7 @@
#else
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
@@ -35,7 +38,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update
static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec");
static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec");
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(State) == 824, "State changed: update CanonicalPipelineState and the recipe codec");
static_assert(sizeof(State) == 832, "State changed: update CanonicalPipelineState and the recipe codec");
static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec");
static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec");
static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec");
@@ -44,7 +47,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel
constexpr std::uint32_t FileMagic = 0x4c4a5041u;
// Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the
// layout guards above do not catch (a field renumbered or reinterpreted at the same size).
constexpr std::uint32_t FileFormat = 9u;
constexpr std::uint32_t FileFormat = 10u;
constexpr std::uint32_t ModuleRecord = 1u;
constexpr std::uint32_t GraphicsRecord = 2u;
constexpr std::uint32_t ComputeRecord = 3u;
@@ -203,6 +206,7 @@ void encodeState(Writer& writer, const State& state) {
writer.Bool(state.rectList);
writer.Bool(state.dualSourceBlend);
writer.U32(state.reversedBlend);
writer.U32(state.colorRop);
writer.U32(static_cast<std::uint32_t>(state.topology));
writer.Bool(state.primitiveRestart);
writer.Bool(state.negativeOneToOne);
@@ -270,6 +274,7 @@ void decodeState(Reader& reader, State& state) {
state.rectList = reader.Bool();
state.dualSourceBlend = reader.Bool();
state.reversedBlend = reader.U32();
state.colorRop = static_cast<std::uint8_t>(reader.U32());
state.topology = static_cast<VkPrimitiveTopology>(reader.U32());
state.primitiveRestart = reader.Bool();
state.negativeOneToOne = reader.Bool();
@@ -339,6 +344,10 @@ std::uint64_t padded(std::uint64_t bytes) {
return (bytes + RecordAlignment - 1u) / RecordAlignment * RecordAlignment;
}
std::uint64_t recordBytes(std::uint64_t payloadBytes) {
return padded(sizeof(RecordHeader) + payloadBytes);
}
bool profiling() {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
return profile;
@@ -359,6 +368,16 @@ std::uint64_t configured(const char* name, std::uint64_t fallback) {
// second run of the same install) takes the same lock file, so its appends and this run's never
// interleave and neither cuts a record the other is writing. Taken before the journal's mutex,
// never while holding it.
bool replaceFile(const std::filesystem::path& temporary, const std::filesystem::path& target) {
#ifdef _WIN32
return ::MoveFileExW(temporary.c_str(), target.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
#else
std::error_code error;
std::filesystem::rename(temporary, target, error);
return !error;
#endif
}
class FileLock {
public:
explicit FileLock(const std::filesystem::path& path) {
@@ -409,13 +428,100 @@ private:
#endif
};
class JournalFile {
public:
explicit JournalFile(const std::filesystem::path& path) {
#ifdef _WIN32
handle = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, nullptr);
LARGE_INTEGER length{};
if (handle != INVALID_HANDLE_VALUE && GetFileSizeEx(handle, &length) && length.QuadPart >= 0) {
size = static_cast<std::uint64_t>(length.QuadPart);
} else if (handle != INVALID_HANDLE_VALUE) {
CloseHandle(handle);
handle = INVALID_HANDLE_VALUE;
}
#else
descriptor = ::open(path.c_str(), O_RDONLY | O_CLOEXEC);
struct stat status{};
if (descriptor >= 0 && ::fstat(descriptor, &status) == 0 && status.st_size >= 0) {
size = static_cast<std::uint64_t>(status.st_size);
} else if (descriptor >= 0) {
::close(descriptor);
descriptor = -1;
}
#endif
}
~JournalFile() {
#ifdef _WIN32
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
#else
if (descriptor >= 0) ::close(descriptor);
#endif
}
JournalFile(const JournalFile&) = delete;
JournalFile& operator=(const JournalFile&) = delete;
#ifdef _WIN32
[[nodiscard]] bool Open() const { return handle != INVALID_HANDLE_VALUE; }
#else
[[nodiscard]] bool Open() const { return descriptor >= 0; }
#endif
[[nodiscard]] std::uint64_t Size() const { return size; }
bool Read(std::uint64_t offset, std::span<std::byte> bytes) const {
for (std::size_t done = 0; done < bytes.size();) {
const auto position = offset + done;
#ifdef _WIN32
struct ReadEvent {
HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
~ReadEvent() {
if (event != nullptr) CloseHandle(event);
}
};
auto& completion = HostThreadLocal<ReadEvent, ReadEvent>();
if (completion.event == nullptr) return false;
OVERLAPPED at{};
at.Offset = static_cast<DWORD>(position);
at.OffsetHigh = static_cast<DWORD>(position >> 32u);
at.hEvent = completion.event;
DWORD count = 0;
if (!ReadFile(handle, bytes.data() + done, static_cast<DWORD>(std::min<std::size_t>(bytes.size() - done, MAXDWORD)), nullptr, &at) && GetLastError() != ERROR_IO_PENDING) return false;
if (!GetOverlappedResult(handle, &at, &count, TRUE) || count == 0) return false;
#else
const auto count = ::pread(descriptor, bytes.data() + done, bytes.size() - done, static_cast<off_t>(position));
if (count < 0 && errno == EINTR) continue;
if (count <= 0) return false;
#endif
done += static_cast<std::size_t>(count);
}
return true;
}
template<typename TValue>
bool ReadValue(std::uint64_t offset, TValue& value) const requires std::is_trivially_copyable_v<TValue> {
return Read(offset, std::as_writable_bytes(std::span(&value, 1)));
}
private:
#ifdef _WIN32
HANDLE handle = INVALID_HANDLE_VALUE;
#else
int descriptor = -1;
#endif
std::uint64_t size = 0;
};
// The end of the last whole record in [from, size) of the file: records another process appended
// after this one loaded the file are kept, the torn tail of a record a crashed run left is cut.
std::uint64_t validTail(const std::filesystem::path& path, std::uint64_t from, std::uint64_t size);
struct LoadedModule {
ShaderRecompiler::ShaderStage stage;
std::shared_ptr<const ShaderRecompiler::RecompileResult> result;
std::uint64_t payloadOffset = 0;
std::uint64_t payloadBytes = 0;
std::uint64_t payloadHash = 0;
};
struct LoadedEntry {
@@ -426,6 +532,7 @@ struct LoadedEntry {
};
struct LoadedJournal {
std::shared_ptr<const JournalFile> file;
std::map<ShaderRecompiler::PipelineIdentity, LoadedModule> modules;
std::vector<LoadedEntry> entries;
std::set<ShaderRecompiler::PipelineIdentity> indexModules;
@@ -436,6 +543,178 @@ struct LoadedJournal {
bool headerValid = false;
};
struct RecordSpan {
std::uint64_t offset = 0;
std::uint64_t bytes = 0;
std::uint32_t kind = 0;
std::uint64_t hash = 0;
ShaderRecompiler::PipelineIdentity identity{};
std::vector<ShaderRecompiler::PipelineIdentity> needs;
bool decodable = false;
};
class PayloadHash {
public:
explicit PayloadHash(std::uint64_t bytes) : hash(bytes * Multiplier) {}
void Update(std::span<const std::byte> bytes) {
while (!bytes.empty()) {
const auto take = std::min(bytes.size(), carry.size() - carryBytes);
std::memcpy(carry.data() + carryBytes, bytes.data(), take);
carryBytes += take;
bytes = bytes.subspan(take);
if (carryBytes == carry.size()) {
std::uint64_t chunk = 0;
std::memcpy(&chunk, carry.data(), sizeof(chunk));
Mix(chunk);
carryBytes = 0;
}
}
}
[[nodiscard]] std::uint64_t Finish() {
if (carryBytes != 0) {
std::uint64_t chunk = 0;
std::memcpy(&chunk, carry.data(), carryBytes);
Mix(chunk ^ 0x8000000000000000ull);
carryBytes = 0;
}
hash ^= hash >> 33u;
hash *= 0xff51afd7ed558ccdull;
hash ^= hash >> 33u;
hash *= 0xc4ceb9fe1a85ec53ull;
hash ^= hash >> 33u;
return hash;
}
private:
static constexpr std::uint64_t Multiplier = 0x9e3779b97f4a7c15ull;
void Mix(std::uint64_t chunk) {
chunk *= 0xff51afd7ed558ccdull;
chunk ^= chunk >> 32u;
hash = (hash ^ chunk) * Multiplier;
hash = (hash << 27u) | (hash >> 37u);
}
std::uint64_t hash;
std::array<std::byte, 8> carry{};
std::size_t carryBytes = 0;
};
struct SpanScan {
bool headerValid = false;
std::vector<RecordSpan> spans;
};
bool readSpanPayload(std::ifstream& file, const RecordHeader& record, std::array<std::byte, 20>& prefix, std::vector<std::byte>& graphics, std::uint64_t& hash) {
constexpr std::uint64_t MaxGraphicsBytes = 16u * 1024u * 1024u;
PayloadHash hasher(record.payloadBytes);
std::array<std::byte, 64u * 1024u> chunk{};
const bool capture = record.kind == GraphicsRecord && record.payloadBytes <= MaxGraphicsBytes;
if (capture) graphics.resize(static_cast<std::size_t>(record.payloadBytes));
std::size_t copied = 0;
std::uint64_t left = record.payloadBytes;
while (left != 0) {
const auto take = static_cast<std::size_t>(std::min<std::uint64_t>(left, chunk.size()));
file.read(reinterpret_cast<char*>(chunk.data()), static_cast<std::streamsize>(take));
if (file.gcount() != static_cast<std::streamsize>(take)) return false;
const auto bytes = std::span<const std::byte>(chunk.data(), take);
hasher.Update(bytes);
const auto prefixBytes = std::min(take, prefix.size() - copied);
std::memcpy(prefix.data() + copied, bytes.data(), prefixBytes);
if (prefixBytes < take) copied = prefix.size();
else copied += take;
if (capture) std::memcpy(graphics.data() + (static_cast<std::size_t>(record.payloadBytes) - static_cast<std::size_t>(left)), bytes.data(), take);
left -= take;
}
hash = hasher.Finish();
return true;
}
bool scanSpans(const std::filesystem::path& path, std::uint64_t device, SpanScan& scan) {
std::error_code error;
const auto fileSize = std::filesystem::file_size(path, error);
if (error) return false;
std::ifstream file(path, std::ios::binary);
if (!file) return false;
FileHeader header{};
file.read(reinterpret_cast<char*>(&header), sizeof(header));
if (file.gcount() != sizeof(header)) return false;
if (header.magic != FileMagic || header.format != FileFormat || header.runtimeAbi != ShaderRecompiler::RuntimeAbi::Version ||
header.sourceVersion != ShaderRecompiler::ShaderDiskCache::SourceVersion() || header.device != device) return false;
scan.headerValid = true;
auto cursor = padded(sizeof(FileHeader));
while (cursor + sizeof(RecordHeader) <= fileSize) {
file.clear();
file.seekg(static_cast<std::streamoff>(cursor), std::ios::beg);
RecordHeader record{};
file.read(reinterpret_cast<char*>(&record), sizeof(record));
if (file.gcount() != sizeof(record) || record.kind == 0 || record.kind > ComputeRecord) break;
const auto payloadOffset = cursor + sizeof(RecordHeader);
if (record.payloadBytes > fileSize - payloadOffset) break;
const auto end = padded(payloadOffset + record.payloadBytes);
if (end > fileSize) break;
std::array<std::byte, 20> prefix{};
std::vector<std::byte> graphics;
std::uint64_t hash = 0;
if (!readSpanPayload(file, record, prefix, graphics, hash) || hash != record.payloadHash) break;
RecordSpan span;
span.offset = cursor;
span.bytes = end - cursor;
span.kind = record.kind;
span.hash = record.payloadHash;
if (record.kind == ModuleRecord) {
if (record.payloadBytes < 20u) break;
std::memcpy(&span.identity.low, prefix.data(), sizeof(span.identity.low));
std::memcpy(&span.identity.high, prefix.data() + 8, sizeof(span.identity.high));
} else if (record.kind == ComputeRecord) {
if (record.payloadBytes >= 16u) {
std::memcpy(&span.identity.low, prefix.data(), sizeof(span.identity.low));
std::memcpy(&span.identity.high, prefix.data() + 8, sizeof(span.identity.high));
span.needs.push_back(span.identity);
span.decodable = true;
}
} else {
GraphicsPipelineRecipe recipe;
if (!graphics.empty() && DecodeGraphicsPipelineRecipe(graphics, recipe)) {
for (const auto& stage : recipe.stages) span.needs.push_back(stage.identity);
span.decodable = true;
}
}
scan.spans.push_back(std::move(span));
cursor = end;
}
return true;
}
bool copySpans(const std::filesystem::path& path, const SpanScan& scan, const std::set<std::size_t>& kept, const std::filesystem::path& temporary) {
std::ifstream source(path, std::ios::binary);
if (!source) return false;
std::ofstream out(temporary, std::ios::binary | std::ios::trunc);
if (!out) return false;
std::array<std::byte, 64u * 1024u> chunk{};
source.read(reinterpret_cast<char*>(chunk.data()), static_cast<std::streamsize>(padded(sizeof(FileHeader))));
if (source.gcount() != static_cast<std::streamsize>(padded(sizeof(FileHeader)))) return false;
out.write(reinterpret_cast<const char*>(chunk.data()), static_cast<std::streamsize>(padded(sizeof(FileHeader))));
for (std::size_t position = 0; position < scan.spans.size(); ++position) {
if (!kept.contains(position)) continue;
const auto& span = scan.spans[position];
source.clear();
source.seekg(static_cast<std::streamoff>(span.offset), std::ios::beg);
auto left = span.bytes;
while (left != 0) {
const auto take = static_cast<std::size_t>(std::min<std::uint64_t>(left, chunk.size()));
source.read(reinterpret_cast<char*>(chunk.data()), static_cast<std::streamsize>(take));
if (source.gcount() != static_cast<std::streamsize>(take)) return false;
out.write(reinterpret_cast<const char*>(chunk.data()), static_cast<std::streamsize>(take));
left -= take;
}
}
out.flush();
return !out.fail();
}
class Journal {
public:
static Journal& Instance() {
@@ -464,20 +743,19 @@ public:
return false;
}
// Reads the whole file (modules keep views into the buffer, which lives as long as they do),
// fills the dedupe index, and remembers how many bytes parsed so a torn tail is cut before the
// next append.
// Parses the file a record at a time and keeps it open for the replay, fills the dedupe index,
// and remembers how many bytes parsed so a torn tail is cut before the next append.
LoadedJournal Load() {
LoadedJournal loaded;
if (!Enabled() || !bound.load(std::memory_order_acquire)) return loaded;
const auto started = std::chrono::steady_clock::now();
auto file = std::make_shared<std::vector<std::byte>>();
bool read = false;
{
FileLock fileLock(lockPath);
read = ShaderRecompiler::ReadWholeFile(path, *file);
auto file = std::make_shared<const JournalFile>(path);
read = file->Open() && parse(*file, loaded, true);
if (read) loaded.file = std::move(file);
}
if (read) parse(*file, file, loaded, true);
const auto ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
{
std::lock_guard lock(mutex);
@@ -543,12 +821,15 @@ public:
totals.recordedModules = counters.recordedModules.load(std::memory_order_relaxed);
totals.writtenBytes = counters.writtenBytes.load(std::memory_order_relaxed);
totals.writeFailures = counters.writeFailures.load(std::memory_order_relaxed);
totals.evictedRecords = counters.evictedRecords.load(std::memory_order_relaxed);
totals.evictedBytes = counters.evictedBytes.load(std::memory_order_relaxed);
return totals;
}
struct Counters {
std::atomic<std::uint64_t> loadedRecipes{0}, loadedModules{0}, loadedBytes{0}, droppedRecords{0};
std::atomic<std::uint64_t> recordedGraphics{0}, recordedCompute{0}, recordedModules{0}, writtenBytes{0}, writeFailures{0};
std::atomic<std::uint64_t> evictedRecords{0}, evictedBytes{0};
};
private:
@@ -594,27 +875,30 @@ private:
// Parses the file: with `loaded` set every record is decoded, otherwise only the index (the
// module identities and record hashes) is built. Stops at the first record that does not
// parse; the bytes before it stay valid.
void parse(const std::vector<std::byte>& file, const std::shared_ptr<const std::vector<std::byte>>& owner, LoadedJournal& loaded, bool decode) {
bool parse(const JournalFile& file, LoadedJournal& loaded, bool decode, std::vector<RecordSpan>* spans = nullptr) {
std::uint64_t valid = 0;
bool header = false;
std::uint64_t dropped = 0;
const auto bytes = std::span<const std::byte>(file);
if (bytes.size() >= sizeof(FileHeader)) {
const auto size = file.Size();
if (size >= sizeof(FileHeader)) {
FileHeader fileHeader;
std::memcpy(&fileHeader, bytes.data(), sizeof(fileHeader));
if (!file.ReadValue(0, fileHeader)) return false;
header = fileHeader.magic == FileMagic && fileHeader.format == FileFormat && fileHeader.runtimeAbi == ShaderRecompiler::RuntimeAbi::Version && fileHeader.sourceVersion == ShaderRecompiler::ShaderDiskCache::SourceVersion() && fileHeader.device == device.load(std::memory_order_relaxed);
}
if (header) {
valid = padded(sizeof(FileHeader));
std::uint64_t cursor = valid;
while (cursor + sizeof(RecordHeader) <= bytes.size()) {
std::vector<std::byte> bytes;
while (cursor + sizeof(RecordHeader) <= size) {
RecordHeader record;
std::memcpy(&record, bytes.data() + cursor, sizeof(record));
if (!file.ReadValue(cursor, record)) return false;
const auto payloadOffset = cursor + sizeof(RecordHeader);
if (record.payloadBytes > bytes.size() - payloadOffset) break;
if (record.payloadBytes > size - payloadOffset) break;
const auto end = padded(payloadOffset + record.payloadBytes);
if (end > bytes.size()) break;
const auto payload = bytes.subspan(static_cast<std::size_t>(payloadOffset), static_cast<std::size_t>(record.payloadBytes));
if (end > size) break;
bytes.resize(static_cast<std::size_t>(record.payloadBytes));
if (!file.Read(payloadOffset, bytes)) return false;
const auto payload = std::span<const std::byte>(bytes);
if (ShaderRecompiler::HashBytes(payload) != record.payloadHash) break;
if (record.kind == ModuleRecord) {
if (payload.size() < 20u) break;
@@ -625,15 +909,24 @@ private:
std::memcpy(&stage, payload.data() + 16, sizeof(stage));
bool decoded = !decode;
if (decode && !loaded.indexModules.contains(identity)) {
auto result = std::make_shared<ShaderRecompiler::RecompileResult>();
decoded = ShaderRecompiler::ShaderDiskCache::DecodeResult(payload.subspan(20), *result, owner) && result->Identity() == identity;
if (decoded) loaded.modules.emplace(identity, LoadedModule{static_cast<ShaderRecompiler::ShaderStage>(stage), std::move(result)});
ShaderRecompiler::RecompileResult result;
decoded = ShaderRecompiler::ShaderDiskCache::DecodeResult(payload.subspan(20), result) && result.Identity() == identity;
if (decoded) loaded.modules.emplace(identity, LoadedModule{static_cast<ShaderRecompiler::ShaderStage>(stage), payloadOffset, record.payloadBytes, record.payloadHash});
else ++dropped;
}
if (decoded) loaded.indexModules.insert(identity);
if (spans != nullptr) {
RecordSpan span;
span.offset = cursor;
span.bytes = end - cursor;
span.kind = record.kind;
span.hash = record.payloadHash;
span.identity = identity;
spans->push_back(std::move(span));
}
} else if (record.kind == GraphicsRecord || record.kind == ComputeRecord) {
if (decode && !loaded.indexRecords.contains(record.payloadHash)) {
LoadedEntry entry;
LoadedEntry entry{};
entry.kind = record.kind;
bool decoded = false;
if (record.kind == GraphicsRecord) {
@@ -650,6 +943,28 @@ private:
else ++dropped;
}
loaded.indexRecords.insert(record.payloadHash);
if (spans != nullptr) {
RecordSpan span;
span.offset = cursor;
span.bytes = end - cursor;
span.kind = record.kind;
span.hash = record.payloadHash;
if (record.kind == ComputeRecord) {
if (payload.size() >= 16u) {
std::memcpy(&span.identity.low, payload.data(), sizeof(span.identity.low));
std::memcpy(&span.identity.high, payload.data() + 8, sizeof(span.identity.high));
span.needs.push_back(span.identity);
span.decodable = true;
}
} else {
GraphicsPipelineRecipe recipe;
if (DecodeGraphicsPipelineRecipe(payload, recipe)) {
for (const auto& stage : recipe.stages) span.needs.push_back(stage.identity);
span.decodable = true;
}
}
spans->push_back(std::move(span));
}
} else {
break;
}
@@ -657,11 +972,12 @@ private:
cursor = end;
}
}
if (valid < bytes.size()) ++dropped;
if (valid < size) ++dropped;
loaded.validBytes = valid;
loaded.bytes = bytes.size();
loaded.bytes = size;
loaded.dropped = dropped;
loaded.headerValid = header;
return true;
}
// The index the writer dedupes against, replaced from the file: what it holds and where it
@@ -669,12 +985,12 @@ private:
// written until it can be (the next record tries again) and the file is never started over for
// a read failure.
bool indexLocked() {
std::vector<std::byte> file;
LoadedJournal index;
std::error_code error;
const bool exists = std::filesystem::exists(path, error);
if (exists && !ShaderRecompiler::ReadWholeFile(path, file)) return false;
if (exists) parse(file, nullptr, index, false);
if (std::filesystem::exists(path, error)) {
const JournalFile file(path);
if (!file.Open() || !parse(file, index, false)) return false;
}
std::lock_guard lock(mutex);
indexed = true;
validBytes = index.validBytes;
@@ -735,43 +1051,119 @@ private:
// tail of a crashed run (bytes past the last parsed record) was cut.
void write(const Pending& pending) {
if (!ensureIndexed()) throw std::runtime_error("the journal could not be read");
for (unsigned attempt = 0; attempt < 2; ++attempt) {
for (unsigned attempt = 0; attempt < 3; ++attempt) {
if (append(pending)) return;
}
throw std::runtime_error("the journal keeps changing under this run");
}
bool evictUnderLock(std::uint64_t reserve) {
SpanScan scan;
if (!scanSpans(path, device.load(std::memory_order_relaxed), scan) || !scan.headerValid) return false;
const auto& spans = scan.spans;
std::map<ShaderRecompiler::PipelineIdentity, std::size_t> moduleSpans;
for (std::size_t position = 0; position < spans.size(); ++position) {
if (spans[position].kind == ModuleRecord) moduleSpans[spans[position].identity] = position;
}
const auto headerBytes = padded(sizeof(FileHeader));
const auto target = maxBytes - maxBytes / 4u;
auto budget = target > reserve ? target - reserve : headerBytes;
if (budget < headerBytes) budget = headerBytes;
std::set<ShaderRecompiler::PipelineIdentity> keptModules;
std::set<std::size_t> kept;
std::uint64_t used = headerBytes;
for (std::size_t position = spans.size(); position-- > 0;) {
const auto& span = spans[position];
if (span.kind == ModuleRecord) continue;
if (!span.decodable) continue;
std::uint64_t added = span.bytes;
std::vector<std::size_t> modules;
bool complete = true;
for (const auto& identity : span.needs) {
if (keptModules.contains(identity)) continue;
const auto found = moduleSpans.find(identity);
if (found == moduleSpans.end()) {
complete = false;
break;
}
modules.push_back(found->second);
added += spans[found->second].bytes;
}
if (!complete) continue;
if (used + added > budget) break;
used += added;
for (const auto module : modules) keptModules.insert(spans[module].identity);
kept.insert(position);
}
for (const auto& identity : keptModules) kept.insert(moduleSpans[identity]);
const auto temporary = path.string() + ".evict";
if (!copySpans(path, scan, kept, temporary)) {
std::error_code error;
std::filesystem::remove(temporary, error);
return false;
}
if (!replaceFile(temporary, path)) {
std::error_code error;
std::filesystem::remove(temporary, error);
return false;
}
std::uint64_t evicted = 0;
std::uint64_t evictedBytes = 0;
{
std::lock_guard lock(mutex);
for (std::size_t position = 0; position < spans.size(); ++position) {
if (kept.contains(position)) continue;
++evicted;
evictedBytes += spans[position].bytes;
}
writtenModules.clear();
writtenRecords.clear();
for (const auto& identity : keptModules) writtenModules.insert(identity);
for (const auto position : kept) {
if (spans[position].kind != ModuleRecord) writtenRecords.insert(spans[position].hash);
}
validBytes = used;
headerValid = true;
}
counters.evictedRecords.fetch_add(evicted, std::memory_order_relaxed);
counters.evictedBytes.fetch_add(evictedBytes, std::memory_order_relaxed);
std::fprintf(stderr, "[pipeline-journal] %s reached its size cap; %llu records (%.1f MiB) were dropped and the file was rewritten to %.1f MiB\n", path.string().c_str(), static_cast<unsigned long long>(evicted), static_cast<double>(evictedBytes) / (1024.0 * 1024.0), static_cast<double>(used) / (1024.0 * 1024.0));
return true;
}
std::vector<std::byte> EncodeModule(const PendingModule& module) {
std::vector<std::byte> payload;
Writer writer(payload);
writer.U64(module.identity.low);
writer.U64(module.identity.high);
writer.U32(static_cast<std::uint32_t>(module.stage));
ShaderRecompiler::ShaderDiskCache::EncodeResult(*module.result, payload);
return payload;
}
// One attempt: false when the file was cut or replaced by someone else since it was indexed
// (it is indexed again, and the batch is rebuilt against that index).
// (it is indexed again, and the batch is rebuilt against that index), or when the file was
// rewritten to make room for the record (the batch is rebuilt against that file).
bool append(const Pending& pending) {
const auto recordHash = ShaderRecompiler::HashBytes(pending.payload);
std::vector<std::byte> batch;
std::vector<ShaderRecompiler::PipelineIdentity> newModules;
std::vector<ShaderRecompiler::PipelineIdentity> missing;
{
std::lock_guard lock(mutex);
if (writtenRecords.contains(recordHash)) return true;
for (const auto& module : pending.modules) {
if (!writtenModules.contains(module.identity) && std::ranges::find(newModules, module.identity) == newModules.end()) newModules.push_back(module.identity);
if (writtenModules.contains(module.identity)) continue;
if (std::ranges::find(missing, module.identity) != missing.end()) continue;
missing.push_back(module.identity);
}
}
const auto appendRecord = [&](std::uint32_t kind, std::span<const std::byte> payload) {
const RecordHeader header{kind, 0u, payload.size(), ShaderRecompiler::HashBytes(payload)};
Writer writer(batch);
writer.Raw(header);
batch.insert(batch.end(), payload.begin(), payload.end());
batch.resize(static_cast<std::size_t>(padded(batch.size())));
};
for (const auto& identity : newModules) {
std::vector<std::vector<std::byte>> missingPayloads;
missingPayloads.reserve(missing.size());
for (const auto& identity : missing) {
const auto module = std::ranges::find_if(pending.modules, [&](const PendingModule& candidate) { return candidate.identity == identity; });
std::vector<std::byte> payload;
Writer writer(payload);
writer.U64(identity.low);
writer.U64(identity.high);
writer.U32(static_cast<std::uint32_t>(module->stage));
ShaderRecompiler::ShaderDiskCache::EncodeResult(*module->result, payload);
appendRecord(ModuleRecord, payload);
missingPayloads.push_back(EncodeModule(*module));
}
appendRecord(pending.kind, pending.payload);
std::uint64_t batchSize = recordBytes(pending.payload.size());
for (const auto& payload : missingPayloads) batchSize += recordBytes(payload.size());
std::error_code error;
std::filesystem::create_directories(path.parent_path(), error);
FileLock fileLock(lockPath);
@@ -782,6 +1174,7 @@ private:
return true;
}
bool fresh = false;
bool overCap = false;
{
std::lock_guard lock(mutex);
fresh = !headerValid;
@@ -795,13 +1188,41 @@ private:
else validBytes = tail;
}
}
if (!fresh && validBytes + batch.size() > maxBytes) {
overCap = !fresh && validBytes + batchSize > maxBytes;
}
if (overCap) {
std::uint64_t needed = batchSize;
std::vector<ShaderRecompiler::PipelineIdentity> held;
for (const auto& module : pending.modules) {
if (std::ranges::find(missing, module.identity) != missing.end()) continue;
if (std::ranges::find(held, module.identity) != held.end()) continue;
held.push_back(module.identity);
needed += recordBytes(EncodeModule(module).size());
}
if (needed + padded(sizeof(FileHeader)) > maxBytes) {
static std::atomic<bool> reported{false};
if (!reported.exchange(true)) std::fprintf(stderr, "[pipeline-journal] %s cannot hold a record of %llu bytes under APS5_PIPELINE_JOURNAL_MAX_MIB; it is not recorded\n", path.string().c_str(), static_cast<unsigned long long>(needed));
counters.writeFailures.fetch_add(1, std::memory_order_relaxed);
return true;
}
if (!evictUnderLock(needed)) {
static std::atomic<bool> reported{false};
if (!reported.exchange(true)) std::fprintf(stderr, "[pipeline-journal] %s reached APS5_PIPELINE_JOURNAL_MAX_MIB; later pipelines are not recorded\n", path.string().c_str());
counters.writeFailures.fetch_add(1, std::memory_order_relaxed);
return true;
}
return false;
}
std::vector<std::byte> batch;
const auto appendRecord = [&](std::uint32_t kind, std::span<const std::byte> payload) {
const RecordHeader header{kind, 0u, payload.size(), ShaderRecompiler::HashBytes(payload)};
Writer writer(batch);
writer.Raw(header);
batch.insert(batch.end(), payload.begin(), payload.end());
batch.resize(static_cast<std::size_t>(padded(batch.size())));
};
for (const auto& payload : missingPayloads) appendRecord(ModuleRecord, payload);
appendRecord(pending.kind, pending.payload);
if (!fresh) {
const auto size = std::filesystem::file_size(path, error);
if (!error && size < validBytes) {
@@ -825,9 +1246,9 @@ private:
std::lock_guard lock(mutex);
headerValid = true;
validBytes = (fresh ? padded(sizeof(FileHeader)) : validBytes) + batch.size();
for (const auto& identity : newModules) writtenModules.insert(identity);
for (const auto& identity : missing) writtenModules.insert(identity);
writtenRecords.insert(recordHash);
counters.recordedModules.fetch_add(newModules.size(), std::memory_order_relaxed);
counters.recordedModules.fetch_add(missing.size(), std::memory_order_relaxed);
if (pending.kind == GraphicsRecord) counters.recordedGraphics.fetch_add(1, std::memory_order_relaxed);
else counters.recordedCompute.fetch_add(1, std::memory_order_relaxed);
counters.writtenBytes.fetch_add(prefix.size() + batch.size(), std::memory_order_relaxed);
@@ -881,6 +1302,7 @@ std::uint64_t validTail(const std::filesystem::path& path, std::uint64_t from, s
struct ReplayCounters {
std::atomic<std::uint64_t> graphics{0}, compute{0}, present{0}, skipped{0}, rejected{0}, failed{0};
std::atomic<std::uint64_t> held{0}, heldPeak{0};
std::atomic<std::uint32_t> threads{0};
std::atomic<double> ms{0};
};
@@ -901,6 +1323,7 @@ public:
Replay(Context context, PipelineJournalHooks hooks, unsigned threads) : context(std::move(context)), hooks(std::move(hooks)), active(threads) {
started = std::chrono::steady_clock::now();
replayCounters().threads.store(threads, std::memory_order_relaxed);
replayCounters().heldPeak.store(0, std::memory_order_relaxed);
workers.reserve(threads);
for (unsigned index = 0; index < threads; ++index) workers.emplace_back([this] { run(); });
}
@@ -927,7 +1350,7 @@ private:
try {
auto journal = Journal::Instance().Load();
const auto limit = configured("APS5_PIPELINE_JOURNAL_ENTRIES", 1024u);
if (journal.entries.size() > limit) journal.entries.resize(static_cast<std::size_t>(limit));
if (journal.entries.size() > limit) journal.entries.erase(journal.entries.begin(), journal.entries.end() - static_cast<std::ptrdiff_t>(limit));
loaded = std::move(journal);
} catch (const std::exception& error) {
std::fprintf(stderr, "[pipeline-journal] cannot load %s: %s\n", Journal::Instance().Path().string().c_str(), error.what());
@@ -954,6 +1377,27 @@ private:
}
}
std::shared_ptr<const ShaderRecompiler::RecompileResult> readModule(const ShaderRecompiler::PipelineIdentity& identity, const LoadedModule& module) const {
auto bytes = std::make_shared<std::vector<std::byte>>(static_cast<std::size_t>(module.payloadBytes));
if (loaded.file == nullptr || !loaded.file->Read(module.payloadOffset, *bytes) || ShaderRecompiler::HashBytes(*bytes) != module.payloadHash) return nullptr;
auto result = std::make_shared<ShaderRecompiler::RecompileResult>();
if (!ShaderRecompiler::ShaderDiskCache::DecodeResult(std::span<const std::byte>(*bytes).subspan(20), *result, bytes) || !(result->Identity() == identity)) return nullptr;
return result;
}
struct Held {
explicit Held(std::uint64_t bytes) : bytes(bytes) {
auto& counters = replayCounters();
const auto now = counters.held.fetch_add(bytes, std::memory_order_relaxed) + bytes;
auto peak = counters.heldPeak.load(std::memory_order_relaxed);
while (peak < now && !counters.heldPeak.compare_exchange_weak(peak, now, std::memory_order_relaxed)) {}
}
~Held() { replayCounters().held.fetch_sub(bytes, std::memory_order_relaxed); }
Held(const Held&) = delete;
Held& operator=(const Held&) = delete;
std::uint64_t bytes;
};
void replay(const LoadedEntry& entry) {
struct Replaying {
Replaying() { replayingHere = true; }
@@ -966,19 +1410,27 @@ private:
counters.rejected.fetch_add(1, std::memory_order_relaxed);
return;
}
(hooks.compute(*module->second.result, entry.pushConstants) ? counters.compute : counters.present).fetch_add(1, std::memory_order_relaxed);
const Held held(module->second.payloadBytes);
const auto result = readModule(module->first, module->second);
if (result == nullptr) {
counters.rejected.fetch_add(1, std::memory_order_relaxed);
return;
}
(hooks.compute(*result, entry.pushConstants) ? counters.compute : counters.present).fetch_add(1, std::memory_order_relaxed);
return;
}
std::vector<CompiledShader> shaders;
std::vector<const std::pair<const ShaderRecompiler::PipelineIdentity, LoadedModule>*> modules;
std::uint64_t bytes = 0;
for (const auto& stage : entry.graphics.stages) {
const auto module = loaded.modules.find(stage.identity);
if (module == loaded.modules.end() || module->second.stage != stage.stage) {
counters.rejected.fetch_add(1, std::memory_order_relaxed);
return;
}
shaders.push_back({stage.stage, module->second.result.get(), stage.pushConstantOffset});
modules.push_back(&*module);
bytes += module->second.payloadBytes;
}
if (shaders.empty() || context.descriptorCache == nullptr) {
if (modules.empty() || context.descriptorCache == nullptr) {
counters.rejected.fetch_add(1, std::memory_order_relaxed);
return;
}
@@ -987,6 +1439,18 @@ private:
counters.skipped.fetch_add(1, std::memory_order_relaxed);
return;
}
const Held held(bytes);
std::vector<std::shared_ptr<const ShaderRecompiler::RecompileResult>> results;
std::vector<CompiledShader> shaders;
for (std::size_t index = 0; index < modules.size(); ++index) {
auto result = readModule(modules[index]->first, modules[index]->second);
if (result == nullptr) {
counters.rejected.fetch_add(1, std::memory_order_relaxed);
return;
}
shaders.push_back({entry.graphics.stages[index].stage, result.get(), entry.graphics.stages[index].pushConstantOffset});
results.push_back(std::move(result));
}
const auto layouts = DescribeSetLayouts(context, shaders);
if (!std::ranges::equal(layouts.mainKey, entry.graphics.mainLayoutKey) || !std::ranges::equal(layouts.fragmentKey, entry.graphics.fragmentLayoutKey) || layouts.layouts.split != entry.graphics.splitSets) {
counters.rejected.fetch_add(1, std::memory_order_relaxed);
@@ -1049,6 +1513,7 @@ State CanonicalPipelineState(const State& state) {
canonical.rectList = state.rectList;
canonical.dualSourceBlend = state.dualSourceBlend;
canonical.reversedBlend = state.reversedBlend;
canonical.colorRop = state.colorRop;
canonical.topology = state.topology;
canonical.primitiveRestart = state.primitiveRestart;
canonical.negativeOneToOne = state.negativeOneToOne;
@@ -1133,6 +1598,7 @@ PipelineJournalCounters PipelineJournalTotals() {
totals.replayFailed = replay.failed.load(std::memory_order_relaxed);
totals.replayMs = replay.ms.load(std::memory_order_relaxed);
totals.replayThreads = replay.threads.load(std::memory_order_relaxed);
totals.replayHeldPeakBytes = replay.heldPeak.load(std::memory_order_relaxed);
return totals;
}
@@ -1152,10 +1618,10 @@ void FlushPipelineJournal() {
void ReportPipelineJournal() {
if (!profiling() || !Journal::Instance().Enabled()) return;
const auto totals = PipelineJournalTotals();
AgcDriver::ProfilePrint_nid_no_patch("[pipeline-journal] loaded %llu pipelines, %llu modules (%.1f MiB, %.0f ms); replay on %u threads: %llu graphics and %llu compute built, %llu present, %llu skipped (store full), %llu rejected, %llu failed in %.0f ms; recorded this run: %llu graphics, %llu compute, %llu modules (%.1f MiB), %llu write failures\n",
AgcDriver::ProfilePrint_nid_no_patch("[pipeline-journal] loaded %llu pipelines, %llu modules (%.1f MiB, %.0f ms); replay on %u threads: %llu graphics and %llu compute built, %llu present, %llu skipped (store full), %llu rejected, %llu failed in %.0f ms, holding at most %.1f MiB of modules; recorded this run: %llu graphics, %llu compute, %llu modules (%.1f MiB), %llu write failures, %llu records evicted (%.1f MiB)\n",
static_cast<unsigned long long>(totals.loadedRecipes), static_cast<unsigned long long>(totals.loadedModules), static_cast<double>(totals.loadedBytes) / (1024.0 * 1024.0), totals.loadMs, totals.replayThreads,
static_cast<unsigned long long>(totals.replayedGraphics), static_cast<unsigned long long>(totals.replayedCompute), static_cast<unsigned long long>(totals.replayPresent), static_cast<unsigned long long>(totals.replaySkipped), static_cast<unsigned long long>(totals.replayRejected), static_cast<unsigned long long>(totals.replayFailed), totals.replayMs,
static_cast<unsigned long long>(totals.recordedGraphics), static_cast<unsigned long long>(totals.recordedCompute), static_cast<unsigned long long>(totals.recordedModules), static_cast<double>(totals.writtenBytes) / (1024.0 * 1024.0), static_cast<unsigned long long>(totals.writeFailures));
static_cast<unsigned long long>(totals.replayedGraphics), static_cast<unsigned long long>(totals.replayedCompute), static_cast<unsigned long long>(totals.replayPresent), static_cast<unsigned long long>(totals.replaySkipped), static_cast<unsigned long long>(totals.replayRejected), static_cast<unsigned long long>(totals.replayFailed), totals.replayMs, static_cast<double>(totals.replayHeldPeakBytes) / (1024.0 * 1024.0),
static_cast<unsigned long long>(totals.recordedGraphics), static_cast<unsigned long long>(totals.recordedCompute), static_cast<unsigned long long>(totals.recordedModules), static_cast<double>(totals.writtenBytes) / (1024.0 * 1024.0), static_cast<unsigned long long>(totals.writeFailures), static_cast<unsigned long long>(totals.evictedRecords), static_cast<double>(totals.evictedBytes) / (1024.0 * 1024.0));
}
std::uint64_t PipelineJournalDeviceFingerprint(const VkPhysicalDeviceProperties& properties) {
@@ -32,6 +32,7 @@
#include <map>
#include <mutex>
#include <string>
#include <string_view>
#include <thread>
#include <tuple>
#include <unordered_map>
@@ -2134,6 +2135,22 @@ void Recorder::NoteSampledDraw(VkCommandBuffer commands) {
if (open->sampleActive) open->samplesDrawn = true;
}
void Recorder::NoteDrawTraceToken(std::uint64_t token) noexcept {
if (token == 0 || open == nullptr) return;
try {
auto inserted = open->drawTraceTokenSet.insert(token);
if (!inserted.second) return;
try {
open->drawTraceTokens.push_back(token);
} catch (...) {
open->drawTraceTokenSet.erase(inserted.first);
throw;
}
} catch (...) {
AgcDriver::CaptureTrace::Log("draw-token-note-failed token=%llu", static_cast<unsigned long long>(token));
}
}
bool Recorder::gpuSampleCounter() {
if (sampleCounterState != 0) return sampleCounterState > 0;
sampleCounterState = -1;
@@ -2794,9 +2811,13 @@ void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t byte
unwrittenBytes += bytes;
}
std::shared_ptr<Buffer> Recorder::ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint32_t* derived, std::uint64_t generation) {
std::shared_ptr<Buffer> Recorder::ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::uint32_t* derived, std::uint64_t generation, VkDeviceSize* offset) {
auto found = use == SnapshotUse::Vertex ? drawSnapshots.lower_bound({address, use, bytes}) : drawSnapshots.find({address, use, bytes});
if (found == drawSnapshots.end() || std::get<0>(found->first) != address || std::get<1>(found->first) != use) return {};
if (found == drawSnapshots.end() || std::get<0>(found->first) != address || std::get<1>(found->first) != use) {
if (use != SnapshotUse::Storage || offset == nullptr) return {};
return containingDrawSnapshot(address, bytes, offset);
}
if (offset != nullptr) *offset = 0;
const bool sameRegistry = found->second.registryGeneration == GuestAllocations::GuestAllocationsGeneration_nid_postfix();
if (!sameRegistry || !GuestMemory::UnchangedSince(address, bytes, found->second.generation)) {
if (!sameRegistry || generation == 0 || use != SnapshotUse::Storage || !refreshDrawSnapshot(found->second, address, bytes)) {
@@ -2811,6 +2832,23 @@ std::shared_ptr<Buffer> Recorder::ReusableDrawSnapshot(std::uint64_t address, st
return found->second.buffer;
}
std::shared_ptr<Buffer> Recorder::containingDrawSnapshot(std::uint64_t address, std::size_t bytes, VkDeviceSize* offset) {
const VkDeviceSize alignment = std::max<VkDeviceSize>(context.limits.minStorageBufferOffsetAlignment, 4);
const auto registry = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
auto entry = drawSnapshots.upper_bound({address, SnapshotUse::Storage, std::numeric_limits<std::size_t>::max()});
for (std::size_t probe = 0; probe < ContainingSnapshotProbes && entry != drawSnapshots.begin(); ++probe) {
--entry;
const auto& [start, use, held] = entry->first;
if (use != SnapshotUse::Storage || start + held < address + bytes || (address - start) % alignment != 0) continue;
if (entry->second.registryGeneration != registry || !GuestMemory::UnchangedSince(address, bytes, entry->second.generation)) continue;
auto& recency = drawSnapshotPools[SnapshotPool(use)].recency;
recency.splice(recency.end(), recency, entry->second.recent);
*offset = address - start;
return entry->second.buffer;
}
return {};
}
bool Recorder::refreshDrawSnapshot(DrawSnapshot& entry, std::uint64_t address, std::size_t bytes) {
const auto held = entry.buffer->Bytes();
if (held.size() != bytes || bytes == 0) return false;
@@ -3596,7 +3634,13 @@ void Recorder::Submit() {
submission.pCommandBuffers = &batch->commands;
// The timeline reaches this batch's serial when it completes (see WaitSerial).
const std::uint64_t serial = submissions + 1;
CaptureTrace::Log("submit batch=%llu reads=%zu writes=%zu", static_cast<unsigned long long>(serial), batch->reads.size(), batch->writes.size());
std::string tokensStr = "[";
for (std::size_t i = 0; i < batch->drawTraceTokens.size(); ++i) {
if (i > 0) tokensStr += ",";
tokensStr += std::to_string(batch->drawTraceTokens[i]);
}
tokensStr += "]";
CaptureTrace::Log("submit batch=%llu tokens=%s", static_cast<unsigned long long>(serial), tokensStr.c_str());
VkTimelineSemaphoreSubmitInfoKHR timelineInfo{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR};
const VkSemaphore hold = std::exchange(submitHold, VK_NULL_HANDLE);
const std::uint64_t holdValue = submitHoldValue;
@@ -3625,6 +3669,7 @@ void Recorder::Submit() {
batch->submitted = true;
batch->serial = ++submissions;
batch->submittedAt = std::chrono::steady_clock::now();
CaptureTrace::Log("submit-confirmed batch=%llu tokens=%s", static_cast<unsigned long long>(batch->serial), tokensStr.c_str());
if (DrawProfiled()) {
const auto us = std::chrono::duration<double, std::micro>(batch->submittedAt - submitStart).count();
++submitCount;
@@ -3654,6 +3699,10 @@ std::uint64_t Recorder::SubmitAndEpoch() {
return inFlight.empty() ? 0 : submissions;
}
std::uint64_t Recorder::OpenBatchSerial() {
return open != nullptr ? submissions + 1 : 0;
}
namespace {
// One thread's registration as a timeline waiter (see `unlockedWaiters`): constructed while the
@@ -3707,6 +3756,20 @@ void Recorder::WaitSerial(std::uint64_t serial) {
Check(result, "vkWaitSemaphoresKHR recorder");
}
bool Recorder::WaitSerialFor(std::uint64_t serial, std::uint64_t timeoutNanos) {
PerformanceTimer frameTiming("Recorder.WaitSerialFor");
if (timeline == VK_NULL_HANDLE || serial == 0) return false;
const UnlockedWaiter waiter{id};
VkSemaphoreWaitInfoKHR wait{VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO_KHR};
wait.semaphoreCount = 1;
wait.pSemaphores = &timeline;
wait.pValues = &serial;
const auto result = context.Function<PFN_vkWaitSemaphoresKHR>("vkWaitSemaphoresKHR")(context.device, &wait, timeoutNanos);
if (result == VK_TIMEOUT) return false;
Check(result, "vkWaitSemaphoresKHR submission progress");
return true;
}
void Recorder::FinishUpTo(std::uint64_t serial) {
// A drain: the wait already happened (unlocked), the fences are signaled, so the source-0 wait
// recorded here is the bookkeeping only (a caller that did not wait first, WaitForLeases, shows
@@ -646,7 +646,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) {
context.memoryProperties2(context.physical, &properties);
const auto sampled = SampledTextureMemory();
const auto storage = storageCacheBytes();
const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage);
const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage, storage);
const bool fresh = cache.budgetDevice != context.physical;
const auto storageLimit = storageBudget(context);
if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt) || SampledBudgetReportDue(cache.reportedStorage, storageLimit, now - cache.reportedAt))) {
@@ -1196,12 +1196,12 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
reportGuestAllocation(viewed, viewedBytes);
}
}
if (DepthPlanesStale() || written) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), written ? DepthPlaneScope::Whole : DepthPlaneScope::Overlapped));
static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr;
if ((DepthPlanesStale() || written) && (written || !layers || disabled)) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), written ? DepthPlaneScope::Whole : DepthPlaneScope::Overlapped));
if (written && DepthSurfaceOverlaps(context.device, viewed.baseAddress, viewedBytes)) {
const auto storage = WrittenStorageBytes(viewed, mip);
RetireCoveredDepthSurfaces(context.device, storage.bytes, storage.extents);
}
static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr;
if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip);
static_cast<void>(words);
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
@@ -1289,16 +1289,16 @@ std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memor
const auto budget = reported->heapBudget[*heap];
const auto usage = reported->heapUsage[*heap];
const auto reserved = usage - std::min<std::uint64_t>(usage, sampledBytes + storageBytes) + sampledBytes + budget / 8u;
const auto cap = std::min<std::uint64_t>(TextureCacheBudget(memory), budget / 4u);
const auto cap = std::min<std::uint64_t>(memory.memoryHeaps[*heap].size / 2u, budget / 2u);
return std::max<std::uint64_t>(512ull << 20u, std::min<std::uint64_t>(cap, budget > reserved ? budget - reserved : 0u));
}
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) {
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes, std::uint64_t storageBytes) {
const auto heap = largestDeviceLocalHeap(memory);
if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory);
const auto budget = reported->heapBudget[*heap];
const auto usage = reported->heapUsage[*heap];
const auto reserved = usage - std::min<std::uint64_t>(usage, textureBytes) + TextureCacheBudget(memory) + budget / 8u;
const auto reserved = usage - std::min<std::uint64_t>(usage, textureBytes) + std::max(TextureCacheBudget(memory), storageBytes) + budget / 8u;
return std::max<std::uint64_t>(2048ull << 20u, budget > reserved ? budget - reserved : 0u);
}
@@ -5671,8 +5671,9 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
if (verifySnapshots && buffer != nullptr) {
static std::atomic<std::uint64_t> checked{0}, differing{0};
const auto fast = buffer->Bytes().subspan(static_cast<std::size_t>(offset), bytes);
const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes));
const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == fast.size() && std::memcmp(slow->Bytes().data(), fast.data(), fast.size()) == 0) || (slow == nullptr && std::memcmp(fast.data(), reinterpret_cast<const void*>(begin), bytes) == 0);
VkDeviceSize slowOffset = 0;
const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes), &slowOffset);
const bool same = (slow == buffer && slowOffset == offset) || (slow != nullptr && slow->Bytes().size() >= slowOffset + fast.size() && std::memcmp(slow->Bytes().data() + slowOffset, fast.data(), fast.size()) == 0) || (slow == nullptr && std::memcmp(fast.data(), reinterpret_cast<const void*>(begin), bytes) == 0);
if (!same) differing.fetch_add(1);
if (checked.fetch_add(1) % 200000 == 199999) std::fprintf(stderr, "[draw-snapshots] verify: %llu fast answers checked, %llu differing%c", static_cast<unsigned long long>(checked.load()), static_cast<unsigned long long>(differing.load()), 10);
}
@@ -5685,7 +5686,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
offset = slice.offset;
} else {
const auto generation = GuestMemory::CollectWrites(begin, bytes);
buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation);
buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation, &offset);
if (buffer == nullptr) {
buffer = std::make_shared<Buffer>(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
std::memcpy(buffer->Bytes().data(), reinterpret_cast<const void*>(begin), bytes);
@@ -382,30 +382,19 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
return face;
}
void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, bool opValExport, State& result) {
const bool opValReference = opValExport && StencilOperationValueReplaces(cx);
zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)");
const bool depthClear = (read(cx, 0x000) & 1u) != 0;
const auto view = read(cx, 0x002);
zero(cx, 0x010, 0x00001000u, "partially resident depth (DB_Z_INFO)");
DepthTarget decodeDepthPlane(const Registers& cx) {
const auto viewRegister = find(cx, 0x002);
const auto view = viewRegister == cx.end() ? 0u : viewRegister->second;
const auto viewMip = (view >> 26u) & 0xfu;
const auto maxMip = (read(cx, 0x010) >> 16u) & 0xfu;
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;
const bool stencilClear = stencil && (read(cx, 0x000) & 2u) != 0;
Require(zFormat != 2, "Z_24 depth is unsupported");
if (zFormat == 0) depthControl &= ~6u;
if (!stencil) depthControl &= ~1u;
const auto stencilRegister = find(cx, 0x011);
const auto stencilInfo = stencilRegister == cx.end() ? 0u : stencilRegister->second;
const bool stencil = (stencilInfo & 1u) != 0;
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;
Require(!stencilClear || !stencilReadOnly, "stencil clear requires a writable stencil plane");
Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported");
Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane");
DepthTarget depth{};
depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u);
depth.depthSwizzle = (read(cx, 0x010) >> 4u) & 31u;
@@ -414,9 +403,6 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx
depth.lastSlice = (view >> 13u) & 0x7ffu;
depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0;
depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0;
const bool depthWritten = zFormat != 0 && !depthReadOnly && (depthClear || (depthControl & 6u) == 6u || (read(cx, 0x000) & 0x60u) != 0);
Require(!depthWritten || 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");
if (depth.samples != 1u) {
depth.pipeBankXor = static_cast<std::uint32_t>(depth.address & 65535u);
depth.address &= ~std::uint64_t{65535u};
@@ -453,13 +439,44 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx
if (depth.stencilAddress != 0) depth.stencilAddress += static_cast<std::uint64_t>(slice) * DepthSliceBytes(depth.extent, 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);
depth.htileDepthOnly = (read(cx, 0x011) & (1u << 29u)) != 0u;
depth.htileDepthOnly = (stencilInfo & (1u << 29u)) != 0u;
if (const auto htile = find(cx, 0x005); zFormat != 0 && htile != cx.end() && htile->second != 0 && (read(cx, 0x010) & (1u << 29u)) != 0) {
depth.htileAddress = base(0x005, 0x01e) + static_cast<std::uint64_t>(view & 0x1fffu) * HtileMetadataBytes(depth.extent);
depth.htileStencil = stencil && !depth.htileDepthOnly;
}
return depth;
}
void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValExport, bool opValExport, State& result) {
const bool opValReference = opValExport && StencilOperationValueReplaces(cx);
zero(cx, 0x000, 0x0000101cu, "depth copy, resummarize or decompress draws (DB_RENDER_CONTROL)");
const bool depthClear = (read(cx, 0x000) & 1u) != 0;
const auto view = read(cx, 0x002);
zero(cx, 0x010, 0x00001000u, "partially resident 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;
const bool stencilClear = stencil && (read(cx, 0x000) & 2u) != 0;
Require(zFormat != 2, "Z_24 depth is unsupported");
if (zFormat == 0) depthControl &= ~6u;
if (!stencil) depthControl &= ~1u;
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;
Require(!stencilClear || !stencilReadOnly, "stencil clear requires a writable stencil plane");
Require(!stencilClear || !testValExport, "stencil clear with a pixel shader that exports the stencil test value is unsupported");
Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane");
const auto readAddress = zFormat != 0 ? base(0x012, 0x01a) : 0;
const auto stencilReadAddress = stencil ? base(0x013, 0x01b) : 0;
const bool depthWritten = zFormat != 0 && !depthReadOnly && (depthClear || (depthControl & 6u) == 6u || (read(cx, 0x000) & 0x60u) != 0);
Require(!depthWritten || base(0x014, 0x01c) == readAddress, "depth read and written at different addresses is unsupported");
Require(!stencil || stencilReadOnly || base(0x015, 0x01d) == stencilReadAddress, "stencil read and written at different addresses is unsupported");
auto depth = decodeDepthPlane(cx);
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;
@@ -590,6 +607,11 @@ constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u;
constexpr std::uint32_t SampleZeroRefusedInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage);
constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid);
bool staticAnchors(const Registers& cx, std::uint32_t depthSamples) {
const auto eqaa = find(cx, 0x201);
return eqaa != cx.end() && (eqaa->second & 7u) == static_cast<std::uint32_t>(std::countr_zero(depthSamples)) && (eqaa->second & 0x100000u) != 0;
}
bool overrasterized(const Registers& cx, std::uint32_t samples) {
const auto eqaa = find(cx, 0x201);
const auto mode = find(cx, 0x292);
@@ -713,15 +735,19 @@ void rasterizeSampleZero(State& state) {
state.sampleLocationGrid = sampleZero[8] != sampleZero[0] || sampleZero[16] != sampleZero[0] || sampleZero[24] != sampleZero[0];
}
bool documentedRop(std::uint32_t rop) {
return (rop >> 4u) == (rop & 0xfu) || ((rop >> 2u) & 0x33u) == (rop & 0x33u);
}
bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) {
colorControl &= ~1u;
return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u);
return ((colorControl & ~0xff0000u) == 0x10u && documentedRop((colorControl >> 16u) & 0xffu)) || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u);
}
std::string colorControlMessage(std::uint32_t colorControl) {
static constexpr const char* modes[8] = {"disable", "normal", "eliminate fast clear", "resolve", "decompress", "FMASK decompress", "DCC decompress", "reserved"};
char text[160];
std::snprintf(text, sizeof(text), "AGC graphics: only normal color rendering with copy ROP is supported (CB_COLOR_CONTROL 0x%08x, mode %s)", colorControl, modes[(colorControl >> 4u) & 7u]);
std::snprintf(text, sizeof(text), "AGC graphics: only normal color rendering with a ROP3 code of the source or brush form is supported (CB_COLOR_CONTROL 0x%08x, mode %s)", colorControl, modes[(colorControl >> 4u) & 7u]);
return text;
}
@@ -1095,6 +1121,21 @@ std::array<std::uint16_t, 4> QuadSampleMasks(const State& state, const SampleOrd
return {mask, mask, mask, mask};
}
VkLogicOp LogicOpFromRop(std::uint8_t rop) {
if (!documentedRop(rop)) {
char text[96];
std::snprintf(text, sizeof(text), "AGC graphics: ROP3 code 0x%02x combines source and brush, which is not implemented", rop);
throw std::runtime_error(text);
}
static constexpr std::array<VkLogicOp, 16> operations{
VK_LOGIC_OP_CLEAR, VK_LOGIC_OP_NOR, VK_LOGIC_OP_AND_INVERTED, VK_LOGIC_OP_COPY_INVERTED,
VK_LOGIC_OP_AND_REVERSE, VK_LOGIC_OP_INVERT, VK_LOGIC_OP_XOR, VK_LOGIC_OP_NAND,
VK_LOGIC_OP_AND, VK_LOGIC_OP_EQUIVALENT, VK_LOGIC_OP_NO_OP, VK_LOGIC_OP_OR_INVERTED,
VK_LOGIC_OP_COPY, VK_LOGIC_OP_OR_REVERSE, VK_LOGIC_OP_OR, VK_LOGIC_OP_SET};
const std::uint32_t index = (rop & 3u) | ((rop >> 4u) & 4u) | ((rop >> 4u) & 8u);
return operations[index];
}
std::array<VkSampleLocationEXT, 32> SampleLocations(const State& state) {
Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count");
std::array<VkSampleLocationEXT, 32> locations{};
@@ -1395,8 +1436,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode");
if (depthKeepsSampleZero && (read(cx, 0x292) & 1u) == 0) {
const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage));
Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || sampleInputs == 0) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f080000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA source Z interpolation or overrasterization");
const auto sampleIndex = (read(cx, 0x293) & PixelShaderIteratesSamples) != 0u ? ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) : 0u;
const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (sampleIndex | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage));
Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || sampleInputs == 0) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f080000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index input under PS_ITER_SAMPLE, a coverage input, a sample-mask export, or DB_EQAA source Z interpolation or overrasterization");
result.samples = 1;
result.customSampleLocations = false;
result.sampleLocations.fill(0);
@@ -1437,7 +1479,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
if (auto reason = sampleMaskRejection(queue, read(cx, 0x2f8), read(cx, 0x30e), read(cx, 0x30f)); !reason.empty()) throw std::runtime_error(reason);
const auto viewportControl = read(cx, 0x206);
if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl));
zero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags");
zero(cx, 0x204, ClipControlMask, "a PA_CL_CLIP_CNTL flag");
result.depthClamp = (read(cx, 0x204) & 0x0c000000u) != 0;
result.negativeOneToOne = (read(cx, 0x204) & 0x80000u) == 0;
const auto raster = read(cx, 0x205);
@@ -1474,6 +1516,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
// CB_COLOR_CONTROL mode 0 disables color writes, which only matters when a target is written.
if (const auto colorControl = read(cx, 0x202); !colorControlSupported(colorControl, result.hasColorTarget)) throw std::runtime_error(colorControlMessage(colorControl));
if (const auto colorControl = read(cx, 0x202); ((colorControl >> 4u) & 7u) == 1u) result.colorRop = static_cast<std::uint8_t>(colorControl >> 16u);
zero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export");
const auto exportFormat = result.hasColorTarget ? read(cx, 0x1c5) : 0u;
APS5_LOG_OUT_DEBUG("Export format=%u", exportFormat);
@@ -1492,9 +1535,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
color.uintExport = slotExport == 7;
if (color.samples == 1 && result.samples > 1 && result.colors.empty() && !result.depth && (read(cx, 0x292) & 1u) == 0) {
const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage));
Require(!result.perSampleShading && sampleInputs == 0 && (read(cx, 0x203) & 0x100u) == 0, "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with PS_ITER_SAMPLE, a sample index or coverage input, or a sample-mask export");
Require(sampleInputs == 0 && (read(cx, 0x203) & 0x100u) == 0, "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with a sample index or coverage input, or a sample-mask export");
result.samples = 1;
result.customSampleLocations = false;
result.perSampleShading = false;
}
if (color.samples > 1 && result.samples == 1 && result.colors.empty() && !result.depth && !result.alphaToCoverage && (read(cx, 0x292) & 1u) == 0) {
Require(result.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || (read(cx, 0x1b3) & 0x44u) == 0, "conservative rasterization with centroid interpolation of a draw raised to a multisampled target is unsupported");
@@ -1502,10 +1546,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
result.customSampleLocations = true;
result.sampleLocations.fill(0);
}
const bool merged = color.samples == 1u && !result.depth && !result.perSampleShading && (result.colors.empty() || result.mergedCoverage) && overrasterized(cx, result.samples);
if (!merged && color.samples == 1 && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result);
const bool merged = color.samples == 1u && color.coverageSamples == 1u && !result.depth && !result.perSampleShading && (result.colors.empty() || result.mergedCoverage) && overrasterized(cx, result.samples);
if (!merged && color.samples == 1 && color.coverageSamples == 1u && result.colors.empty() && !result.depth && rasterizesSampleZeroOnly(cx, result)) rasterizeSampleZero(result);
Require(color.coverageSamples == color.samples || color.coverageSamples == result.samples || (result.samples == 1u && (read(cx, 0x292) & 1u) == 0), "a one-fragment color target's sample count differs from the rasterizer's");
const bool truncated = color.samples == 1u && result.samples > 1u && result.depth && result.depth->samples == result.samples && (result.colors.empty() || result.truncatedCoverage) && !overrasterized(cx, result.samples);
Require((color.samples == result.samples && !result.mergedCoverage && !result.truncatedCoverage) || merged || truncated, "a color target's sample count differs from the rasterizer's");
Require(color.coverageSamples == color.samples || result.samples == 1u || staticAnchors(cx, result.depth->samples), "a one-fragment color target is supported only with static DB_EQAA anchors at the depth target's sample count");
result.mergedCoverage = merged;
result.truncatedCoverage = truncated;
APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast<unsigned long long>(color.address), color.extent.width, color.extent.height, static_cast<unsigned long long>(color.bytes), static_cast<unsigned>(color.format));
@@ -1689,6 +1735,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
result.color = result.colors.front();
result.blend = result.blends[result.colors.front().exportIndex];
}
if (LogicOpFromRop(result.colorRop) != VK_LOGIC_OP_COPY) {
for (const auto& blend : result.blends) Require(blend.blendEnable == VK_FALSE, "a color logic operation together with blending is not implemented");
}
Require(result.reversedBlend == 0 || (exportSlots.size() == 1 && exportCount == 1), "blending into a color target with a reversed component order through a second source color needs that target to be the shader's only color export");
if (result.dualSourceBlend) {
Require(exportSlots.size() == 2 && exportSlots[0] == 0 && exportSlots[1] == 1 && exportCount == 1, "dual-source blending needs pixel exports to MRT slots 0 and 1 and color writes to slot 0 alone");
@@ -1881,10 +1930,12 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
const auto decoded = DecodeColorFormat(format, number, swap);
const auto attrib = read(cx, 0x31d + stride);
zero(cx, 0x31d + stride, ~0x3f000u, "color destination alpha override or other CB_COLOR_ATTRIB modes");
color.samples = 1u << ((attrib >> 12u) & 7u);
Require(color.samples <= 8u, "color targets with more than eight samples are unsupported");
Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported");
const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u;
color.coverageSamples = 1u << ((attrib >> 12u) & 7u);
Require(color.coverageSamples <= 8u, "color targets with more than eight samples are unsupported");
color.samples = 1u << ((attrib >> 15u) & 3u);
Require(color.samples <= color.coverageSamples, "color targets with more fragments than samples are unsupported");
Require(color.samples == color.coverageSamples || color.samples == 1u, "color targets with more than one but fewer fragments than samples (EQAA) are unsupported");
const auto multisampleInfo = color.coverageSamples > 1 ? 0x0c006000u : 0u;
// ROUND_MODE (bit 18) only affects unorm rounding and packed float truncation. With DCC_ENABLE (bit 28) the target is written
if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 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 <= 1u || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
@@ -2024,6 +2075,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
color.fmaskAddress = address - color.fmaskPipeBankXor;
}
}
if (color.coverageSamples > color.samples && (info & 0x4000u) != 0) {
const auto fmaskHigh = find(cx, 0x3a0 + slot);
color.fmaskAddress = (((fmaskHigh == cx.end() ? 0ull : static_cast<std::uint64_t>(fmaskHigh->second & 0xffu)) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x321 + stride)) << 8u)) & ~0xffffull;
Require(color.fmaskAddress != 0, "a compressed one-fragment color target without an FMASK address is unsupported");
}
if ((info & 0x10000000u) != 0) {
if (maxMip == 0) {
const auto dccHigh = find(cx, 0x3a8 + slot);
@@ -2080,28 +2136,20 @@ std::optional<DepthCopyPass> DecodeDepthCopyPass(const QueueState& queue) {
Require((control->second & ~0xf84u) == 0, "DB depth copy combined with a stencil copy, clear, resummarize or decompress");
Require((read(cx, 0x200) & 0xfu) == 0, "DB depth copy with depth or stencil tests");
Require((read(cx, 0x010) & 3u) == 3u, "DB depth copy from a depth plane that is not Z_32_FLOAT");
Require(ColorWriteMask(cx) == 1u, "DB depth copy into anything but the red channel of color target 0");
const auto viewRegister = find(cx, 0x002);
const auto view = viewRegister == cx.end() ? 0u : viewRegister->second;
Require((read(cx, 0x010) & 0x000f1000u) == 0 && (view & 0x3c000000u) == 0, "DB depth copy from a mipmapped or partially resident depth plane");
Require((read(cx, 0x010) & 0x000f1000u) == 0 && ((viewRegister == cx.end() ? 0u : viewRegister->second) & 0x3c000000u) == 0, "DB depth copy from a mipmapped or partially resident depth plane");
Require(ColorWriteMask(cx) == 1u, "DB depth copy into anything but the red channel of color target 0");
Require((read(cx, 0x1e0) & (1u << 30u)) == 0, "DB depth copy with color target 0 blending is unsupported");
DepthCopyPass pass{};
const auto high = find(cx, 0x01a);
pass.depthAddress = (high == cx.end() ? 0ull : static_cast<std::uint64_t>(high->second & 0xffu) << 40u) | (static_cast<std::uint64_t>(read(cx, 0x012)) << 8u);
const auto size = read(cx, 0x007);
pass.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u};
pass.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u);
if (pass.samples != 1u) pass.depthAddress &= ~std::uint64_t{65535u};
if (const auto slice = view & 0x1fffu; slice != 0) pass.depthAddress += static_cast<std::uint64_t>(slice) * DepthSliceBytes(pass.extent, 4u);
pass.source = decodeDepthPlane(cx);
const auto maskLow = find(cx, 0x30e);
const auto maskHigh = find(cx, 0x30f);
Require((maskLow == cx.end() || maskLow->second == 0xffffffffu) && (maskHigh == cx.end() || maskHigh->second == 0xffffffffu), "DB depth copy with a sample mask is unsupported");
pass.sample = (control->second & 0x80u) != 0 ? 0u : (control->second >> 8u) & 0xfu;
Require(pass.sample < pass.samples, "DB depth copy of a sample the depth plane does not have");
Require(pass.sample < pass.source.samples, "DB depth copy of a sample the depth plane does not have");
pass.color = DecodeColorBuffer(cx, 0);
Require(pass.color.format == VK_FORMAT_R32_SFLOAT && pass.color.samples == 1 && pass.color.mip == 0 && pass.color.layers == 1, "DB depth copy into a color target that is not a single-sample R32 float");
Require(pass.color.extent.width == pass.extent.width && pass.color.extent.height == pass.extent.height, "DB depth copy into a color target of another size");
Require(pass.color.cmaskAddress == 0, "DB depth copy into a color target with CMASK fast clears is unsupported");
Require(drawCovers(cx, pass.extent), "DB depth copy over part of the depth surface");
Require(pass.color.extent.width == pass.source.extent.width && pass.color.extent.height == pass.source.extent.height, "DB depth copy into a color target of another size");
return pass;
}
@@ -2249,7 +2297,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
if (auto reason = sampleMaskRejection(queue, config, word, other); !reason.empty()) return reason;
}
if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word);
if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason;
if (auto reason = nonzero(cx, 0x204, ClipControlMask, "a PA_CL_CLIP_CNTL flag"); !reason.empty()) return reason;
std::uint32_t targetMask = 0, shaderMask = 0;
if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, ColorWriteMask(cx) != 0)) return colorControlMessage(word);
if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(zFormat) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason;
@@ -2066,16 +2066,22 @@ bool StorageTexture::Refresh() {
}
bool StorageTexture::RefreshLayers(std::uint32_t first, std::uint32_t count) {
if (count == 0 || geometry.thick || geometry.imageLayers <= 1 || first >= geometry.imageLayers || count > geometry.imageLayers - first || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) return Refresh();
const auto whole = [this] {
static_cast<void>(FlushDepthPlanes(descriptor.baseAddress, static_cast<std::size_t>(guestBytes)));
return Refresh();
};
if (count == 0 || geometry.thick || geometry.imageLayers <= 1 || first >= geometry.imageLayers || count > geometry.imageLayers - first || geometry.layerBytes == 0 || trackedLayerBytes == 0 || descriptor.dccAddress != 0) return whole();
const auto begin = geometry.GuestLayerOffset(first);
const auto end = geometry.GuestLayerOffset(first + count - 1) + geometry.layerBytes;
const auto firstLayer = static_cast<std::uint32_t>(begin / trackedLayerBytes);
const auto lastLayer = static_cast<std::uint32_t>(std::min<std::uint64_t>((end + trackedLayerBytes - 1) / trackedLayerBytes, trackedLayers));
if (firstLayer >= lastLayer || (firstLayer == 0 && lastLayer == trackedLayers)) return Refresh();
if (firstLayer >= lastLayer || (firstLayer == 0 && lastLayer == trackedLayers)) return whole();
for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) {
if (layerPending[layer] && (layer < firstLayer || layer >= lastLayer)) return Refresh();
if (layerPending[layer] && (layer < firstLayer || layer >= lastLayer)) return whole();
}
if (pendingAlias() != nullptr) return Refresh();
if (pendingAlias() != nullptr) return whole();
const auto scopeBegin = layerBegin(firstLayer);
static_cast<void>(FlushDepthPlanes(scopeBegin, static_cast<std::size_t>(layerBegin(lastLayer - 1) + layerBytes(lastLayer - 1) - scopeBegin)));
return refresh(firstLayer, lastLayer);
}
@@ -4,8 +4,6 @@
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/shaders/TextureDetile_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/CmaskClear_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/DepthSampleMultisample_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/DepthSampleSingle_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/TextureDetileImage_spv.h"
#include "prx/libSceAgcDriver/Graphics/shaders/TextureRepack_spv.h"
#include <algorithm>
@@ -120,11 +118,6 @@ void TextureDetiler::release() noexcept {
cmaskErrors.reset();
for (const auto pool : descriptorSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
for (const auto pool : imageDescriptorSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
for (const auto pool : depthSampleSets.pools) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
for (const auto pipeline : depthSamplePipelines) if (pipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, pipeline, nullptr);
if (depthSamplePipelineLayout) context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout")(context.device, depthSamplePipelineLayout, nullptr);
if (depthSampleLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, depthSampleLayout, nullptr);
if (depthSampler) context.Function<PFN_vkDestroySampler>("vkDestroySampler")(context.device, depthSampler, nullptr);
if (imageModule) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, imageModule, nullptr);
if (repackPipeline) context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline")(context.device, repackPipeline, nullptr);
if (repackModule) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, repackModule, nullptr);
@@ -402,79 +395,6 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask
barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, anyAccess);
}
void TextureDetiler::DispatchDepthSample(VkCommandBuffer commands, VkImageView depth, bool multisampled, VkImageView target, std::uint32_t width, std::uint32_t height, std::uint32_t sample) {
Require(commands != VK_NULL_HANDLE && depth != VK_NULL_HANDLE && target != VK_NULL_HANDLE, "a depth sample copy needs a command buffer, the depth view and the target view");
if (depthSamplePipelineLayout == VK_NULL_HANDLE) {
VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
samplerInfo.magFilter = VK_FILTER_NEAREST;
samplerInfo.minFilter = VK_FILTER_NEAREST;
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
Check(context.Function<PFN_vkCreateSampler>("vkCreateSampler")(context.device, &samplerInfo, nullptr, &depthSampler), "vkCreateSampler depth sample copy");
std::array<VkDescriptorSetLayoutBinding, 2> bindings{};
bindings[0] = {0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, &depthSampler};
bindings[1] = {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
VkDescriptorSetLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layoutInfo.bindingCount = static_cast<std::uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();
Check(context.Function<PFN_vkCreateDescriptorSetLayout>("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &depthSampleLayout), "vkCreateDescriptorSetLayout depth sample copy");
const VkPushConstantRange pushRange{VK_SHADER_STAGE_COMPUTE_BIT, 0, 3 * sizeof(std::uint32_t)};
VkPipelineLayoutCreateInfo pipelineLayoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &depthSampleLayout;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushRange;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &depthSamplePipelineLayout), "vkCreatePipelineLayout depth sample copy");
}
auto& depthSamplePipeline = depthSamplePipelines[multisampled ? 1u : 0u];
if (depthSamplePipeline == VK_NULL_HANDLE) {
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = multisampled ? sizeof(DEPTH_SAMPLE_MULTISAMPLE_SPV) : sizeof(DEPTH_SAMPLE_SINGLE_SPV);
moduleInfo.pCode = multisampled ? DEPTH_SAMPLE_MULTISAMPLE_SPV : DEPTH_SAMPLE_SINGLE_SPV;
VkShaderModule depthModule = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &depthModule), "vkCreateShaderModule depth sample copy");
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO};
pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
pipelineInfo.stage.module = depthModule;
pipelineInfo.stage.pName = "main";
pipelineInfo.layout = depthSamplePipelineLayout;
const auto created = context.Function<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &depthSamplePipeline);
context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, depthModule, nullptr);
Check(created, "vkCreateComputePipelines depth sample copy");
}
const auto set = allocateFrom(depthSampleSets, depthSampleLayout, {{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 64}, {VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 64}}, "vkCreateDescriptorPool depth sample copy", "vkAllocateDescriptorSets depth sample copy", "vkResetDescriptorPool depth sample copy");
const VkDescriptorImageInfo depthInfo{VK_NULL_HANDLE, depth, VK_IMAGE_LAYOUT_GENERAL};
const VkDescriptorImageInfo targetInfo{VK_NULL_HANDLE, target, VK_IMAGE_LAYOUT_GENERAL};
std::array<VkWriteDescriptorSet, 2> writes{};
for (std::uint32_t index = 0; index < writes.size(); ++index) {
writes[index].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
writes[index].dstSet = set;
writes[index].dstBinding = index;
writes[index].descriptorCount = 1;
writes[index].descriptorType = index == 0 ? VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER : VK_DESCRIPTOR_TYPE_STORAGE_IMAGE;
}
writes[0].pImageInfo = &depthInfo;
writes[1].pImageInfo = &targetInfo;
context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, static_cast<std::uint32_t>(writes.size()), writes.data(), 0, nullptr);
const auto barrier = [&](VkPipelineStageFlags source, VkPipelineStageFlags destination, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) {
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = sourceAccess;
memory.dstAccessMask = destinationAccess;
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, source, destination, 0, 1, &memory, 0, nullptr, 0, nullptr);
};
constexpr VkAccessFlags anyWrite = 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_HOST_WRITE_BIT;
constexpr VkAccessFlags anyAccess = anyWrite | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_HOST_READ_BIT;
barrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, anyWrite, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
context.Function<PFN_vkCmdBindPipeline>("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, depthSamplePipeline);
context.Function<PFN_vkCmdBindDescriptorSets>("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, depthSamplePipelineLayout, 0, 1, &set, 0, nullptr);
const std::array<std::uint32_t, 3> push{width, height, sample};
context.Function<PFN_vkCmdPushConstants>("vkCmdPushConstants")(commands, depthSamplePipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(push), push.data());
context.Function<PFN_vkCmdDispatch>("vkCmdDispatch")(commands, (width + 7u) / 8u, (height + 7u) / 8u, 1);
barrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_SHADER_WRITE_BIT, anyAccess);
}
void TextureDetiler::DispatchImage(VkCommandBuffer commands, TextureTileMode tileMode, std::uint32_t elementBytes, VkBuffer tiled, std::uint64_t tiledOffset, VkImageView view, const TileMipLayout& layout, bool retile, std::uint32_t slice, const DetileWindow& window) {
Require(commands != VK_NULL_HANDLE, "texture detiling requires an active command buffer");
Require(window.rangeBegin < window.rangeEnd && window.tiledBase <= window.rangeBegin, "texture detiling window is empty");
@@ -243,7 +243,12 @@ void FullQueueBlocksCaller() {
constexpr int ChildLines = 50;
[[noreturn]] void EndChild(const std::string& mode, const char* output) {
std::FILE* file = std::fopen(output, "wb");
const auto path = std::filesystem::temp_directory_path() / output;
#ifdef _WIN32
std::FILE* file = _wfopen(path.c_str(), L"wb");
#else
std::FILE* file = std::fopen(path.c_str(), "wb");
#endif
Check(file != nullptr, "child output file");
#ifdef _WIN32
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX);
@@ -288,12 +293,18 @@ std::string Quote(const std::string& text) {
void LinesSurviveEnd(const std::string& self, const std::string& mode) {
const auto output = std::filesystem::temp_directory_path() / ("aps5-async-log-" + mode + "-" + std::to_string(Clock::now().time_since_epoch().count()) + ".txt");
std::string command = Quote(self) + " child " + mode + " " + Quote(output.string());
#ifdef _WIN32
command = Quote(command);
#endif
(void)self;
std::wstring executable(32768, L'\0');
executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast<DWORD>(executable.size())));
const std::wstring command = L"\"\"" + executable + L"\" child " + std::filesystem::path(mode).wstring() + L" \"" + output.filename().wstring() + L"\"\"";
const auto start = Clock::now();
const int status = _wsystem(command.c_str());
#else
const std::string command = Quote(self) + " child " + mode + " " + Quote(output.filename().string());
const auto start = Clock::now();
const int status = std::system(command.c_str());
#endif
const auto lived = Clock::now() - start;
std::error_code error;
const bool started = std::filesystem::exists(output, error);
@@ -602,6 +602,9 @@ void importJoinTests(const Context& context) {
Require(pieces != nullptr && pieces->base == heapBase && pieces->bytes == 2 * piece && piecesBuffer == pieces->buffer && piecesCopied == 0, "a buffer bound exactly over two pieces no larger than the import window was not imported as one");
change(heap + 2 * piece, piece, true);
static_cast<void>(bind(heapBase + 2 * piece - 16));
const auto [prefixBuffer, prefixCopied] = bind(heapBase, 2 * piece + 16);
const auto* prefix = HostImportFor(importing, heapBase, 2 * piece + 16);
Require(prefix != nullptr && prefix->base == heapBase && prefix->bytes == 3 * piece && prefixBuffer == prefix->buffer && prefixCopied == 0, "a buffer bound from the first of three pieces into the last was not imported over the pieces it crosses");
const auto* kept = HostImportFor(importing, first, 16);
Require(kept != nullptr && kept->buffer == joinedBuffer && kept->bytes == 2 * large, "a registry change elsewhere re-imported the joined ranges");
change(guest + 2 * large, large, true);
@@ -880,7 +883,7 @@ private:
void importMappedFailureGenerationTests(const Context& context) {
ClearHostImports(context.device);
WindowedRange range(context, WindowedRange::piece, 1);
WindowedRange range(context, 2 * WindowedRange::piece, 1);
ImportAllocationProbe probe(range.importing);
probe.failure = VK_ERROR_INVALID_EXTERNAL_HANDLE;
GuestAllocations::Mapped mapped;
@@ -1081,7 +1084,7 @@ void importNeighbouringEdgesTests(const Context& context) {
// observer holds no GPU lock, so it must not evict (before, it evicted the least recently used import to fit).
void importAtMapOverBudgetTests(const Context& context) {
ClearHostImports(context.device);
constexpr std::size_t bytes = 65536;
constexpr std::size_t bytes = 2 * 65536;
auto importing = context;
importing.hostImportAlignment = bytes;
void* kept = ::operator new(bytes, std::align_val_t{bytes});
@@ -1142,17 +1145,32 @@ void importRequesterTests(const Context& context, const BdaTestAccess& access) {
const auto counted = [](ImportRequester requester) { return HostImportCounters().requesters[static_cast<std::size_t>(requester)]; };
const auto made = [&](ImportRequester requester) { return counted(requester).made; };
Require(std::string(ImportRequesterName(ImportRequester::Map)) == "map" && std::string(ImportRequesterName(ImportRequester::Build)) == "build" && std::string(ImportRequesterName(ImportRequester::Access)) == "access" && std::string(ImportRequesterName(ImportRequester::Join)) == "join" && std::string(ImportRequesterName(ImportRequester::Segment)) == "segment", "an import requester has the wrong name");
change(0, true);
{
const auto mappedRange = [&](std::size_t index) {
GuestAllocations::Mapped ranges;
const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix();
for (const auto& range : lease) {
if (range->address == address(0)) ranges.push_back(range);
if (range->address == address(index)) ranges.push_back(range);
}
Require(ranges.size() == 1, "the requester test's mapped range is not registered");
return ranges;
};
{
GuestAllocations::Mutation mutation;
mutation.Add(block + 6 * piece, 2 * piece, true, true, true);
}
{
const auto before = made(ImportRequester::Map);
static_cast<void>(ImportMappedRanges(importing, ranges, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true));
Require(made(ImportRequester::Map) == before + 1 && HostImportCovers(importing, address(0), piece), "a range imported at map time was not counted for the map requester");
static_cast<void>(ImportMappedRanges(importing, mappedRange(6), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true));
Require(made(ImportRequester::Map) == before + 1 && HostImportCovers(importing, address(6), 2 * piece), "a range imported at map time was not counted for the map requester");
}
change(0, true);
{
const auto map = made(ImportRequester::Map);
const auto access = made(ImportRequester::Access);
static_cast<void>(ImportMappedRanges(importing, mappedRange(0), GuestAllocations::GuestAllocationsGeneration_nid_postfix(), true));
Require(made(ImportRequester::Map) == map && !HostImportCovers(importing, address(0), 16), "a range of 64 KiB was imported at map time");
Require(HostImportFor(importing, address(0), 16) != nullptr && HostImportCovers(importing, address(0), piece) && made(ImportRequester::Access) == access + 1 && made(ImportRequester::Map) == map,
"a range of 64 KiB left to its first GPU use was not imported there, or not counted for that use's requester");
}
change(1, true);
{
@@ -1230,6 +1248,10 @@ void importRequesterTests(const Context& context, const BdaTestAccess& access) {
const auto counts = HostImportCounters();
Require(counts.live >= 4 && counts.liveBytes == HostImportLiveBytes(), "the live import counts disagree with the live bytes");
for (std::size_t index = 0; index < 6; ++index) change(index, false);
{
GuestAllocations::Mutation mutation;
mutation.Remove(block + 6 * piece);
}
ClearHostImports(importing.device);
::operator delete(block, std::align_val_t{piece});
}
@@ -2170,6 +2192,119 @@ ShaderRecompiler::DescriptorBinding binding(Role role, std::uint32_t slot) {
return {ShaderRecompiler::DescriptorKind::StorageBuffer, role, 0, slot, 1, {}, false};
}
void partialHeapImportBudgetCase(const Context& context, const BdaTestAccess& access, std::size_t bytes, std::size_t budgetBytes, std::size_t prefixBytes) {
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) return;
constexpr std::size_t window = 2 * 65536;
const auto allocated = bytes + 65536;
void* block = GuestArena::GuestArenaAllocate_nid_postfix(allocated, 65536);
#ifdef _WIN32
GuestArena::GuestArenaCommit_nid_postfix(block, allocated, PAGE_READWRITE, allocated);
#endif
auto* guest = static_cast<std::uint8_t*>(block);
std::memset(block, 0x39, bytes);
std::memset(guest + bytes, 0xa7, allocated - bytes);
const auto first = reinterpret_cast<std::uintptr_t>(block);
auto importing = context;
importing.hostImportAlignment = 65536;
const auto live = HostImportLiveBytes();
const auto before = MirrorCounters();
SetHostImportRangeWindow(window);
SetHostImportBudget(live + budgetBytes);
const auto registry = [&](bool add) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, true, true);
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
registry(true);
{
auto withoutImports = context;
withoutImports.hostImportAlignment = 0;
GuestBufferMemory leased(withoutImports);
leased.AcquireRegistered();
leased.AddWritable(first + bytes - 16, 4);
leased.Upload(true);
std::uint32_t adjustment = 0;
const auto view = leased.Descriptor(first + bytes - 16, 4, adjustment);
access.bytes(view.buffer)[view.offset + adjustment] = std::byte{0x43};
leased.WriteBack();
}
Require(MirrorCounters().heapBytes == before.heapBytes + bytes && guest[bytes - 16] == 0x43, "the partial-import fixture did not start with a valid full heap mirror");
GuestAllocations::GuestAllocationsEnd_nid_postfix(GuestAllocations::GuestAllocationsBegin_nid_postfix());
const auto build = [&](bool write) {
GuestBufferMemory leased(importing);
leased.AcquireRegistered();
if (write) {
leased.AddWritable(first + 16, 32);
leased.AddWritable(first + bytes - 48, 32);
}
leased.Upload(true);
const auto* prefix = ExistingHostImportFor(importing, first, window);
Require(prefix != nullptr, "a heap exceeding the import budget did not import its fitting prefix");
Require(HostImportLiveBytes() <= live + budgetBytes, "partial heap imports exceeded the configured budget");
Require(MirrorCounters().heapBytes == before.heapBytes + bytes - prefixBytes, "a partially imported heap mirrored bytes already imported in place");
auto covered = first;
for (const auto& entry : leased.AddressRanges()) {
if (entry.end <= first || entry.begin >= first + bytes) continue;
Require(entry.begin == covered && entry.end <= first + bytes, "partial heap BDA ranges have a gap or overlap");
if (entry.end <= first + prefixBytes) {
const auto* imported = ExistingHostImportFor(importing, entry.begin, static_cast<std::size_t>(entry.end - entry.begin));
Require(imported != nullptr && imported->bytes <= window && entry.deviceAddress == imported->address + entry.begin - imported->base, "the partial heap prefix is not served by bounded imports");
}
if (entry.begin >= first + prefixBytes) {
std::uint32_t adjustment = 0;
const auto view = leased.Descriptor(entry.begin, static_cast<std::size_t>(entry.end - entry.begin), adjustment);
const auto data = access.bytes(view.buffer).subspan(view.offset + adjustment);
Require(data[0] == static_cast<std::byte>(guest[entry.begin - first]) && data[entry.end - entry.begin - 1] == static_cast<std::byte>(guest[entry.end - first - 1]), "a partial heap mirror lost its first or last bytes");
}
covered = entry.end;
}
Require(covered == first + bytes, "a partially imported heap does not cover its registered range");
if (write) {
for (const auto at : {first + 16, first + bytes - 48}) {
if (at < first + prefixBytes) guest[at - first] = 0x71;
else {
std::uint32_t adjustment = 0;
const auto view = leased.Descriptor(at, 32, adjustment);
access.bytes(view.buffer)[view.offset + adjustment] = std::byte{0x71};
}
guest[at - first + 8] = 0x52;
}
}
leased.WriteBack();
};
build(true);
Require(guest[16] == 0x71 && guest[24] == 0x52 && guest[bytes - 48] == 0x71 && guest[bytes - 40] == 0x52, "partial heap write-back lost GPU or genuine CPU stores");
guest[bytes - 1] = 0x67;
build(false);
const auto quiet = MirrorCounters();
guest[bytes + 9] = 0x8a;
build(false);
Require(MirrorCounters().blocksCopied == quiet.blocksCopied, "a neighboring CPU write refreshed an unrelated partial heap mirror");
Require(guest[bytes + 9] == 0x8a && guest[bytes] == 0xa7 && guest[allocated - 1] == 0xa7, "partial heap uses changed unregistered neighboring memory");
registry(false);
{
GuestBufferMemory leased(importing);
leased.AcquireRegistered();
leased.Upload(true);
leased.WriteBack();
}
Require(MirrorCounters().heapBytes == before.heapBytes, "a partial heap mirror outlived its registered range");
SetHostImportBudget(0);
SetHostImportRangeWindow(std::uint64_t{64} << 20u);
GuestArena::GuestArenaReset_nid_postfix(block, allocated);
GuestArena::GuestArenaRelease_nid_postfix(block, allocated);
}
void partialHeapImportBudgetTests(const Context& context, const BdaTestAccess& access) {
constexpr std::size_t window = 2 * 65536;
partialHeapImportBudgetCase(context, access, 3 * window, 2 * window, 2 * window);
partialHeapImportBudgetCase(context, access, 3 * window + 65536, 3 * window, 2 * window);
SetHostImportMaxBytes(window);
partialHeapImportBudgetCase(context, access, 3 * window, 2 * window, 2 * window);
SetHostImportMaxBytes(0);
}
void importedHeapMirrorTests(const Context& context, const BdaTestAccess& access) {
if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) {
std::cout << "guest arena unavailable or not write-watched: heap mirrors of imported ranges not tested\n";
@@ -3706,6 +3841,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
importReportRefusalMemoryTests(context, access);
sparseImportedHeapTests(context, access);
heapMirrorTests(context, access);
partialHeapImportBudgetTests(context, access);
importedHeapMirrorTests(context, access);
importWatchTests();
importedFreshTests(context);
@@ -0,0 +1,160 @@
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
#define _POSIX_C_SOURCE 200809L
#endif
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>
namespace {
void Check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void RemoveEnvironmentVariable(const char* name) {
#ifdef _WIN32
_putenv_s(name, "");
#else
unsetenv(name);
#endif
}
bool FileExists(const char* path) {
std::error_code error;
return std::filesystem::exists(path, error) && !error;
}
std::string ReadFile(const char* path) {
std::ifstream stream(path, std::ios::binary);
Check(stream.is_open(), "the trace file was not created at the expected path");
std::ostringstream buffer;
buffer << stream.rdbuf();
return buffer.str();
}
bool WaitForRecord(const char* path, const std::string& record) {
for (int attempt = 0; attempt < 100; ++attempt) {
if (FileExists(path) && ReadFile(path).find(record) != std::string::npos) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return false;
}
const char* RequiredPathVariable() {
const char* configured = std::getenv("APS5_CAPTURE_TRACE_PATH");
Check(configured != nullptr && configured[0] != '\0',
"this case requires a non-empty APS5_CAPTURE_TRACE_PATH");
return configured;
}
void PathDefault() {
RemoveEnvironmentVariable("APS5_CAPTURE_TRACE_PATH");
Check(std::string(AgcDriver::CaptureTracePath()) == "capture-trace.log",
"an absent APS5_CAPTURE_TRACE_PATH must select capture-trace.log");
}
void PathEmpty() {
const char* configured = std::getenv("APS5_CAPTURE_TRACE_PATH");
Check(configured != nullptr && configured[0] == '\0',
"this case requires an empty APS5_CAPTURE_TRACE_PATH");
Check(std::string(AgcDriver::CaptureTracePath()) == "capture-trace.log",
"an empty APS5_CAPTURE_TRACE_PATH must select capture-trace.log");
}
void PathExplicit() {
const char* configured = RequiredPathVariable();
Check(std::string(AgcDriver::CaptureTracePath()) == std::string(configured),
"a non-empty APS5_CAPTURE_TRACE_PATH must be selected verbatim");
}
void ExplicitWrites() {
const char* configured = RequiredPathVariable();
Check(AgcDriver::CaptureTrace::Enabled(), "APS5_CAPTURE_TRACE must enable capture tracing");
AgcDriver::CaptureTrace::Log("capture-trace-explicit label=%d", 7);
Check(WaitForRecord(configured, "capture-trace-explicit label=7"),
"the configured trace path did not receive the capture record");
std::remove(configured);
}
void DefaultWrites() {
RemoveEnvironmentVariable("APS5_CAPTURE_TRACE_PATH");
Check(AgcDriver::CaptureTrace::Enabled(), "APS5_CAPTURE_TRACE must enable capture tracing");
Check(std::string(AgcDriver::CaptureTracePath()) == "capture-trace.log",
"an absent APS5_CAPTURE_TRACE_PATH must keep the default path");
AgcDriver::CaptureTrace::Log("capture-trace-default label=%d", 5);
Check(WaitForRecord("capture-trace.log", "capture-trace-default label=5"),
"the default trace path did not receive the capture record");
std::remove("capture-trace.log");
}
void OpenFailure() {
const char* configured = RequiredPathVariable();
Check(!FileExists(configured), "the configured trace path must not already exist");
Check(AgcDriver::CaptureTrace::Enabled(), "APS5_CAPTURE_TRACE must enable capture tracing");
bool threw = false;
try {
AgcDriver::CaptureTrace::Log("capture-trace-failure label=%d", 9);
} catch (const std::exception& error) {
threw = true;
std::cerr << "unexpected exception: " << error.what() << '\n';
}
Check(!threw, "an unopenable trace path must not throw from Log");
Check(!FileExists(configured), "an unopenable trace path must not create the requested file");
Check(!FileExists("capture-trace.log"),
"an unopenable trace path must not fall back to capture-trace.log");
}
void BestEffortOverflow() {
const char* configured = RequiredPathVariable();
Check(AgcDriver::CaptureTrace::Enabled(), "APS5_CAPTURE_TRACE must enable capture tracing");
const std::string oversized(4096, 'x');
bool threw = false;
try {
AgcDriver::CaptureTrace::LogBestEffort("capture-trace-best-effort payload=%s", oversized.c_str());
} catch (...) {
threw = true;
}
Check(!threw, "best-effort capture logging must not propagate formatting failures");
std::remove(configured);
}
void Disabled() {
RemoveEnvironmentVariable("APS5_CAPTURE_TRACE");
Check(!AgcDriver::CaptureTrace::Enabled(),
"capture tracing must stay disabled when APS5_CAPTURE_TRACE is absent");
AgcDriver::CaptureTrace::Log("capture-trace-disabled label=%d", 3);
Check(!FileExists("capture-trace.log"),
"disabled tracing must not initialize or write capture-trace.log");
}
}
int main(int argc, char** argv) {
try {
if (argc != 2) throw std::runtime_error("expected exactly one test mode argument");
const std::string mode = argv[1];
if (mode == "path-default") PathDefault();
else if (mode == "path-empty") PathEmpty();
else if (mode == "path-explicit") PathExplicit();
else if (mode == "explicit-writes") ExplicitWrites();
else if (mode == "default-writes") DefaultWrites();
else if (mode == "open-failure") OpenFailure();
else if (mode == "best-effort-overflow") BestEffortOverflow();
else if (mode == "disabled") Disabled();
else throw std::runtime_error("unknown capture trace test mode: " + mode);
std::cout << "capture trace tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -155,6 +155,32 @@ void unknownCode(AgcDriver::VulkanDevice& device) {
expect(device, color, [](std::uint32_t, std::uint32_t) { return Clear; }, "fast-cleared target eliminated after a CMASK code 3");
}
void expectRefused(const std::function<void()>& action, const std::string& message, const std::string& what) {
try {
action();
} catch (const std::exception& error) {
Require(std::string(error.what()).find(message) != std::string::npos, what + ": refused with " + error.what());
return;
}
throw std::runtime_error(what + ": was not refused");
}
void oneFragment(AgcDriver::VulkanDevice& device) {
auto color = target(ColorTileMode::RenderTarget);
color.coverageSamples = 4;
prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX == 2 && tileY == 0; }, 3);
expectRefused([&] { eliminate(device, color); }, "CMASK code 0x3 of a one-fragment color target is not implemented", "a one-fragment target with CMASK code 3");
prepare(color, [](std::uint32_t, std::uint32_t) { return true; });
eliminate(device, color);
expect(device, color, [](std::uint32_t, std::uint32_t) { return Clear; }, "a fast-cleared one-fragment target eliminated");
prepare(color, [](std::uint32_t tileX, std::uint32_t tileY) { return tileX == 7 && tileY == 1; });
eliminate(device, color);
expect(device, color, [](std::uint32_t x, std::uint32_t y) { return x >= 56 && y >= 8 ? Clear : Stale; }, "a one-fragment target with one fast-cleared tile eliminated");
prepare(color, [](std::uint32_t tileX, std::uint32_t) { return tileX < 4; }, 0xc);
eliminate(device, color);
expect(device, color, [](std::uint32_t, std::uint32_t) { return Stale; }, "a one-fragment target with CMASK codes 0xc and 0xf eliminated");
}
}
int main() {
@@ -175,6 +201,7 @@ int main() {
run(*device, ColorTileMode::Linear, "linear");
run(*device, ColorTileMode::RenderTarget, "SW_64KB_R_X");
unknownCode(*device);
oneFragment(*device);
device->WaitIdle();
device.reset();
{
@@ -445,6 +445,26 @@ void verifyProgram(const Program& program) {
}
}
const std::vector<std::uint32_t> TestedHeaderLatchStore{
0xbe880380u, 0x7e020280u, 0xbf0a8408u, 0xbf840004u, 0xe0700000u, 0x80000100u, 0x80088108u, 0xbf82fffau, 0xbf810000u};
const std::vector<std::uint32_t> SelfLoopLatchStore{
0xbe880380u, 0x7e020280u, 0xe0700000u, 0x80000100u, 0x80088108u, 0xbf0a8408u, 0xbf85fffbu, 0xbf810000u};
void verifyEmptyLoopContinues() {
for (const auto& [name, code] : {std::pair{"a loop whose latch stores behind a tested header", &TestedHeaderLatchStore}, std::pair{"a single-block loop that stores", &SelfLoopLatchStore}}) {
auto graph = GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(*code));
Structurizer{}.Structurize(graph);
bool loops = false;
for (const auto& block : graph.blocks) {
if (!block.terminator.loopHeader) continue;
loops = true;
const auto& continueTarget = graph.FindBlock(block.terminator.continueBlock);
require(continueTarget.instructionBegin == continueTarget.instructionEnd, std::string(name) + ": the continue block " + std::to_string(continueTarget.id) + " of the loop at block " + std::to_string(block.id) + " holds instructions");
}
require(loops, std::string(name) + ": no loop was structured");
}
}
const std::vector<std::uint32_t> DwordTableGetpcFirst{
0xbe8c1f00u, 0x800cff0cu, 0x00000058u, 0x820d800du, 0x83928202u, 0xd4c40016u, 0x02000500u, 0x8f128212u, 0xf4000386u, 0x24000000u,
0xbf8cc07fu, 0x7d820003u, 0x808c0e0cu, 0x828d800du, 0xbefd210cu, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u,
@@ -711,6 +731,18 @@ loop:
s_cbranch_scc1 loop
s_endpgm)",
{0xbe880380u, 0x7e020280u, 0xe0700000u, 0x80000100u, 0x80088108u, 0xbf0a8408u, 0xbf85fffbu, 0xbf810000u}, Split::None},
{"memory access in a loop latch behind a tested header", R"(
s_mov_b32 s8, 0
v_mov_b32 v1, 0
loop:
s_cmp_lt_u32 s8, 4
s_cbranch_scc0 done
buffer_store_dword v1, off, s[0:3], 0
s_add_u32 s8, s8, 1
s_branch loop
done:
s_endpgm)",
TestedHeaderLatchStore, Split::None},
{"shared tail in a loop", R"(
s_mov_b32 s8, 0
v_mov_b32 v1, 0
@@ -1421,6 +1453,7 @@ done:
verifyCallLinkMaskReadByScalarInstruction();
verifyLargeLoopExitFallback();
verifyNestedExitForwarders();
verifyEmptyLoopContinues();
verifyIrreducibleLoopEntry();
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
@@ -1,4 +1,5 @@
#include "GraphicsTests.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
@@ -13,6 +14,14 @@
#include <tuple>
#include <utility>
#include <vector>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
namespace {
@@ -417,6 +426,62 @@ void TestRetiredSurfaceRelease(const Context& context) {
ClearDepthSurfaces(context.device);
}
void TestUnmappedStalePlaneFlush(const Context& context) {
ClearDepthSurfaces(context.device);
constexpr VkExtent2D extent{256, 256};
constexpr std::uint64_t unmapped = 0xa0000000;
DepthTarget target{unmapped, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0};
target.depthSwizzle = XorSwizzleMode(TextureTileMode::kZ64KBX);
Require(DepthSurfaceView(context, target, true) != VK_NULL_HANDLE, "a mirrorable depth surface must render");
Require(DepthPlanesStale(), "a written mirrorable depth surface must leave its plane stale");
std::vector<std::pair<std::uint64_t, std::uint64_t>> stored;
try {
stored = FlushDepthPlanes(unmapped, DepthSliceBytes(extent, 4u));
} catch (const std::exception& error) {
Require(false, std::string("flushing a depth plane whose guest memory is not mapped must drop its results: ") + error.what());
}
Require(stored.empty(), "a depth plane without mapped guest memory must not be stored");
Require(!DepthPlanesStale(), "a dropped depth plane must not stay stale");
ClearDepthSurfaces(context.device);
}
void TestReadOnlyStalePlaneFlush(const Context& context) {
ClearDepthSurfaces(context.device);
constexpr std::size_t bytes = 1024 * 1024;
#ifdef _WIN32
const auto release = [](void* pointer) { VirtualFree(pointer, 0, MEM_RELEASE); };
std::unique_ptr<void, decltype(release)> mapping(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READONLY), release);
Require(mapping != nullptr, "cannot map a read-only depth plane");
#else
const auto release = [](void* pointer) { munmap(pointer, bytes); };
std::unique_ptr<void, decltype(release)> mapping(mmap(nullptr, bytes, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0), release);
Require(mapping.get() != MAP_FAILED, "cannot map a read-only depth plane");
#endif
const auto address = (reinterpret_cast<std::uintptr_t>(mapping.get()) + 65535u) & ~std::uintptr_t{65535u};
constexpr VkExtent2D extent{256, 256};
const auto planeBytes = DepthSliceBytes(extent, 4u);
Require(!AgcDriver::GuestMemory::CommittedRanges(address, planeBytes).empty(), "a read-only depth plane must remain readable");
Require(AgcDriver::GuestMemory::CommittedRanges(address, planeBytes, true).empty(), "a read-only depth plane must not be writable");
auto storageContext = context;
storageContext.formatProperties = [](VkPhysicalDevice, VkFormat, VkFormatProperties* properties) {
*properties = {};
properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
};
DepthTarget target{address, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0};
target.depthSwizzle = XorSwizzleMode(TextureTileMode::kZ64KBX);
Require(DepthSurfaceView(storageContext, target, true) != VK_NULL_HANDLE, "a read-only depth plane must render");
Require(DepthPlanesStale(), "the read-only depth plane must have pending results");
bool rejected = false;
try {
static_cast<void>(FlushDepthPlanes(address, planeBytes));
} catch (const std::runtime_error& error) {
rejected = std::string_view(error.what()).find("storage texture has no committed guest pages") != std::string_view::npos;
}
Require(rejected, "a mapped read-only depth plane must retain the unsupported-store rejection");
Require(DepthPlanesStale(), "a mapped read-only depth plane must retain its pending results");
ClearDepthSurfaces(context.device);
}
void RunDepthSurfaceReuseTests() {
static int deviceTag = 0;
Context context{};
@@ -432,6 +497,8 @@ void RunDepthSurfaceReuseTests() {
TestMultisampleDepthOwnership(context);
TestMultisampleStencilAliases(context);
TestStencilUnormView(context);
TestUnmappedStalePlaneFlush(context);
TestReadOnlyStalePlaneFlush(context);
constexpr std::uint64_t depth32 = 0x40000000;
constexpr std::uint64_t depth16 = 0x50000000;
constexpr std::uint64_t depthStencil = 0x60000000;
@@ -60,6 +60,25 @@ VkQueue FakeQueue() {
return reinterpret_cast<VkQueue>(static_cast<std::uintptr_t>(0x2000));
}
void Diagnostics() {
using AgcDriver::ChooseDeviceDiagnostics;
constexpr VkDeviceDiagnosticsConfigFlagsNV all = VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_SHADER_DEBUG_INFO_BIT_NV | VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_RESOURCE_TRACKING_BIT_NV | VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_AUTOMATIC_CHECKPOINTS_BIT_NV | VK_DEVICE_DIAGNOSTICS_CONFIG_ENABLE_SHADER_ERROR_REPORTING_BIT_NV;
for (const bool configSupported : {false, true}) {
for (const bool checkpointsSupported : {false, true}) {
const auto off = ChooseDeviceDiagnostics(false, configSupported, checkpointsSupported);
Require(!off.config && !off.checkpoints && off.flags == 0, "diagnostics were chosen without APS5_NV_DIAGNOSTICS");
}
}
for (const bool checkpointsSupported : {false, true}) {
const auto unsupported = ChooseDeviceDiagnostics(true, false, checkpointsSupported);
Require(!unsupported.config && !unsupported.checkpoints && unsupported.flags == 0, "diagnostics were chosen on a device without VK_NV_device_diagnostics_config");
}
const auto alone = ChooseDeviceDiagnostics(true, true, false);
Require(alone.config && !alone.checkpoints && alone.flags == all, "the diagnostics config without checkpoint support did not enable every crash-dump flag alone");
const auto both = ChooseDeviceDiagnostics(true, true, true);
Require(both.config && both.checkpoints && both.flags == all, "the diagnostics config did not add the diagnostic checkpoints the device supports");
}
void FaultInfo() {
faultCalls = 0;
faultFails = false;
@@ -139,6 +158,7 @@ void AddressKinds() {
int main() {
try {
Diagnostics();
FaultInfo();
FaultInfoFails();
Checkpoints();
+5 -1
View File
@@ -355,7 +355,11 @@ void testFailureLog() {
std::fflush(stderr);
const int saved = APS5_DUP(APS5_FILENO(stderr));
check(saved >= 0, "stderr could not be saved");
check(std::freopen(path.string().c_str(), "wb", stderr) != nullptr, "stderr could not be redirected");
#ifdef _WIN32
check(_wfreopen(path.wstring().c_str(), L"wb", stderr) != nullptr, "stderr could not be redirected");
#else
check(std::freopen(path.c_str(), "wb", stderr) != nullptr, "stderr could not be redirected");
#endif
std::string message;
std::string error;
try {
@@ -0,0 +1,238 @@
#include "prx/libSceAgcDriver/Execution/include/FrameCapture.hpp"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>
#ifdef _WIN32
#include <process.h>
#define getpid _getpid
#else
#include <unistd.h>
#endif
namespace {
using AgcDriver::FrameCall;
using AgcDriver::FrameCallKind;
using AgcDriver::FrameCaptureReader;
using AgcDriver::FrameCaptureWriter;
using AgcDriver::FrameMemoryChunk;
using AgcDriver::FrameMemoryKind;
using AgcDriver::FrameRangeKind;
void check(bool condition, const std::string& message) {
if (!condition) {
std::fprintf(stderr, "FAILED: %s\n", message.c_str());
std::exit(1);
}
}
template <typename TFunction>
void checkThrows(TFunction&& function, const std::string& message) {
try {
function();
} catch (const std::runtime_error&) {
return;
}
check(false, message);
}
bool sameRegisters(const std::vector<ShaderRegister>& left, const std::vector<ShaderRegister>& right) {
if (left.size() != right.size()) return false;
for (std::size_t index = 0; index < left.size(); ++index) {
if (left[index].offset != right[index].offset || left[index].value != right[index].value) return false;
}
return true;
}
bool sameCall(const FrameCall& left, const FrameCall& right) {
return left.kind == right.kind && left.ordinal == right.ordinal && left.timeNs == right.timeNs && left.thread == right.thread && left.accepted == right.accepted && left.completed == right.completed && left.queue == right.queue &&
left.flags == right.flags && left.cpuFlip == right.cpuFlip && left.address == right.address && left.size == right.size && left.values == right.values && sameRegisters(left.context, right.context) &&
sameRegisters(left.primitive, right.primitive);
}
std::vector<FrameCall> sampleCalls() {
std::vector<FrameCall> calls;
FrameCall shader;
shader.kind = FrameCallKind::RegisterShader;
shader.ordinal = 1;
shader.address = 0x2000010000;
calls.push_back(shader);
FrameCall abi;
abi.kind = FrameCallKind::ResolveGraphicsStagesAbi;
abi.ordinal = 2;
abi.values = {0x2000010000, 0x2000020000, 0};
abi.context = {{0x1a0, 7}, {0x1a1, 0xffffffffu}};
abi.primitive = {{0x242, 3}};
calls.push_back(abi);
FrameCall submit;
submit.kind = FrameCallKind::Submit;
submit.ordinal = 3;
submit.timeNs = 123456789;
submit.thread = 4242;
submit.accepted = 20;
submit.completed = 17;
submit.queue = 0x21;
submit.address = 0x2000030000;
submit.size = 4096;
calls.push_back(submit);
FrameCall flip;
flip.kind = FrameCallKind::Flip;
flip.ordinal = 4;
flip.cpuFlip = true;
flip.values = {1, 2, 0, static_cast<std::uint64_t>(-5)};
calls.push_back(flip);
FrameCall unregister;
unregister.kind = FrameCallKind::UnregisterVideoOutput;
unregister.ordinal = 5;
unregister.values = {1};
calls.push_back(unregister);
FrameCall end;
end.kind = FrameCallKind::End;
end.ordinal = 6;
calls.push_back(end);
return calls;
}
std::vector<std::byte> sampleMemory() {
constexpr std::size_t Block = FrameCaptureWriter::BlockBytes;
std::vector<std::byte> memory(Block * 150 + 4096);
for (std::size_t block = 0; block < 150; ++block) {
if (block % 3 == 1 || (block >= 10 && block < 90)) {
for (std::size_t at = 0; at < Block; at += 97) memory[block * Block + at] = static_cast<std::byte>((block * 131 + at) & 0xff);
}
}
memory[memory.size() - 1] = std::byte{0x5a};
return memory;
}
void truncateFile(const std::filesystem::path& path, std::uintmax_t removed) {
const auto size = std::filesystem::file_size(path);
std::filesystem::resize_file(path, size - removed);
}
void testRoundTrip(const std::filesystem::path& directory) {
constexpr std::uint64_t Base = 0x2000000000;
const auto calls = sampleCalls();
const auto memory = sampleMemory();
std::vector<std::byte> delta(8192);
for (std::size_t at = 0; at < delta.size(); ++at) delta[at] = static_cast<std::byte>(at * 7);
std::uint64_t stored = 0;
{
FrameCaptureWriter writer(directory);
writer.WriteRange(Base, memory.size(), FrameRangeKind::Watched, 3, AgcDriver::FrameRangeAccess::Readable | AgcDriver::FrameRangeAccess::Gpu);
stored = writer.WriteSnapshot(Base, memory);
for (const auto& call : calls) writer.WriteCall(call);
writer.WriteDelta(1, 17, Base + 0x3000, delta);
writer.Finish("{\"magic\":\"aps5-frame-capture 1\"}\n");
check(writer.RawBytes() == stored + delta.size(), "raw byte count does not add the snapshot and the delta");
}
std::size_t nonzero = 0;
for (std::size_t at = 0; at < memory.size(); at += FrameCaptureWriter::BlockBytes) {
const auto bytes = std::min(FrameCaptureWriter::BlockBytes, memory.size() - at);
if (!AgcDriver::AllZero(std::span(memory).subspan(at, bytes))) nonzero += bytes;
}
check(stored == nonzero, "the snapshot stored zero blocks or lost data blocks");
const FrameCaptureReader reader(directory);
check(!reader.Truncated(), "a finished capture reads as truncated");
check(reader.Header() == "{\"magic\":\"aps5-frame-capture 1\"}\n", "header does not round-trip");
check(reader.Calls().size() == calls.size(), "call count does not round-trip");
for (std::size_t index = 0; index < calls.size(); ++index) check(sameCall(reader.Calls()[index], calls[index]), "call " + std::to_string(index) + " does not round-trip");
std::vector<std::byte> restored(memory.size());
std::size_t ranges = 0, snapshots = 0, deltas = 0;
for (const auto& chunk : reader.Chunks()) {
if (chunk.kind == FrameMemoryKind::Range) {
++ranges;
check(chunk.address == Base && chunk.bytes == memory.size() && chunk.rangeKind == FrameRangeKind::Watched && chunk.protection == 3 &&
chunk.access == (AgcDriver::FrameRangeAccess::Readable | AgcDriver::FrameRangeAccess::Gpu), "range does not round-trip");
continue;
}
if (chunk.kind == FrameMemoryKind::Snapshot) {
++snapshots;
check(chunk.bytes <= FrameCaptureWriter::ChunkBytes, "snapshot chunk is larger than the chunk limit");
check(chunk.address >= Base && chunk.address - Base + chunk.bytes <= restored.size(), "snapshot chunk outside its range");
reader.Read(chunk, std::span(restored).subspan(chunk.address - Base, chunk.bytes));
continue;
}
++deltas;
check(chunk.group == 1 && chunk.completed == 17 && chunk.address == Base + 0x3000, "delta header does not round-trip");
check(reader.Read(chunk) == delta, "delta bytes do not round-trip");
}
check(ranges == 1 && deltas == 1 && snapshots >= 2, "unexpected chunk counts");
check(restored == memory, "snapshot does not restore the memory");
std::vector<std::byte> wrong(1);
checkThrows([&] { reader.Read(reader.Chunks()[1], wrong); }, "a read into a wrongly sized destination does not throw");
}
void testTruncated(const std::filesystem::path& source, const std::filesystem::path& directory) {
std::filesystem::remove_all(directory);
std::filesystem::copy(source, directory, std::filesystem::copy_options::recursive);
truncateFile(directory / FrameCaptureWriter::CallsName, 3);
truncateFile(directory / FrameCaptureWriter::MemoryName, 10);
std::filesystem::remove(directory / FrameCaptureWriter::HeaderName);
const FrameCaptureReader reader(directory);
const auto calls = sampleCalls();
check(reader.Truncated(), "a cut capture does not read as truncated");
check(reader.Calls().size() == calls.size() - 1, "a cut call record is not dropped");
check(reader.Chunks().back().kind != FrameMemoryKind::Delta, "a cut memory chunk is not dropped");
}
void testCorrupt(const std::filesystem::path& source, const std::filesystem::path& directory) {
std::filesystem::remove_all(directory);
std::filesystem::copy(source, directory, std::filesystem::copy_options::recursive);
{
std::fstream file(directory / FrameCaptureWriter::CallsName, std::ios::binary | std::ios::in | std::ios::out);
file.seekp(static_cast<std::streamoff>(FrameCaptureWriter::CallsMagic.size()));
const std::uint32_t kind = 99;
file.write(reinterpret_cast<const char*>(&kind), sizeof(kind));
}
checkThrows([&] { FrameCaptureReader reader(directory); }, "an unknown call kind does not throw");
std::ofstream(directory / FrameCaptureWriter::CallsName, std::ios::binary | std::ios::trunc) << "not a capture";
checkThrows([&] { FrameCaptureReader reader(directory); }, "a file without the magic does not throw");
}
void testWriter(const std::filesystem::path& directory) {
FrameCaptureWriter writer(directory);
writer.Finish("{}");
FrameCall call;
call.kind = FrameCallKind::Suspend;
checkThrows([&] { writer.WriteCall(call); }, "a call after Finish does not throw");
checkThrows([&] { writer.Finish("{}"); }, "a second Finish does not throw");
FrameCall unknown;
unknown.kind = static_cast<FrameCallKind>(77);
FrameCaptureWriter other(directory / "other");
checkThrows([&] { other.WriteCall(unknown); }, "an unknown call kind is written");
}
void testAllZero() {
std::vector<std::byte> bytes(37);
check(AgcDriver::AllZero(bytes), "zero bytes are not all zero");
check(AgcDriver::AllZero({}), "an empty span is not all zero");
bytes[36] = std::byte{1};
check(!AgcDriver::AllZero(bytes), "a nonzero tail byte is missed");
bytes[36] = std::byte{0};
bytes[3] = std::byte{1};
check(!AgcDriver::AllZero(bytes), "a nonzero word is missed");
}
}
int main() {
const auto root = std::filesystem::temp_directory_path() / ("aps5-frame-capture-test-" + std::to_string(getpid()));
std::filesystem::remove_all(root);
testAllZero();
testRoundTrip(root / "capture");
testTruncated(root / "capture", root / "truncated");
testCorrupt(root / "capture", root / "corrupt");
testWriter(root / "writer");
std::filesystem::remove_all(root);
std::puts("frame capture tests passed");
return 0;
}
@@ -0,0 +1,143 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/FrameCapture.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "SceTypes.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>
#ifdef _WIN32
#include <windows.h>
#endif
namespace {
using AgcDriver::FrameCallKind;
using AgcDriver::FrameCaptureReader;
constexpr std::uint32_t Handle = 7;
constexpr std::size_t BlockBytes = 1u << 20;
constexpr std::size_t Frames = 6;
constexpr std::size_t CapturedFrames = 3;
void check(bool condition, const std::string& reason) {
if (!condition) throw std::runtime_error(reason);
}
class Request final : public AgcDriver::IFlipRequest {
public:
explicit Request(std::atomic<int>& ready) : ready(ready) {}
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ++ready; }
void Fail(std::exception_ptr error) noexcept override {
if (!error) std::terminate();
}
private:
std::atomic<int>& ready;
};
class Output final : public AgcDriver::IVideoOutput {
public:
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared<Request>(ready); }
void Fail(std::exception_ptr error) noexcept override {
if (!error) std::terminate();
}
std::atomic<int> ready = 0;
};
std::byte* allocate() {
auto* const block = static_cast<std::byte*>(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536));
check(block != nullptr, "cannot allocate guest memory");
GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, 0x04, BlockBytes);
GuestAllocations::Mutation mutation;
mutation.Add(block, BlockBytes, true, true, true);
return block;
}
void submitEopFrame(std::byte* block, std::size_t frame) {
const auto token = AgcDriverReserveEopFlip_nid_postfix(AgcDriver::FlipInfo{Handle, 0, 1, static_cast<std::int64_t>(frame)});
check(token != 0, "the EOP flip of frame " + std::to_string(frame) + " was not reserved");
auto* const words = reinterpret_cast<std::uint32_t*>(block + frame * 256);
const auto label = reinterpret_cast<std::uintptr_t>(block + BlockBytes / 2 + frame * 8);
const std::array<std::uint32_t, 8> release{0xc0064900u, 0x06200504u, 0x42010000u, static_cast<std::uint32_t>(label), static_cast<std::uint32_t>(label >> 32u), 1u, 0u, token};
std::memcpy(words, release.data(), sizeof(release));
Packet packet{words, static_cast<std::uint32_t>(release.size()), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0, "the EOP submission of frame " + std::to_string(frame) + " failed");
}
void testEopFramesCountOnce() {
const char* directory = std::getenv("APS5_FRAME_CAPTURE");
check(directory != nullptr && *directory != '\0', "APS5_FRAME_CAPTURE is not set");
auto* const block = allocate();
auto output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(Handle, output);
for (std::size_t frame = 0; frame < Frames; ++frame) submitEopFrame(block, frame);
AgcDriverWaitIdle_nid_postfix();
check(output->ready.load() == static_cast<int>(Frames), "the driver presented " + std::to_string(output->ready.load()) + " of the EOP flips");
const FrameCaptureReader reader{std::filesystem::path(directory)};
check(!reader.Truncated(), "the capture is truncated");
check(reader.Header().find("\"frames\":" + std::to_string(CapturedFrames)) != std::string::npos, "the capture header does not record " + std::to_string(CapturedFrames) + " frames: " + reader.Header());
const auto& calls = reader.Calls();
const auto start = std::find_if(calls.begin(), calls.end(), [](const auto& call) { return call.kind == FrameCallKind::FrameStart; });
check(start != calls.end(), "the capture has no frame start");
std::size_t submits = 0, reservations = 0;
FrameCallKind first = FrameCallKind::End;
for (auto call = start + 1; call != calls.end(); ++call) {
if (call->kind == FrameCallKind::Submit || call->kind == FrameCallKind::ReserveEopFlip) {
if (first == FrameCallKind::End) first = call->kind;
submits += call->kind == FrameCallKind::Submit;
reservations += call->kind == FrameCallKind::ReserveEopFlip;
}
}
check(first == FrameCallKind::ReserveEopFlip, "the capture started between a reservation and the submission that flips it");
check(submits == CapturedFrames && reservations == CapturedFrames,
"the captured frames hold " + std::to_string(submits) + " submissions and " + std::to_string(reservations) + " reservations, expected " + std::to_string(CapturedFrames) + " of each");
}
void testFlipsCountPerThread() {
auto* const block = allocate();
const auto& driver = AgcDriver::DriverDetail::Driver::Get();
const auto before = driver.FlipSubmissions();
std::uint64_t otherBefore = 0, otherAfter = 0;
std::exception_ptr failure;
std::thread submitter([&] {
try {
otherBefore = driver.FlipSubmissions();
submitEopFrame(block, 0);
otherAfter = driver.FlipSubmissions();
} catch (...) {
failure = std::current_exception();
}
});
submitter.join();
if (failure) std::rethrow_exception(failure);
AgcDriverWaitIdle_nid_postfix();
check(otherAfter == otherBefore + 1, "the flipping submission did not count on the thread that made it");
check(driver.FlipSubmissions() == before, "a flip submitted on another thread changed this thread's flip count, so a submission of this thread that overlaps it would end a captured frame");
}
}
int main() {
try {
testEopFramesCountOnce();
testFlipsCountPerThread();
} catch (const std::exception& error) {
std::fprintf(stderr, "FAILED: %s\n", error.what());
return 1;
}
std::puts("frame capture EOP tests passed");
return 0;
}
@@ -120,13 +120,15 @@ void DisableAfterFailure() {
AgcDriver::FrameDumpOutput output;
const std::array<std::byte, 16> pixels{};
Check(output.Enabled_nid_no_patch(), "BMP output was disabled before any failure");
const auto blocked = directory.string();
const auto previous = std::filesystem::current_path();
std::filesystem::current_path(directory.parent_path());
const auto blocked = directory.filename().string();
Check(!output.Write_nid_no_patch(blocked.c_str(), 2, 2, pixels), "BMP output accepted a directory as a file");
Check(!output.Enabled_nid_no_patch(), "failed BMP output remained enabled");
const auto skipped = directory / "must-not-exist.bmp";
const auto path = skipped.string();
const auto path = (directory.filename() / "must-not-exist.bmp").string();
for (unsigned i = 0; i < 64; ++i) Check(!output.Write_nid_no_patch(path.c_str(), 2, 2, pixels), "disabled BMP output wrote another file");
Check(!std::filesystem::exists(skipped), "disabled BMP output created a file");
std::filesystem::current_path(previous);
Check(!std::filesystem::exists(directory / "must-not-exist.bmp"), "disabled BMP output created a file");
Check(std::filesystem::remove(directory), "cannot remove empty fixture directory");
}
@@ -0,0 +1,209 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/FrameCapture.hpp"
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "SceTypes.hpp"
#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#endif
extern "C" {
std::uint32_t* APS5_VABI sceAgcDcbWriteData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t);
std::uint32_t* APS5_VABI sceAgcDcbDmaData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t, std::uint8_t, std::uint64_t, std::uint32_t, std::uint8_t, std::uint8_t, std::uint8_t);
}
namespace {
using AgcDriver::FrameCallKind;
using AgcDriver::FrameCaptureReader;
using AgcDriver::FrameMemoryKind;
constexpr std::uint32_t Handle = 7;
constexpr std::uint8_t GraphicsMemory = 0x0a;
constexpr std::uint8_t DmaMemory = 0;
constexpr std::size_t BlockBytes = 1u << 20;
constexpr std::size_t CommandBytes = 64u * 1024u;
constexpr std::size_t DataBytes = 4096;
constexpr std::uint32_t Done = 1;
void check(bool condition, const std::string& reason) {
if (!condition) throw std::runtime_error(reason);
}
std::uint64_t at(const volatile void* pointer) {
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(pointer));
}
class Request final : public AgcDriver::IFlipRequest {
public:
explicit Request(std::atomic<int>& ready) : ready(ready) {}
void GpuReady(const std::shared_ptr<AgcDriver::FrameTiming>&) override { ++ready; }
void Fail(std::exception_ptr error) noexcept override {
if (!error) std::terminate();
}
private:
std::atomic<int>& ready;
};
class Output final : public AgcDriver::IVideoOutput {
public:
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared<Request>(ready); }
void Fail(std::exception_ptr error) noexcept override {
if (!error) std::terminate();
}
std::atomic<int> ready = 0;
};
struct Layout {
std::byte* base = nullptr;
std::uint32_t* firstCommands = nullptr;
std::uint32_t* secondCommands = nullptr;
std::byte* source = nullptr;
std::byte* firstTarget = nullptr;
std::byte* secondTarget = nullptr;
std::uint32_t* label = nullptr;
};
Layout allocate() {
auto* const block = static_cast<std::byte*>(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536));
check(block != nullptr, "cannot allocate guest memory");
GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, 0x04, BlockBytes);
{
GuestAllocations::Mutation mutation;
mutation.Add(block, BlockBytes, true, true, true);
}
Layout layout;
layout.base = block;
layout.firstCommands = reinterpret_cast<std::uint32_t*>(block);
layout.secondCommands = reinterpret_cast<std::uint32_t*>(block + CommandBytes);
layout.source = block + 2 * CommandBytes;
layout.firstTarget = layout.source + DataBytes;
layout.secondTarget = layout.firstTarget + DataBytes;
layout.label = reinterpret_cast<std::uint32_t*>(layout.secondTarget + DataBytes);
return layout;
}
std::vector<std::byte> pattern(std::uint8_t seed) {
std::vector<std::byte> bytes(DataBytes);
for (std::size_t index = 0; index < bytes.size(); ++index) bytes[index] = static_cast<std::byte>((index * 31 + seed) & 0xff);
return bytes;
}
void copyFrame(std::uint32_t* words, const Layout& layout, std::byte* target) {
CommandBuffer buffer{words, words + CommandBytes / sizeof(std::uint32_t), words, words + CommandBytes / sizeof(std::uint32_t), nullptr, nullptr, 0};
sceAgcDcbDmaData(&buffer, 0, DmaMemory, 0, at(target), DmaMemory, 0, at(layout.source), DataBytes, 0, 1, 0);
sceAgcDcbWriteData(&buffer, GraphicsMemory, 0, at(layout.label), &Done, 1, 0, 1);
Packet packet{words, static_cast<std::uint32_t>(buffer.cursor_up - words), 0, {}};
check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed");
}
void flip() {
AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{Handle, -1, 1, 0});
}
std::vector<std::byte> readFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
check(file.is_open(), "cannot open " + path.string());
std::vector<char> bytes((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
std::vector<std::byte> result(bytes.size());
std::memcpy(result.data(), bytes.data(), bytes.size());
return result;
}
bool equal(const std::byte* bytes, const std::vector<std::byte>& expected) {
return std::memcmp(bytes, expected.data(), expected.size()) == 0;
}
void testCaptureAndReplay(const std::filesystem::path& tool) {
const char* directory = std::getenv("APS5_FRAME_CAPTURE");
check(directory != nullptr && *directory != '\0', "APS5_FRAME_CAPTURE is not set");
const std::filesystem::path capture(directory);
const auto layout = allocate();
const auto first = pattern(3);
const auto second = pattern(101);
auto output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(Handle, output);
AgcDriverUnregisterVideoOutput_nid_postfix(Handle, output);
output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(Handle, output);
flip();
std::memcpy(layout.source, first.data(), first.size());
copyFrame(layout.firstCommands, layout, layout.firstTarget);
flip();
AgcDriverWaitIdle_nid_postfix();
AgcDriverUnregisterVideoOutput_nid_postfix(Handle, output);
output = std::make_shared<Output>();
AgcDriverRegisterVideoOutput_nid_postfix(Handle, output);
const auto late = allocate();
std::memcpy(late.source, second.data(), second.size());
copyFrame(late.firstCommands, late, late.firstTarget);
flip();
AgcDriverWaitIdle_nid_postfix();
check(equal(layout.firstTarget, first) && equal(late.firstTarget, second), "the driver did not run the copies while capturing");
const FrameCaptureReader reader(capture);
check(!reader.Truncated(), "the capture is truncated");
check(reader.Header().find("\"frames\":2") != std::string::npos, "the capture header does not record two frames: " + reader.Header());
std::size_t submits = 0, flips = 0, memory = 0, starts = 0, registrations = 0, unregistrations = 0;
for (const auto& call : reader.Calls()) {
submits += call.kind == FrameCallKind::Submit;
flips += call.kind == FrameCallKind::Flip;
memory += call.kind == FrameCallKind::Memory;
starts += call.kind == FrameCallKind::FrameStart;
registrations += call.kind == FrameCallKind::RegisterVideoOutput;
unregistrations += call.kind == FrameCallKind::UnregisterVideoOutput;
}
check(starts == 1 && submits == 2 && flips == 2, "the capture holds " + std::to_string(submits) + " submits and " + std::to_string(flips) + " flips after " + std::to_string(starts) + " frame starts");
check(memory >= 2, "the CPU writes before each copy were not recorded as memory deltas");
check(registrations == 3 && unregistrations == 2, "video output close and reopen was not recorded before and during capture");
const auto recorded = [&](const std::byte* base) {
return std::any_of(reader.Chunks().begin(), reader.Chunks().end(), [&](const auto& chunk) {
return chunk.kind == FrameMemoryKind::Range && chunk.address <= at(base) && at(base) + BlockBytes <= chunk.address + chunk.bytes;
});
};
check(recorded(layout.base), "the guest block is not in the snapshot ranges");
check(recorded(late.base), "the block committed during the capture is not in its ranges");
check(reader.Header().find("\"late_ranges\":1") != std::string::npos, "the capture header does not count one range committed during the capture: " + reader.Header());
const auto firstDump = capture / "first.bin";
const auto secondDump = capture / "second.bin";
const auto command = "\"\"" + tool.string() + "\" \"" + capture.string() + "\" --strict --dump " + std::to_string(at(layout.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + firstDump.string() +
"\" --dump " + std::to_string(at(late.firstTarget)) + ":" + std::to_string(DataBytes) + ":\"" + secondDump.string() + "\"\"";
const auto code = std::system(command.c_str());
check(code == 0, "agc_frame_replay exited with " + std::to_string(code));
check(readFile(firstDump) == first, "the replay did not reproduce the first copy");
check(readFile(secondDump) == second, "the replay did not reproduce the second copy");
}
}
int main(int argc, char** argv) {
if (argc != 2) {
std::fprintf(stderr, "usage: agc_driver_frame_replay_tests <agc_frame_replay>\n");
return 2;
}
try {
testCaptureAndReplay(argv[1]);
} catch (const std::exception& error) {
std::fprintf(stderr, "FAILED: %s\n", error.what());
return 1;
}
std::puts("frame replay tests passed");
return 0;
}
@@ -0,0 +1,241 @@
#define main frameReplayToolMain
#include "../tools/FrameReplay.cpp"
#undef main
#include "prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp"
#include <barrier>
namespace {
using AgcDriver::FrameCaptureWriter;
void check(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(reason);
}
std::uint64_t address(const void* pointer) {
return reinterpret_cast<std::uintptr_t>(pointer);
}
std::byte* allocate(std::size_t size, bool gpu = true) {
auto* bytes = static_cast<std::byte*>(GuestArena::GuestArenaAllocate_nid_postfix(size, 65536));
check(bytes != nullptr, "guest allocation failed");
GuestArena::GuestArenaCommit_nid_postfix(bytes, size, 0x04, size);
GuestAllocations::Mutation mutation;
mutation.Add(bytes, size, true, true, gpu);
return bytes;
}
void collectGroups(Replay& replay) {
for (const auto& chunk : replay.reader.Chunks()) {
if (chunk.kind == FrameMemoryKind::Delta) replay.groups[chunk.group].push_back(&chunk);
}
}
FrameCall memoryCall(std::uint64_t group, std::uint64_t thread) {
FrameCall call;
call.kind = FrameCallKind::Memory;
call.values = {group};
call.thread = thread;
return call;
}
void testMemoryOrder(const std::filesystem::path& directory) {
constexpr std::size_t Size = 64u * 1024u * 1024u;
auto* bytes = allocate(Size);
const std::vector<std::byte> first(Size, std::byte{0x11});
const std::vector<std::byte> second(4096, std::byte{0x22});
FrameCaptureWriter writer(directory);
writer.WriteDelta(1, 0, address(bytes), first);
writer.WriteDelta(2, 0, address(bytes + Size - second.size()), second);
writer.Finish("{}");
const FrameCaptureReader reader(directory);
Options options;
options.release = false;
Replay replay{options, reader};
replay.recording = true;
collectGroups(replay);
const std::array calls{memoryCall(1, 1), memoryCall(2, 2)};
replay.runWindow(calls);
check(std::all_of(bytes, bytes + Size - second.size(), [](std::byte value) { return value == std::byte{0x11}; }), "first delta was not restored");
check(std::equal(second.begin(), second.end(), bytes + Size - second.size()), "an earlier delta overwrote the later thread's delta");
}
void testReusedMemory(const std::filesystem::path& directory) {
#ifdef _WIN32
constexpr std::size_t Size = 2 * FrameCaptureWriter::BlockBytes;
auto* bytes = allocate(Size);
auto* cpu = allocate(FrameCaptureWriter::BlockBytes, false);
std::fill_n(bytes, Size, std::byte{0x7f});
std::fill_n(cpu, FrameCaptureWriter::BlockBytes, std::byte{0x55});
std::vector<std::byte> expected(Size);
std::fill(expected.begin() + FrameCaptureWriter::BlockBytes, expected.end(), std::byte{0x22});
std::uintptr_t base = 0;
std::size_t arenaSize = 0;
GuestArena::GuestArenaRange_nid_postfix(&base, &arenaSize);
FrameCaptureWriter writer(directory);
writer.WriteRange(address(bytes), Size, FrameRangeKind::Watched, PAGE_READWRITE, FrameRangeAccess::Readable | FrameRangeAccess::Writable | FrameRangeAccess::Gpu);
writer.WriteRange(address(cpu), FrameCaptureWriter::BlockBytes, FrameRangeKind::Unwatched, PAGE_READWRITE, FrameRangeAccess::Readable | FrameRangeAccess::Writable);
check(writer.WriteSnapshot(address(bytes), expected) == FrameCaptureWriter::BlockBytes, "zero snapshot block was not elided");
writer.Finish("{\"arena_base\":" + std::to_string(base) + "}");
const FrameCaptureReader reader(directory);
const auto skipped = restoreMemory(reader);
check(std::equal(expected.begin(), expected.end(), bytes), "reused GPU memory did not restore zero and nonzero snapshot blocks");
check(skipped.bytes == FrameCaptureWriter::BlockBytes, "unexpected skipped memory");
check(std::all_of(cpu, cpu + FrameCaptureWriter::BlockBytes, [](std::byte value) { return value == std::byte{0x55}; }), "reused CPU memory was overwritten");
#endif
}
void testEopTokens() {
Frames frames;
auto output = std::make_shared<ReplayOutput>(frames);
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
std::barrier together(2);
std::atomic<int> wrong{0};
const auto worker = [&](int lane) {
for (int round = 0; round < 2000; ++round) {
const auto requested = AgcDriver::EopFlipTokenBase + round * 8 + lane * 2;
together.arrive_and_wait();
try {
if (AgcDriverReserveEopFlipAt_nid_postfix({7, -1, 1, 0}, requested) != requested) ++wrong;
} catch (const std::exception&) { ++wrong; }
}
};
std::thread first(worker, 0), second(worker, 1);
first.join();
second.join();
check(wrong.load() == 0, "concurrent explicit EOP reservations returned the wrong token");
const auto high = AgcDriver::EopFlipTokenBase + 20000;
check(AgcDriverReserveEopFlipAt_nid_postfix({7, -1, 1, 0}, high) == high, "explicit high token was not reserved");
check(AgcDriverReserveEopFlipAt_nid_postfix({7, -1, 1, 0}, high - 1) == high - 1, "explicit lower token was not reserved");
check(AgcDriverReserveEopFlip_nid_postfix({7, -1, 1, 0}) == high + 1, "ordinary reservation reused an explicit token");
bool rejected = false;
try { AgcDriverReserveEopFlipAt_nid_postfix({7, -1, 1, 0}, high); }
catch (const std::runtime_error&) { rejected = true; }
check(rejected, "duplicate explicit token was not rejected");
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
}
class WaitingOutput final : public AgcDriver::IVideoOutput {
public:
WaitingOutput(Frames& frames, const std::uint32_t* label) : frames(frames), label(label) {}
std::shared_ptr<AgcDriver::IFlipRequest> Reserve(const AgcDriver::FlipInfo&) override {
const auto deadline = Clock::now() + std::chrono::seconds(2);
while (*static_cast<const volatile std::uint32_t*>(label) == 0) {
check(Clock::now() < deadline, "a blocking driver call prevented the later memory update");
std::this_thread::yield();
}
return std::make_shared<ReplayFlip>(frames);
}
void Fail(std::exception_ptr error) noexcept override { frames.Failed(error); }
private:
Frames& frames;
const std::uint32_t* label;
};
void testBlockingSubmission(const std::filesystem::path& directory) {
auto* bytes = allocate(FrameCaptureWriter::BlockBytes);
auto* label = reinterpret_cast<std::uint32_t*>(bytes);
const std::uint32_t ready = 1;
FrameCaptureWriter writer(directory);
writer.WriteDelta(1, 0, address(label), std::as_bytes(std::span(&ready, 1)));
writer.Finish("{}");
const FrameCaptureReader reader(directory);
Options options;
options.pace = false;
options.release = false;
Replay replay{options, reader};
collectGroups(replay);
auto output = std::make_shared<WaitingOutput>(replay.frames, label);
AgcDriverRegisterVideoOutput_nid_postfix(7, output);
std::array<std::uint32_t, AgcDriver::FlipPacketWords> words{AgcDriver::FlipPacketHeader, 7, 0xffffffffu, 1, 0, 0};
for (const auto kind : {FrameCallKind::Submit, FrameCallKind::Flip, FrameCallKind::ReserveEopFlip}) {
*label = 0;
FrameCall submit;
submit.kind = kind;
submit.thread = 1;
submit.address = address(words.data());
submit.size = words.size();
submit.values = {7, static_cast<std::uint64_t>(-1), 1, 0, AgcDriver::EopFlipTokenBase};
const std::array calls{submit, memoryCall(1, 2)};
replay.runWindow(calls);
check(*label == ready && replay.unfinished == 0, "blocking submission did not complete");
}
check(replay.frames.Times().size() == 2, "blocking submissions lost a flip");
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
}
void testShaderCode(const std::filesystem::path& directory, bool host) {
auto* bytes = allocate(1u << 20);
auto* shader = reinterpret_cast<Shader*>(bytes);
auto* code = bytes + (1u << 19) + 256;
constexpr std::size_t CodeBytes = 3 * FrameCaptureWriter::BlockBytes;
#ifdef _WIN32
if (host) {
auto* memory = static_cast<std::byte*>(VirtualAlloc(nullptr, CodeBytes + 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
check(memory != nullptr, "host shader allocation failed");
GuestAllocations::Mutation mutation;
mutation.Add(memory, CodeBytes + 4096, true, true, true);
code = memory + 256;
}
#endif
*shader = {};
shader->file_header = 0x34333231u;
shader->version = 0x18u;
shader->header_size = sizeof(Shader);
shader->shader_size = CodeBytes;
shader->code = code;
auto* recorder = AgcDriver::FrameRecorder::Active();
check(recorder != nullptr, "frame recorder is disabled");
recorder->FrameBoundary();
std::vector<std::byte> expected(CodeBytes);
for (std::size_t i = 0; i < expected.size(); ++i) expected[i] = static_cast<std::byte>(i * 31 + 17);
std::memcpy(code, expected.data(), expected.size());
recorder->RegisterShader(shader);
recorder->FrameBoundary();
const FrameCaptureReader reader(directory);
std::vector<std::byte> captured(CodeBytes);
const auto apply = [&](const FrameMemoryChunk& chunk) {
const auto begin = std::max(address(code), chunk.address);
const auto end = std::min(address(code) + CodeBytes, chunk.address + chunk.bytes);
if (begin >= end) return;
const auto data = reader.Read(chunk);
std::memcpy(captured.data() + begin - address(code), data.data() + begin - chunk.address, end - begin);
};
for (const auto& chunk : reader.Chunks()) if (chunk.kind == FrameMemoryKind::Snapshot) apply(chunk);
bool registered = false;
for (const auto& call : reader.Calls()) {
if (call.kind == FrameCallKind::RegisterShader) {
registered = true;
break;
}
if (call.kind != FrameCallKind::Memory) continue;
for (const auto& chunk : reader.Chunks()) {
if (chunk.kind == FrameMemoryKind::Delta && chunk.group == call.values.at(0)) apply(chunk);
}
}
check(registered && captured == expected, "the complete separate shader code was not captured before registration");
}
}
int main(int argc, char** argv) {
try {
check(argc == 3, "expected a test name and capture directory");
const std::string mode = argv[1];
const std::filesystem::path directory = argv[2];
if (mode == "order") testMemoryOrder(directory);
else if (mode == "zero") testReusedMemory(directory);
else if (mode == "tokens") testEopTokens();
else if (mode == "blocking") testBlockingSubmission(directory);
else if (mode == "shader" || mode == "shader_host") testShaderCode(directory, mode == "shader_host");
else throw std::runtime_error("unknown test");
AgcDriverShutdown_nid_postfix();
std::puts("frame replay state test passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
try { AgcDriverShutdown_nid_postfix(); } catch (...) {}
return 1;
}
}
+158 -43
View File
@@ -10,7 +10,6 @@
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "ShaderDiskCache.hpp"
#include "ShaderCacheDirectory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
@@ -824,7 +823,7 @@ void msaaEnableClearOneSampleTargetTests() {
queue = drawC();
queue.context[0x1b3] |= ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
requireOneSample(queue, "draw C with an enabled but unaddressed ANCILLARY input");
constexpr std::string_view refused = "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with PS_ITER_SAMPLE, a sample index or coverage input, or a sample-mask export";
constexpr std::string_view refused = "a one-sample color target under a multisampled rasterizer without MSAA_ENABLE is unsupported with a sample index or coverage input, or a sample-mask export";
for (const auto input : {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) {
queue = drawC();
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(input);
@@ -832,6 +831,10 @@ void msaaEnableClearOneSampleTargetTests() {
}
queue = drawC();
queue.context[0x293] = 0x10000u;
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample);
requireOneSample(queue, "draw C with PS_ITER_SAMPLE and a sample-interpolated input");
Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE into a one-sample color target without MSAA_ENABLE asked for sample shading");
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
expectFailure([&] { DecodeState(queue); }, refused);
queue = drawC();
queue.context[0x1c4] = 9;
@@ -906,44 +909,25 @@ void msaaEnableClearOneSampleTargetTests() {
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage);
Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "the depth-only draw did not skip its pixel program");
requireOneSampleDepth(queue, 0, "a one-sample depth target whose skipped pixel program has PS_ITER_SAMPLE and sample index and coverage inputs");
constexpr std::string_view depthRefused = "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA source Z interpolation or overrasterization";
for (const bool color : {false, true}) {
for (const auto input : {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) {
queue = depthTarget(color);
queue.context[0x203] |= 0x40u;
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(input);
expectFailure([&] { DecodeState(queue); }, depthRefused);
}
queue = depthTarget(color);
queue.context[0x203] |= 0x40u;
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "the ANCILLARY draw skipped its pixel program");
requireOneSampleDepth(queue, color ? 1u : 0u, "a one-sample depth target whose running pixel program reads ANCILLARY without PS_ITER_SAMPLE");
queue.context[0x1b3] = queue.context[0x1b4] = 0x2b01u;
requireOneSampleDepth(queue, color ? 1u : 0u, "a one-sample depth target whose running pixel program reads PERSP_SAMPLE, the position and ANCILLARY without PS_ITER_SAMPLE");
queue = depthTarget(color);
queue.context[0x203] |= 0x40u;
queue.context[0x293] = 0x10000u;
Require(!AgcDriver::Graphics::PixelProgramSkipped(queue), "the PS_ITER_SAMPLE draw skipped its pixel program");
requireOneSampleDepth(queue, color ? 1u : 0u, "a one-sample depth target whose running pixel program has PS_ITER_SAMPLE and no sample index or coverage input");
Require(!DecodeState(queue).perSampleShading, "a PS_ITER_SAMPLE program drawn as one sample still shades per sample");
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
expectFailure([&] { DecodeState(queue); }, depthRefused);
queue = depthTarget(color);
queue.context[0x1c4] = 9;
queue.context[0x203] |= 0x100u;
expectFailure([&] { DecodeState(queue); }, depthRefused);
queue = depthTarget(color);
queue.context[0x2dc] = 0x0000aa01u;
expectFailure([&] { DecodeState(queue); }, depthRefused);
for (const auto primitive : {2u, 3u}) {
queue = depthTarget(color);
queue.userConfig[0x242] = primitive;
expectFailure([&] { DecodeState(queue); }, depthRefused);
}
for (const auto eqaa : {0x00040000u, 0x00050000u}) {
queue = depthTarget(color);
queue.context[0x201] = eqaa;
requireOneSampleDepth(queue, color ? 1u : 0u, "a one-sample depth target under a 4x PA_SC_AA_CONFIG without MSAA_ENABLE with DB_EQAA INTERPOLATE_COMP_Z");
}
for (const auto eqaa : {0x00090002u, 0x000d0000u, 0x01010002u, 0x08010002u}) {
queue = depthTarget(color);
queue.context[0x201] = eqaa;
expectFailure([&] { DecodeState(queue); }, depthRefused);
}
}
queue = depthTarget(true);
queue.context[0x31d] = 0x12000;
@@ -1941,8 +1925,35 @@ void TuningFieldTests() {
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "DISABLE_DUAL_QUAD was rejected");
Require(state.hasColorTarget && state.blend.colorWriteMask == baseline.blend.colorWriteMask, "DISABLE_DUAL_QUAD changed color output");
queue.context[0x202] = 0xcc0013;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "copy ROP");
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "source or brush form");
queue.context[0x202] = 0x5b0010;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "source or brush form");
queue.context[0x202] = 0x5a0010;
state = AgcDriver::Graphics::DecodeState(queue);
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an XOR ROP was rejected");
Require(state.hasColorTarget && state.colorRop == 0x5a && AgcDriver::Graphics::LogicOpFromRop(state.colorRop) == VK_LOGIC_OP_XOR, "an XOR ROP did not decode");
queue.context[0x202] = 0xcc0010;
Require(AgcDriver::Graphics::DecodeState(queue).colorRop == AgcDriver::Graphics::CopyRop, "the copy ROP did not decode as the copy operation");
const std::array<std::pair<std::uint8_t, VkLogicOp>, 14> ropOperations{{
{0x0f, VK_LOGIC_OP_COPY_INVERTED}, {0xa0, VK_LOGIC_OP_AND}, {0xf0, VK_LOGIC_OP_COPY}, {0xfa, VK_LOGIC_OP_OR},
{0x00, VK_LOGIC_OP_CLEAR}, {0x11, VK_LOGIC_OP_NOR}, {0x33, VK_LOGIC_OP_COPY_INVERTED}, {0x55, VK_LOGIC_OP_INVERT}, {0x66, VK_LOGIC_OP_XOR},
{0x88, VK_LOGIC_OP_AND}, {0xaa, VK_LOGIC_OP_NO_OP}, {0xcc, VK_LOGIC_OP_COPY}, {0xee, VK_LOGIC_OP_OR}, {0xff, VK_LOGIC_OP_SET}}};
for (const auto& [rop, operation] : ropOperations) Require(AgcDriver::Graphics::LogicOpFromRop(rop) == operation, "a ROP3 code mapped to the wrong logic operation");
std::uint32_t documentedRops = 0;
for (std::uint32_t rop = 0; rop < 256u; ++rop) {
try {
static_cast<void>(AgcDriver::Graphics::LogicOpFromRop(static_cast<std::uint8_t>(rop)));
++documentedRops;
} catch (const std::runtime_error&) {
}
}
Require(documentedRops == 28u, "a ROP3 code outside the source and brush forms was accepted, or one inside them was refused");
auto blendedLogic = queue;
blendedLogic.context[0x202] = 0x5a0010;
blendedLogic.context[0x1e0] = 0x40000101u;
for (std::uint32_t index = 0; index < 4u; ++index) blendedLogic.context[0x105 + index] = 0u;
blendedLogic.context[0x31c] |= 0x8000u;
expectFailure([&] { AgcDriver::Graphics::DecodeState(blendedLogic); }, "logic operation together with blending");
for (const auto groups : {1u, 2u, 15u}) {
queue.context[0x2d5] = 0x2000u | (groups << 15u);
state = AgcDriver::Graphics::DecodeState(queue);
@@ -2982,7 +2993,12 @@ void depthCopyPassTests() {
queue.context[0x31c] = 0x40710;
queue.context[0x8e] = 1;
auto pass = DecodeDepthCopyPass(queue);
Require(pass.has_value() && pass->depthAddress == 0x100000 && pass->samples == 2 && pass->sample == 0 && pass->extent.width == 64 && pass->extent.height == 4, "a centroid DB depth copy of a two-sample depth plane decoded differently");
Require(pass.has_value() && pass->source.address == 0x100000 && pass->source.samples == 2 && pass->sample == 0 && pass->source.extent.width == 64 && pass->source.extent.height == 4, "a centroid DB depth copy of a two-sample depth plane decoded differently");
{
const auto words = AgcDriver::Graphics::DepthCopySourceWords(pass->source);
const auto resource = AgcDriver::Graphics::DecodeTextureResource(words);
Require(resource.baseAddress == pass->source.address && resource.dimension == AgcDriver::Graphics::TextureDimension::k2DMsaa && resource.samples == 2 && resource.width == 64 && resource.height == 4 && resource.tileMode == AgcDriver::Graphics::TextureTileMode::kZ64KBX && resource.pipeBankXor == pass->source.pipeBankXor && resource.dccAddress == pass->source.htileAddress, "the DB depth copy source descriptor of a two-sample plane decoded differently");
}
Require(pass->color.format == VK_FORMAT_R32_SFLOAT && pass->color.samples == 1 && pass->color.address == reinterpret_cast<std::uintptr_t>(colorMemory.data()), "the DB depth copy target decoded differently");
queue.context[0x000] = 0x104;
Require(DecodeDepthCopyPass(queue)->sample == 1, "COPY_SAMPLE without COPY_CENTROID did not select the sample");
@@ -3007,23 +3023,31 @@ void depthCopyPassTests() {
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "single-sample R32 float");
queue.context[0x31c] = 0x40710;
queue.context[0x10f] = std::bit_cast<std::uint32_t>(16.0f);
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "over part of the depth surface");
Require(DecodeDepthCopyPass(queue).has_value(), "a DB depth copy whose viewport covers part of the depth surface was refused");
queue.context[0x10f] = std::bit_cast<std::uint32_t>(32.0f);
queue.context[0x1e0] = 1u << 30u;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "blending");
queue.context[0x1e0] = 0;
queue.context[0x010] = 0x80001187;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "mipmapped or partially resident");
queue.context[0x010] = 0x80000187;
queue.context[0x012] = 0x00001003;
pass = DecodeDepthCopyPass(queue);
Require(pass.has_value() && pass->depthAddress == 0x100000, "a DB depth copy kept the pipe-bank XOR of a two-sample depth plane in its address");
Require(pass.has_value() && pass->source.address == 0x100000 && pass->source.pipeBankXor == 0x300, "a DB depth copy kept the pipe-bank XOR of a two-sample depth plane in its address");
queue.context[0x012] = 0x00001000;
queue.context[0x002] = 1u | (1u << 13u);
pass = DecodeDepthCopyPass(queue);
Require(pass.has_value() && pass->depthAddress == 0x100000 + AgcDriver::Graphics::DepthSliceBytes({64, 4}, 4u), "a DB depth copy of slice 1 did not address the slice the depth surface registers");
Require(pass.has_value() && pass->source.address == 0x100000 + AgcDriver::Graphics::DepthSliceBytes({64, 4}, 4u), "a DB depth copy of slice 1 did not address the slice the depth surface registers");
queue.context[0x002] = 0;
Require(DecodeDepthCopyPass(queue).has_value(), "the restored DB depth copy was refused");
queue.context[0x010] = 0x80000183;
pass = DecodeDepthCopyPass(queue);
Require(pass.has_value() && pass->samples == 1 && pass->sample == 0, "a centroid DB depth copy of a one-sample depth plane decoded differently");
Require(pass.has_value() && pass->source.samples == 1 && pass->sample == 0, "a centroid DB depth copy of a one-sample depth plane decoded differently");
{
const auto words = AgcDriver::Graphics::DepthCopySourceWords(pass->source);
const auto resource = AgcDriver::Graphics::DecodeTextureResource(words);
Require(resource.baseAddress == pass->source.address && resource.dimension == AgcDriver::Graphics::TextureDimension::k2D && resource.samples == 1 && resource.width == 64 && resource.height == 4 && resource.dccAddress == 0, "the DB depth copy source descriptor of a one-sample plane decoded differently");
}
queue.context[0x000] = 0x104;
expectFailure([&] { (void)DecodeDepthCopyPass(queue); }, "a sample the depth plane does not have");
}
@@ -3847,7 +3871,7 @@ void multisampleTests() {
}
queue = multisampled();
queue.context[0x31d] = 0x1000;
expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples");
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
queue.context[0x31d] = 0;
queue.context[0x293] = 0x10000u;
Require(DecodeState(queue).samples == 1 && !DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE into a one-sample target did not rasterize sample 0 alone");
@@ -4343,6 +4367,53 @@ void multisampledDepthTests() {
queue = truncatedTwoSample();
queue.context[0x31d] = 0x12000;
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
const auto oneFragment = [&] {
auto queue = singleSampleScan();
queue.context[0x2f8] = 0x00210002;
queue.context[0x292] = 3;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae;
queue.context[0x31d] = 0x2000;
queue.context[0x201] = 0x112202;
return queue;
};
queue = oneFragment();
state = DecodeState(queue);
Require(state.samples == 4 && !state.customSampleLocations && state.depth && state.depth->samples == 4 && state.colors.size() == 1 && state.colors[0].samples == 1 && state.truncatedCoverage && !state.mergedCoverage && !state.perSampleShading, "a 4-sample color target with one fragment beside a 4x depth target (PPSA02015's registers) did not keep sample 0's color in its one fragment");
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a 4-sample color target with one fragment beside a 4x depth target");
queue = oneFragment();
queue.context[0x31c] |= 0x4000u;
queue.context[0x321] = 0x00c3a1b2u;
queue.context[0x3a0] = 0x01u;
state = DecodeState(queue);
Require(state.colors.size() == 1 && state.colors[0].fmaskAddress == ((1ull << 40u) | 0xc3a10000ull), "a compressed one-fragment color target did not decode its FMASK address");
queue.context[0x321] = 0u;
queue.context[0x3a0] = 0u;
expectFailure([&] { DecodeState(queue); }, "a compressed one-fragment color target without an FMASK address is unsupported");
queue.context[0x321] = 0xa5u;
expectFailure([&] { DecodeState(queue); }, "a compressed one-fragment color target without an FMASK address is unsupported");
queue = oneFragment();
Require(DecodeState(queue).colors[0].fmaskAddress == 0u, "an uncompressed one-fragment color target noted an FMASK address");
queue = oneFragment();
queue.context[0x31d] = 0xa000;
expectFailure([&] { DecodeState(queue); }, "color targets with more than one but fewer fragments than samples (EQAA) are unsupported");
queue = oneFragment();
queue.context[0x31d] = 0x8000;
expectFailure([&] { DecodeState(queue); }, "color targets with more fragments than samples are unsupported");
queue = oneFragment();
queue.context[0x201] = 0x012202;
expectFailure([&] { DecodeState(queue); }, "a one-fragment color target is supported only with static DB_EQAA anchors at the depth target's sample count");
queue = oneFragment();
queue.context[0x201] = 0x112201;
expectFailure([&] { DecodeState(queue); }, "a one-fragment color target is supported only with static DB_EQAA anchors at the depth target's sample count");
queue = oneFragment();
queue.context[0x2f8] = 0x00100001;
queue.context[0x010] = 0x22900987;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44;
expectFailure([&] { DecodeState(queue); }, "a one-fragment color target's sample count differs from the rasterizer's");
queue = oneFragment();
queue.context[0x010] = 0;
queue.context[0x011] = 0;
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
queue = singleSampleScan();
queue.context[0x010] = 0x22900983;
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
@@ -6919,8 +6990,37 @@ void sampledTextureBudgetTests() {
Require(SampledTextureBudget(memory, &reported, 5 * GiB) == 5 * GiB + 128 * MiB, "a 15 GiB budget with 4 GiB used outside the texture caches does not leave 5 GiB 128 MiB after the 4 GiB storage cache and 1 GiB 896 MiB of headroom");
reported.heapUsage[2] = 3 * GiB;
Require(SampledTextureBudget(memory, &reported, 5 * GiB) == 9 * GiB + 128 * MiB, "usage below the cached bytes is not read as no other device memory in use");
reported.heapUsage[2] = 9 * GiB;
Require(SampledTextureBudget(memory, &reported, 5 * GiB, 4 * GiB + 512 * MiB) == 4 * GiB + 640 * MiB, "a storage image cache larger than TextureCacheBudget was not reserved in full");
Require(SampledTextureBudget(memory, &reported, 5 * GiB, 1 * GiB) == 5 * GiB + 128 * MiB, "a storage image cache smaller than TextureCacheBudget did not keep the TextureCacheBudget reservation");
reported.heapUsage[2] = 12 * GiB;
Require(SampledTextureBudget(memory, &reported, 1 * GiB) == 2 * GiB, "a heap whose other users leave no room does not keep the 2 GiB floor");
{
using AgcDriver::Graphics::StorageTextureBudget;
auto small = memory;
small.memoryHeaps[2].size = 8 * GiB;
VkPhysicalDeviceMemoryBudgetPropertiesEXT example{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
example.heapBudget[2] = 7373 * MiB;
example.heapUsage[2] = 2 * GiB + 3686 * MiB;
Require(SampledTextureBudget(small, &example, 3686 * MiB, 3686 * MiB) == 2 * GiB, "3686 MiB of storage images on an 8 GiB heap with a 7373 MiB budget and 2 GiB of other use left the sampled cache above its floor");
for (const auto heapGiB : {8u, 16u}) {
small.memoryHeaps[2].size = heapGiB * GiB;
for (std::uint64_t budget = 4 * GiB; budget <= heapGiB * GiB; budget += GiB) {
example.heapBudget[2] = budget;
for (std::uint64_t other = 0; other <= budget; other += GiB) {
for (std::uint64_t sampled = 0; sampled <= 4 * GiB; sampled += 512 * MiB) {
for (std::uint64_t storage = 0; storage <= heapGiB * GiB / 2u; storage += 512 * MiB) {
example.heapUsage[2] = other + sampled + storage;
const auto sampledLimit = SampledTextureBudget(small, &example, sampled + storage, storage);
const auto storageLimit = StorageTextureBudget(small, &example, sampled, storage);
if (sampledLimit > 2 * GiB) Require(other + storage + sampledLimit + budget / 8u <= budget, "the sampled cache budget and the storage images held exceed the reported budget less its eighth");
if (storageLimit > 512 * MiB) Require(other + sampled + storageLimit + budget / 8u <= budget, "the storage cache budget and the sampled textures held exceed the reported budget less its eighth");
}
}
}
}
}
}
memory.memoryHeaps[0].flags = 0;
memory.memoryHeaps[2].flags = 0;
Require(SampledTextureBudget(memory, &reported, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of sampled textures");
@@ -6941,29 +7041,29 @@ void storageTextureBudgetTests() {
reported.heapBudget[1] = 100 * GiB;
Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap");
reported.heapBudget[2] = 10 * GiB;
Require(StorageTextureBudget(memory, &reported, 0, 0) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
Require(StorageTextureBudget(memory, &reported, 0, 0) == 5 * GiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to half of the budget");
reported.heapUsage[2] = 5 * GiB;
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "2 GiB used outside the texture caches on a 10 GiB budget lowered the storage image cache below its quarter");
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 5 * GiB, "2 GiB used outside the texture caches on a 10 GiB budget lowered the storage image cache below its half");
reported.heapUsage[2] = 8 * GiB + 512 * MiB;
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2304 * MiB, "a 10 GiB budget with 5632 MiB used outside the texture caches does not leave 2304 MiB after 1 GiB of sampled textures and 1280 MiB of headroom");
reported.heapUsage[2] = 9 * GiB;
Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved in full");
reported.heapUsage[2] = 1 * GiB;
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "usage below the cached bytes is not read as no other device memory in use");
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 5 * GiB, "usage below the cached bytes is not read as no other device memory in use");
Require(StorageTextureBudget(memory, &reported, 7 * GiB, 1 * GiB) == 1792 * MiB, "7 GiB of sampled textures with the usage below the cached bytes do not leave 1792 MiB after their reservation and 1280 MiB of headroom");
reported.heapUsage[2] = 9 * GiB + 512 * MiB;
Require(StorageTextureBudget(memory, &reported, 2 * GiB, 512 * MiB) == 512 * MiB, "a heap whose other users leave no room does not keep the 512 MiB floor");
reported.heapUsage[2] = 0;
reported.heapBudget[2] = 6 * GiB;
Require(StorageTextureBudget(memory, &reported, 0, 0) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
Require(StorageTextureBudget(memory, &reported, 0, 0) == 3 * GiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to half of the budget");
reported.heapBudget[2] = 1 * GiB;
Require(StorageTextureBudget(memory, &reported, 0, 0) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor");
reported.heapBudget[2] = 40 * GiB;
Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap");
Require(StorageTextureBudget(memory, &reported, 0, 0) == 8 * GiB, "a budget above the heap size raised the storage image cache past half of the heap");
memory.memoryHeaps[2].size = 8 * GiB;
reported.heapBudget[2] = 7189 * MiB;
reported.heapUsage[2] = 4491 * MiB;
Require(StorageTextureBudget(memory, &reported, 49 * MiB, 506 * MiB) == 7189 * MiB / 4u, "49 MiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches held the storage image cache below a quarter of the budget");
Require(StorageTextureBudget(memory, &reported, 49 * MiB, 506 * MiB) == 7189 * MiB - 3936 * MiB - 49 * MiB - 7189 * MiB / 8u, "49 MiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches did not leave the storage image cache what the budget has left after them and its eighth");
reported.heapUsage[2] = 6490 * MiB;
Require(StorageTextureBudget(memory, &reported, 2 * GiB, 506 * MiB) == 512 * MiB, "2 GiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches did not take the storage image cache back to its 512 MiB floor");
memory.memoryHeaps[2].size = 10 * GiB;
@@ -6980,8 +7080,8 @@ void storageTextureBudgetTests() {
reported.heapUsage[2] = outside + sampled + 512 * MiB;
const auto limit = StorageTextureBudget(memory, &reported, sampled, 512 * MiB);
Require(limit >= 512 * MiB, "pressure sweep lost the storage cache floor");
Require(limit <= std::max<std::uint64_t>(512 * MiB, budget / 4u), "pressure sweep exceeded a quarter of the reported budget above the floor");
Require(limit <= std::max<std::uint64_t>(2 * GiB, heapGiB * GiB / 4u), "pressure sweep exceeded the heap storage cache cap");
Require(limit <= std::max<std::uint64_t>(512 * MiB, budget / 2u), "pressure sweep exceeded half of the reported budget above the floor");
Require(limit <= std::max<std::uint64_t>(512 * MiB, heapGiB * GiB / 2u), "pressure sweep exceeded half of the heap");
if (outside != 0) Require(limit <= previous, "additional non-cache memory usage grew the storage cache budget");
if (limit > 512 * MiB) Require(outside + sampled + budget / 8u + limit <= budget, "storage cache budget consumed live sampled memory or pressure headroom");
previous = limit;
@@ -10714,6 +10814,21 @@ void validationTests() {
attribute.components = 2;
attribute.resource.fields[1] |= 0x80000000u;
expectFailure([&] { AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1)); }, "descriptor flags");
attribute.resource.fields = {0x1000, 8u << 16u, 3, 71u << 12u};
attribute.components = 3;
context.formatProperties = [](VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) {
*properties = {};
if (format == VK_FORMAT_R16G16B16A16_SFLOAT || format == VK_FORMAT_R8G8B8A8_UNORM) properties->bufferFeatures = VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT;
};
Require(AgcDriver::Graphics::DecodeVertexFormat(attribute).format == VK_FORMAT_R16G16B16_SFLOAT && AgcDriver::Graphics::DecodeVertexFormat(attribute).bytes == 6, "a 3-component 16_16_16_16 float attribute was not decoded as 3 components");
const auto widened = AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1));
Require(widened.attributes[0].format == VK_FORMAT_R16G16B16A16_SFLOAT && widened.bindings[0].stride == 8 && AgcDriver::Graphics::VertexReadBytes(attribute, widened.attributes[0].format) == 8, "a 3-component 16_16_16_16 float attribute was not widened to the format the device supports");
Require(AgcDriver::Graphics::VertexAttributeSignature(attribute) == "f32x3", "widening the vertex format changed the shader input");
attribute.resource.fields = {0x1000, 4u << 16u, 3, 56u << 12u};
Require(AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1)).attributes[0].format == VK_FORMAT_R8G8B8A8_UNORM, "a 3-component 8_8_8_8 unorm attribute was not widened to the format the device supports");
attribute.resource.fields = {0x1000, 12u << 16u, 3, 74u << 12u};
expectFailure([&] { AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1)); }, "device does not support vertex format 106");
expectFailure([&] { AgcDriver::Graphics::VertexReadBytes(attribute, VK_FORMAT_R32G32B32A32_SFLOAT); }, "is not a form of");
}
for (const auto capability : {spv::CapabilityInt64Atomics, spv::CapabilityInt64ImageEXT}) {
ShaderRecompiler::RecompileResult vertex;
@@ -5,6 +5,7 @@
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
@@ -112,6 +113,50 @@ std::size_t PrivateBytes() {
#endif
}
void BlocksFlowToAnotherThread() {
constexpr int rounds = 12;
constexpr int perRound = 200000;
std::size_t afterEarlyRounds = 0;
for (int round = 0; round < rounds; ++round) {
std::mutex lock;
std::vector<unsigned char*> handed;
std::atomic<bool> done{false};
std::thread freer([&] {
std::vector<unsigned char*> taken;
for (;;) {
const bool finished = done.load(std::memory_order_acquire);
{
std::lock_guard guard(lock);
taken.swap(handed);
}
for (auto* block : taken) {
const auto size = static_cast<std::size_t>(block[0]) * 4 + 1;
Check(block[size - 1] == block[0], "a block freed on another thread keeps its bytes until then");
::operator delete(block);
}
taken.clear();
if (finished) break;
std::this_thread::yield();
}
});
for (int i = 0; i < perRound; ++i) {
const auto seed = static_cast<unsigned char>(i % 256);
const auto size = static_cast<std::size_t>(seed) * 4 + 1;
auto* block = static_cast<unsigned char*>(::operator new(size));
block[0] = seed;
block[size - 1] = seed;
std::lock_guard guard(lock);
handed.push_back(block);
}
done.store(true, std::memory_order_release);
freer.join();
if (round == 3) afterEarlyRounds = PrivateBytes();
}
const auto afterLastRound = PrivateBytes();
std::printf("private bytes after 4 and after %d rounds of blocks freed on another thread: %zu, %zu\n", rounds, afterEarlyRounds, afterLastRound);
Check(afterLastRound < afterEarlyRounds + (64u << 20), "blocks freed on another thread do not pile up");
}
void ThreadsEndWithCachedBlocks() {
constexpr int waves = 24;
std::size_t afterEarlyWaves = 0;
@@ -144,6 +189,7 @@ int main() {
SizedDeletesKeepTheirClass();
BlocksCrossIntoLibstdcxx();
BlocksMoveBetweenThreads();
BlocksFlowToAnotherThread();
ThreadsEndWithCachedBlocks();
if (failures != 0) {
std::fprintf(stderr, "%d host allocator checks failed\n", failures);
@@ -114,7 +114,7 @@ void verify(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) {
const bool native = int64Atomics && limit < 68u;
require(widths.contains(64u) == native, native ? "device-memory LDS did not use a 64-bit compare exchange" : "a 64-bit compare exchange appeared without device-memory LDS and shaderBufferInt64Atomics");
require(widths.contains(32u) != native, native ? "device-memory LDS with shaderBufferInt64Atomics kept the workgroup lock" : "the 64-bit LDS atomic lost its workgroup lock");
require(result.bdaAbiVersion == (limit < 68u ? 1u : 0u), "the fault buffer for a misaligned 64-bit LDS atomic does not follow device-memory LDS at limit " + std::to_string(limit));
require(result.bdaAbiVersion == 1u, "a 64-bit LDS atomic did not get the fault buffer for a misaligned address at limit " + std::to_string(limit));
}
}
@@ -105,6 +105,8 @@ public:
Device() {
#ifdef _WIN32
library = SDL_LoadObject("vulkan-1.dll");
#elif defined(__APPLE__)
library = SDL_LoadObject("libvulkan.1.dylib");
#else
library = SDL_LoadObject("libvulkan.so.1");
#endif
@@ -116,6 +118,12 @@ public:
application.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
info.pApplicationInfo = &application;
#ifdef __APPLE__
const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME;
info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
info.enabledExtensionCount = 1;
info.ppEnabledExtensionNames = &portability;
#endif
Check(function<PFN_vkCreateInstance>("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance");
std::uint32_t count = 0;
const auto enumerate = function<PFN_vkEnumeratePhysicalDevices>("vkEnumeratePhysicalDevices");
@@ -402,8 +402,17 @@ void testIndirectDraw() {
alignas(4) std::array<std::uint16_t, 8> indices{};
state.indexBase = reinterpret_cast<std::uintptr_t>(indices.data());
state.indexType = 0;
state.indexBufferSize.reset();
const auto unlimited = AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state);
check(unlimited.indexed && unlimited.indirect && !unlimited.indirect->indexLimitKnown && unlimited.indexCount == 0 && unlimited.indexAddress == state.indexBase && unlimited.indexSize == 2 && unlimited.indirect->recordBytes == 20, "an indexed indirect draw without INDEX_BUFFER_SIZE did not resolve with an unknown limit");
const auto unlimitedMulti = AgcDriver::Pm4::ResolveDraw(makePacket(0x38, {0, 0x8c, 0x280, 0x280, 3, 0, 0, 20, 0}), state);
check(unlimitedMulti.indirect && !unlimitedMulti.indirect->indexLimitKnown && unlimitedMulti.indirect->count == 3 && unlimitedMulti.indirect->stride == 20, "a multi indexed indirect draw without INDEX_BUFFER_SIZE did not resolve with an unknown limit");
execute(state, makePacket(0x13, {0}));
check(state.indexBufferSize.has_value() && *state.indexBufferSize == 0, "INDEX_BUFFER_SIZE of zero was not recorded as written");
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state); }, "INDEX_BUFFER_SIZE");
expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x38, {0, 0x8c, 0x280, 0x280, 3, 0, 0, 20, 0}), state); }, "INDEX_BUFFER_SIZE");
state.indexBufferSize = 8;
check(AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state).indirect->indexLimitKnown, "a written INDEX_BUFFER_SIZE resolved with an unknown limit");
const auto indexed = AgcDriver::Pm4::ResolveDraw(makePacket(0x25, {0, 0x8c, 0x280, 0}), state);
check(indexed.indexed && indexed.indexAddress == state.indexBase && indexed.indexCount == 8 && indexed.indexSize == 2 && indexed.indirect->recordBytes == 20 && indexed.indirect->stride == 20 && indexed.firstVertex == 0 && indexed.indirect->indxOffset == 0, "indexed indirect draw mismatch");
check(indexed.indirect->VertexDwordOffset() == 12 && indexed.indirect->InstanceDwordOffset() == 16, "indexed record geometry mismatch");
@@ -1128,6 +1137,63 @@ void testReleaseMemDataSelects() {
for (const auto select : {6u, 7u}) expectFailure([&] { AgcDriver::Pm4::Validate(release(select), 0); }, "reserved RELEASE_MEM DATA_SEL");
}
void testMemSemaphore() {
alignas(8) static std::uint64_t semaphore = 0;
alignas(8) static std::uint32_t marker = 0;
const auto packet = [](std::uint32_t control) { return makePacket(0x39, {low(&semaphore), high(&semaphore), control}); };
check(AgcDriver::Pm4::AccessesMemory(packet(0xc0100000u)[0]), "MEM_SEMAPHORE must synchronize guest memory");
for (const auto queue : {0u, 0x20u}) {
AgcDriver::Pm4::Validate(packet(0xc0100000u), queue);
AgcDriver::Pm4::Validate(packet(0xe0000000u), queue);
}
expectFailure([&] { AgcDriver::Pm4::Validate(packet(0xe0010000u), 0); }, "mailbox");
expectFailure([&] { AgcDriver::Pm4::Validate(packet(0xa0000000u), 0); }, "MEM_SEMAPHORE operation");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x39, {low(&semaphore), high(&semaphore), 0xc0100000u, 0}), 0); }, "packet size");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x39, {0, 0, 0xc0100000u}), 0); }, "MEM_SEMAPHORE address");
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x39, {low(&semaphore) + 4, high(&semaphore), 0xc0100000u}), 0); }, "MEM_SEMAPHORE address");
semaphore = 0x500000000ull;
auto words = packet(0xc0100000u);
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint64_t>(semaphore).load() == 1, "a MEM_SEMAPHORE signal of type 1 did not write 1");
semaphore = 5;
words = packet(0xc0000000u);
submitWords(words);
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint64_t>(semaphore).load() == 6, "a MEM_SEMAPHORE signal of type 0 did not increment");
constexpr std::uint64_t queueSignals = 2000, cpuSignals = 200000;
semaphore = 0;
std::vector<std::uint32_t> graphicsWords, computeWords;
for (std::uint64_t index = 0; index < queueSignals; ++index) {
const auto signal = packet(0xc0000000u);
graphicsWords.insert(graphicsWords.end(), signal.begin(), signal.end());
computeWords.insert(computeWords.end(), signal.begin(), signal.end());
}
std::thread cpu([] {
for (std::uint64_t index = 0; index < cpuSignals; ++index) std::atomic_ref<std::uint64_t>(semaphore).fetch_add(1);
});
submitWords(graphicsWords);
submitWords(computeWords, 0x20);
cpu.join();
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint64_t>(semaphore).load() == 2 * queueSignals + cpuSignals, "competing MEM_SEMAPHORE signals of type 0 lost an increment");
semaphore = 0;
marker = 0;
words = joinPackets({packet(0xe0000000u), makePacket(0x49, {0x0030c514, 0x20000000, low(&marker), high(&marker), 1, 0, 0})});
submitWords(words);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
check(std::atomic_ref<std::uint32_t>(marker).load() == 0, "a MEM_SEMAPHORE wait passed a zero semaphore");
std::atomic_ref<std::uint64_t>(semaphore).store(2);
for (int waited = 0; waited < 2000 && std::atomic_ref<std::uint32_t>(marker).load() == 0; ++waited) std::this_thread::sleep_for(std::chrono::milliseconds(1));
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint32_t>(marker).load() == 1, "work after a signalled MEM_SEMAPHORE wait never ran");
check(std::atomic_ref<std::uint64_t>(semaphore).load() == 1, "a MEM_SEMAPHORE wait did not take one from the semaphore");
}
void testSuspendPointWritesQueuedLabels() {
alignas(8) static std::uint32_t gate = 0, marker = 0;
gate = 0;
@@ -2015,6 +2081,7 @@ int main(int argc, char** argv) {
testPredicatedSubmission();
testConditionalSubmission();
testSuspendPointWritesQueuedLabels();
testMemSemaphore();
testNopPadSubmission();
testBranchSubmission();
testWaitRegMemMasking();
@@ -7,6 +7,7 @@
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
#include "execution/FixtureDeviceSelection.hpp"
#include <SDL_loadso.h>
#include <algorithm>
#include <array>
@@ -38,6 +39,8 @@ public:
Device() {
#ifdef _WIN32
library = SDL_LoadObject("vulkan-1.dll");
#elif defined(__APPLE__)
library = SDL_LoadObject("libvulkan.1.dylib");
#else
library = SDL_LoadObject("libvulkan.so.1");
#endif
@@ -49,6 +52,12 @@ public:
application.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
info.pApplicationInfo = &application;
#ifdef __APPLE__
const char* portability = VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME;
info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
info.enabledExtensionCount = 1;
info.ppEnabledExtensionNames = &portability;
#endif
Check(function<PFN_vkCreateInstance>("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance");
std::uint32_t count = 0;
const auto enumerate = function<PFN_vkEnumeratePhysicalDevices>("vkEnumeratePhysicalDevices");
@@ -56,7 +65,7 @@ public:
Require(count != 0, "no Vulkan device");
std::vector<VkPhysicalDevice> devices(count);
Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices");
context.physical = devices.front();
context.physical = SelectFixturePhysicalDevice(devices, function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties"));
const auto queues = function<PFN_vkGetPhysicalDeviceQueueFamilyProperties>("vkGetPhysicalDeviceQueueFamilyProperties");
queues(context.physical, &count, nullptr);
std::vector<VkQueueFamilyProperties> families(count);
@@ -2561,7 +2561,7 @@ void remappedImportTests(const Device& device) {
void hostImportRetirementTests(const Device& device) {
const auto& context = device.GetContext();
constexpr std::size_t UnitBytes = 65536;
constexpr std::size_t UnitBytes = 2 * 65536;
constexpr std::size_t AllocationBytes = 4 * UnitBytes;
constexpr std::size_t CopyBytes = 256;
#ifdef _WIN32
@@ -3955,10 +3955,11 @@ void drawSnapshotTableSetTests(const Device& device, Recorder& recorder) {
}
constexpr std::uint32_t width = 4;
constexpr std::uint32_t height = 4;
alignas(256) static std::array<float, width * height> texels{};
alignas(256) static std::array<float, width * height * 16u> texels{};
texels.fill(1.0f);
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels.data()));
const std::array<std::uint32_t, 8> texture{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u};
Require(DescribeSurface(DecodeTextureResource(texture)).guestBytes <= sizeof(texels), "the draw snapshot table set test T# reads past its texels");
constexpr auto bindingCount = static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Count);
const auto addTable = [&](ShaderRecompiler::RecompileResult& program, ShaderRecompiler::RuntimeAbi::Stage stage, std::uint32_t set, std::uint32_t elements) {
ShaderRecompiler::DescriptorBinding map{};
@@ -4552,6 +4553,46 @@ void drawSnapshotEvictionTests(const Device& device) {
Require(cache.ReusableDrawSnapshot(address, 16) == nullptr && cache.ReusableDrawSnapshot(address, 16) == nullptr, "a snapshot outlived a CPU store");
}
void containingSnapshotTests(const Device& device) {
using namespace AgcDriver::GuestMemory;
constexpr std::size_t bytes = 131072;
void* block = WriteWatched() ? AllocateWatched(bytes, 65536) : nullptr;
if (block == nullptr) {
std::cout << "write watching unavailable: containing draw snapshots not tested\n";
return;
}
struct Release {
void* block;
~Release() { ReleaseWatched(block, bytes); }
} release{block};
const auto address = reinterpret_cast<std::uint64_t>(block);
const VkDeviceSize alignment = std::max<VkDeviceSize>(device.GetContext().limits.minStorageBufferOffsetAlignment, 4);
Recorder cache(device.GetContext());
const auto whole = std::make_shared<Buffer>(device.GetContext(), bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
const auto exact = std::make_shared<Buffer>(device.GetContext(), 4096, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
const auto registry = GuestAllocations::GuestAllocationsGeneration_nid_postfix();
const auto generation = CollectWrites(address, bytes);
Require(generation != 0, "the watched block has no generation");
cache.KeepDrawSnapshot(address, bytes, generation, registry, whole);
const auto inner = address + 65536 + 16 * alignment;
VkDeviceSize offset = 1;
Require(cache.ReusableDrawSnapshot(inner, 4096) == nullptr, "a lookup without an offset took a containing snapshot");
Require(cache.ReusableDrawSnapshot(inner, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == whole && offset == inner - address, "an unchanged range inside a kept snapshot was not taken from it at its offset");
Require(cache.ReusableDrawSnapshot(inner, address + bytes - inner + 4, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == nullptr, "a range past the end of a kept snapshot was taken from it");
Require(cache.ReusableDrawSnapshot(inner + alignment / 2, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == nullptr, "a misaligned range was taken from a kept snapshot");
Require(cache.ReusableDrawSnapshot(inner, 4096, Recorder::SnapshotUse::Vertex, nullptr, 0, &offset) == nullptr, "a vertex lookup took a storage snapshot");
cache.KeepDrawSnapshot(inner, 4096, generation, registry, exact);
offset = 1;
Require(cache.ReusableDrawSnapshot(inner, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == exact && offset == 0, "an exact snapshot lost to a containing one");
std::memset(block, 0x5a, 16);
CollectWrites(address, 16);
Require(cache.ReusableDrawSnapshot(inner + 4096, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == whole && offset == inner + 4096 - address, "a store outside the range stopped it from being taken from a containing snapshot");
Require(cache.ReusableDrawSnapshot(address, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == nullptr, "a range over a CPU store was taken from a containing snapshot");
std::memset(reinterpret_cast<void*>(inner + 4096), 0x5a, 16);
CollectWrites(inner + 4096, 16);
Require(cache.ReusableDrawSnapshot(inner + 4096, 4096, Recorder::SnapshotUse::Storage, nullptr, 0, &offset) == nullptr, "a containing snapshot outlived a CPU store into the range");
}
void unwrittenSnapshotTests(const Device& device) {
Recorder cache(device.GetContext());
const auto buffer = std::make_shared<Buffer>(device.GetContext(), 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
@@ -8711,6 +8752,106 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
std::cout << "Multisampled target guest access tests passed\n";
}
void oneFragmentFmaskGuardTests(const Device& device) {
const auto& base = device.GetContext();
if (!device.NullDescriptors()) {
std::cout << "nullDescriptor unavailable: one-fragment FMASK guard not tested\n";
return;
}
constexpr std::uint32_t side = 64;
constexpr std::size_t bytes = 3u * 65536u;
#ifdef _WIN32
void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
#else
void* block = std::aligned_alloc(65536, bytes);
#endif
Require(block != nullptr, "cannot allocate the one-fragment FMASK block");
std::memset(block, 0, bytes);
const auto address = reinterpret_cast<std::uint64_t>(block);
{
GuestAllocations::Mutation mutation;
mutation.Add(block, bytes, true, true, true);
}
struct Unregister {
const Context& context;
void* block;
~Unregister() {
ClearMultisampleTargets(context.device);
ClearCachedTextures(context.device);
{
GuestAllocations::Mutation mutation;
mutation.Remove(block);
}
#ifdef _WIN32
VirtualFree(block, 0, MEM_RELEASE);
#else
std::free(block);
#endif
}
} unregister{base, block};
TextureDetiler detiler(base);
auto context = base;
context.detiler = &detiler;
context.nullDescriptors = true;
TextureCache cache(context);
context.textureCache = &cache;
const auto refused = [](const std::function<void()>& access, const std::string& what) {
std::string message;
try {
access();
} catch (const std::exception& error) {
message = error.what();
}
Require(message.find("FMASK") != std::string::npos && message.find("one-fragment color target") != std::string::npos, what + " of a host-drawn one-fragment target's FMASK did not throw: " + message);
};
ColorTarget target{};
target.extent = {128, 128};
target.samples = 1;
target.coverageSamples = 4;
target.fmaskAddress = address;
GuardOneFragmentFmask(context, target);
GuardOneFragmentFmask(context, target);
std::array<std::byte, 64> words{};
refused([&] { AgcDriver::GuestMemory::Read(address + 256u, words); }, "a CPU read");
refused([&] { RequireNoMultisampleTarget(address + 65535u, 1u, "a DMA copy"); }, "a DMA copy of the last guarded byte");
RequireNoMultisampleTarget(address + 65536u, 64u, "a DMA copy");
ShaderRecompiler::RecompileResult program;
ShaderRecompiler::DescriptorBinding binding;
binding.kind = ShaderRecompiler::DescriptorKind::SampledImage;
binding.role = ShaderRecompiler::DescriptorRole::GuestImages;
binding.descriptorSet = 0;
binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding;
binding.count = 1;
binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D;
binding.imageSamplers = {0};
binding.guestDescriptor = {
static_cast<std::uint32_t>(address >> 8u),
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (56u << 20u) | (((side - 1u) & 3u) << 30u),
((side - 1u) >> 2u) | ((side - 1u) << 14u),
0xfacu | (9u << 28u),
0u,
0u,
0u,
0u,
};
program.bindings.push_back(binding);
const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
for (const char* what : {"a texture read", "a repeated texture read"}) {
refused([&] {
std::lock_guard gpu(GpuMutex());
ShaderResources resources(context, compute);
}, what);
}
{
GuestAllocations::Mutation mutation;
mutation.Unmap(block, bytes, [](const void*, std::size_t, const void*, bool) {});
mutation.Add(block, bytes, true, true, true);
}
AgcDriver::GuestMemory::Read(address + 256u, words);
RequireNoMultisampleTarget(address, 65536u, "a DMA copy");
std::cout << "One-fragment FMASK guard tests passed\n";
}
void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) {
const auto& base = device.GetContext();
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || base.hostImportAlignment == 0) {
@@ -9867,7 +10008,7 @@ int pipelineCacheFileTests(const Device& device) {
} removeDirectory{directory};
#ifdef _WIN32
_putenv_s("ANYPS5_NO_SHADER_CACHE", "0");
_putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str());
_wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", directory.wstring().c_str());
#else
setenv("ANYPS5_NO_SHADER_CACHE", "0", 1);
setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1);
@@ -11165,6 +11306,7 @@ int main(int argc, char** argv) {
}
if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") {
multisampleGuestAccessTests(device, recorder);
oneFragmentFmaskGuardTests(device);
fmaskDecompressMsaaTests(device, recorder);
return 0;
}
@@ -11183,6 +11325,7 @@ int main(int argc, char** argv) {
}
if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") {
drawSnapshotCapTests(device, recorder);
containingSnapshotTests(device);
unwrittenSnapshotTests(device);
drawSnapshotUploadTests(device, recorder);
std::cout << "Draw snapshot cap tests passed\n";
@@ -11225,6 +11368,7 @@ int main(int argc, char** argv) {
bdaStagingTests(device, recorder);
depthSurfaceProofTests(device, recorder);
multisampleGuestAccessTests(device, recorder);
oneFragmentFmaskGuardTests(device);
tiledResolveTests(device, recorder);
fmaskDecompressMsaaTests(device, recorder);
misalignedSnapshotTests(device, recorder);
@@ -11235,6 +11379,7 @@ int main(int argc, char** argv) {
imageTableFirstLayerRevalidateTests(device, recorder);
drawSnapshotCapTests(device, recorder);
drawSnapshotEvictionTests(device);
containingSnapshotTests(device);
unwrittenSnapshotTests(device);
drawSnapshotUploadTests(device, recorder);
drawInputReuseTests(device, recorder);
@@ -1,4 +1,5 @@
#include "IntermediateRepresentation/IrProgram.hpp"
#include "Optimization/DeadCodeEliminator.hpp"
#include "Optimization/RedundantValueEliminator.hpp"
#include "Recompiler.hpp"
#include <array>
@@ -505,6 +506,55 @@ int main() {
return b.eliminate() == 1u && b.program.Metadata().blockInfo.front().condition == &first;
});
passed &= run("descriptor dwords name the surviving values after redundancy and dead code elimination", [] {
Builder b;
auto& input = b.unknown();
DescriptorSource descriptor;
descriptor.dwordCount = 8u;
std::array<IrValue*, 8> kept{};
std::array<IrValue*, 8> removed{};
for (std::uint32_t index = 0; index < 8u; ++index) {
auto& first = b.emit(IrOpcode::IAdd32, IrType::U32, {&input, &b.constant(index + 1u)});
auto& second = b.emit(IrOpcode::IAdd32, IrType::U32, {&input, &b.constant(index + 1u)});
b.keep(first);
b.keep(second);
descriptor.dwords[index] = &second;
kept[index] = &first;
removed[index] = &second;
}
b.program.Resources().descriptorSources.push_back(descriptor);
b.program.Resources().descriptorSources.emplace_back();
if (b.eliminate() != 8u) return false;
DeadCodeEliminator{}.Eliminate(b.program);
const auto& descriptors = b.program.Resources().descriptorSources;
for (std::size_t index = 0; index < kept.size(); ++index) {
if (descriptors[0].dwords[index] != kept[index] || kept[index]->Parent() != b.block || removed[index]->Parent() != nullptr || descriptors[1].dwords[index] != nullptr) return false;
}
return true;
});
passed &= run("a selected table column keeps its surviving runtime offset", [] {
Builder b;
auto& input = b.unknown();
auto& first = b.emit(IrOpcode::IMul32, IrType::U32, {&input, &b.constant(48u)});
auto& second = b.emit(IrOpcode::IMul32, IrType::U32, {&input, &b.constant(48u)});
b.keep(first);
b.keep(second);
DescriptorSource selection;
selection.dwordCount = 1u;
selection.dwords[0] = &second;
DescriptorSource image;
image.tableColumn.emplace();
image.tableColumn->selected = true;
image.tableColumn->selection = 0u;
b.program.Resources().descriptorSources = {selection, image};
if (b.eliminate() != 1u) return false;
DeadCodeEliminator{}.Eliminate(b.program);
const auto& descriptors = b.program.Resources().descriptorSources;
const auto selected = descriptors[1].tableColumn->selection;
return descriptors[selected].dwords[0] == &first && first.Parent() == b.block && second.Parent() == nullptr;
});
passed &= run("lane reads are not merged", [] {
Builder b;
auto& first = b.unknown();
@@ -62,11 +62,19 @@ void setEnvironment(const char* name, const std::string& value) {
#endif
}
void setPathEnvironment(const char* name, const std::filesystem::path& value) {
#ifdef _WIN32
_wputenv_s(std::filesystem::path(name).wstring().c_str(), value.wstring().c_str());
#else
setenv(name, value.c_str(), 1);
#endif
}
#ifdef _WIN32
void verifyHostStandardError() {
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-host-stderr-" + std::to_string(std::random_device{}()));
setEnvironment("ANYPS5_NO_SHADER_CACHE", "0");
setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string());
setPathEnvironment("ANYPS5_SHADER_CACHE_DIR", directory);
int capture[2], guest[2];
require(::_pipe(capture, 65536, _O_BINARY) == 0, "cannot create host capture");
require(::_pipe(guest, 65536, _O_BINARY) == 0, "cannot create guest capture");
@@ -324,6 +332,7 @@ CompiledVariant sampleVariant() {
image.volumeArraySampleCompatible = true;
image.flatLineCompatible = false;
image.flatVolumeLineCompatible = false;
image.dimensionQueryOnly = false;
image.foldTexelOffsetsOnly = true;
image.remappedDepth1D = true;
image.byElements = 4;
@@ -1073,7 +1082,7 @@ void verifyLongHostPaths() {
const auto extended = [](const auto& path) { return std::filesystem::path(L"\\\\?\\" + path.native()); };
std::filesystem::create_directories(extended(directory));
setEnvironment("ANYPS5_NO_SHADER_CACHE", "0");
setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string());
setPathEnvironment("ANYPS5_SHADER_CACHE_DIR", directory);
std::vector<std::filesystem::path> files;
std::filesystem::path entries;
struct Cleanup {
@@ -2120,6 +2129,36 @@ void verifyMalformedPlans() {
require(!ShaderDiskCache::EncodePlan(unblocked, bytes), "a phi without predecessor blocks was encoded");
}
void verifySharedPlans() {
auto fixture = planFixtures().front();
const auto request = fixture.Request(false);
const auto plan = GetResourcePlan(request);
const std::vector<std::byte> first{std::byte{0x5a}, std::byte{0x01}, std::byte{0x02}};
const std::vector<std::byte> second{std::byte{0x5a}, std::byte{0x01}, std::byte{0x03}};
for (const auto* key : {&first, &second}) {
const auto entry = ShaderDiskCache::EncodePlanEntry(*key, *plan);
require(WriteFileAtomically(ShaderDiskCache::EntryDirectory() / ShaderDiskCache::EntryName(*key), entry), "cannot write the shared plan entries");
}
constexpr std::uint64_t address = 0x7000u;
IrResourcePlan expected;
require(ShaderDiskCache::DecodePlan(encodedPlan(*plan), expected), "the shared plan fixture does not decode");
expected.shaderHash = address;
const auto hits = ShaderDiskCache::Totals().planHits;
auto a = ShaderDiskCache::LoadSharedPlan(first, address);
auto b = ShaderDiskCache::LoadSharedPlan(second, address);
require(a != nullptr && b != nullptr && ShaderDiskCache::Totals().planHits == hits + 2u, "the shared plan entries did not load");
require(a.get() == b.get(), "two plan entries with the same payload for one shader decoded into separate plans");
require(samePlan(*a, expected), "a shared plan differs from the stored plan");
const auto other = ShaderDiskCache::LoadSharedPlan(second, address + 0x100u);
require(other != nullptr && other.get() != a.get() && other->shaderHash == address + 0x100u && a->shaderHash == address, "a plan of another shader address was shared");
const std::weak_ptr<const IrResourcePlan> released = a;
a.reset();
b.reset();
require(released.expired(), "the shared plan table kept a plan no source uses");
const auto again = ShaderDiskCache::LoadSharedPlan(first, address);
require(again != nullptr && samePlan(*again, expected), "a released shared plan did not load again");
}
void verifyPlanRoundTrip() {
const auto handBuilt = handBuiltPlan();
const auto handBytes = encodedPlan(handBuilt);
@@ -2499,7 +2538,7 @@ int main(int argc, char** argv) {
std::filesystem::remove_all(directory);
verifyDefaultDirectory(argv[0]);
setEnvironment("ANYPS5_NO_SHADER_CACHE", "0");
setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string());
setPathEnvironment("ANYPS5_SHADER_CACHE_DIR", directory);
verifyAtomicReplacement(directory);
verifyResultRoundTrip();
verifyEntryRoundTrip();
@@ -2510,6 +2549,7 @@ int main(int argc, char** argv) {
verifyFloatModeSerialization();
verifyPlanRoundTrip();
verifyPlanKey();
verifySharedPlans();
verifyStore();
verifyPersist();
verifyAcrossProcesses(argv[0]);
@@ -1327,6 +1327,69 @@ void verifyLaneMaskGuardedDescriptorSources() {
}
}
void verifyExecGuardedDescriptorSources() {
using namespace ShaderRecompiler;
constexpr std::uint32_t Format8888UNorm = 56;
constexpr std::uint32_t Type2D = 9;
struct alignas(256) Texture { std::array<std::uint8_t, 256> bytes{}; };
static Texture texture;
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texture.bytes.data()));
const std::array<std::uint32_t, 8> valid{static_cast<std::uint32_t>(base >> 8u), static_cast<std::uint32_t>((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u};
const std::array<std::uint32_t, 8> invalid{0x43700000u, 0x43070000u, 0x3b888889u, 0x3bf2b9d6u, 0x3b088889u, 0x3b72b9d6u, 0x3f7f7777u, 0x3f7f0d46u};
const std::array<std::uint32_t, 4> sampler{0u, 0u, 0u, 0u};
std::array<std::uint32_t, 4> output{};
const auto outputAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output.data()));
const std::array<std::uint32_t, 4> outputV{static_cast<std::uint32_t>(outputAddress), static_cast<std::uint32_t>((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu};
std::array<std::uint32_t, 32> srt{};
const auto fill = [&](const std::array<std::uint32_t, 8>& image, std::uint32_t mode) {
std::copy(image.begin(), image.end(), srt.begin());
std::copy(sampler.begin(), sampler.end(), srt.begin() + 8);
std::copy(outputV.begin(), outputV.end(), srt.begin() + 16);
srt[20] = mode;
};
const auto srtAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(srt.data()));
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(srtAddress), static_cast<std::uint32_t>(srtAddress >> 32u)};
const std::array<std::uint32_t, 4> capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
RecompileRequest request{};
const std::vector<std::uint32_t> code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0x7da80010u, 0xbf880006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u};
request.shader = {ShaderStage::Compute, 0x24000u, code, 0, {}};
request.context.waveSize = 32;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.target.vulkanVersion = 0x00401000u;
request.target.spirvVersion = 0x00010300u;
request.target.subgroupSize = 32;
request.target.supportedCapabilities = capabilities;
request.target.supportedExtensions = extensions;
request.target.bdaAbiVersion = BdaAbi::Version;
request.target.fragmentShaderBarycentricEnabled = false;
request.layout.pushConstantSizeBytes = 128;
fill(invalid, 0u);
const auto plan = GetResourcePlan(request);
require(plan->info.images.size() == 1u && plan->info.samplers.size() == 1u, "exec guarded sources: the image and its sampler were not planned");
require(!plan->controlFlow.empty() && std::ranges::any_of(plan->controlFlow, [](const ResourceBlock& block) { return block.condition != nullptr; }), "exec guarded sources: the EXEC branch around the image kept no condition");
{
fill(invalid, 0u);
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(request);
require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "exec guarded sources: the T# behind a branch no lane takes was not left null");
}
fill(invalid, 1u);
{
AgcDriver::ShaderMemory memory({});
expectFailure([&] { static_cast<void>(memory.Capture(request)); }, "runtime image descriptor is invalid", "exec guarded sources: an invalid T# behind a branch the lanes may take was accepted");
}
fill(valid, 1u);
{
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(request);
require(capture->snapshot.images.size() == 1u && std::equal(valid.begin(), valid.end(), capture->snapshot.images[0].dwords.begin()), "exec guarded sources: the T# behind a branch the lanes may take was not read");
}
}
void verifyGuardedDescriptorSources() {
using namespace ShaderRecompiler;
constexpr std::uint32_t Format8888UNorm = 56;
@@ -1713,6 +1776,41 @@ std::array<std::uint32_t, 8> TableSelected(const ShaderRecompiler::ImageTableSna
return column.words.at(column.codes[0]).dwords;
}
void verifyTargetFeatureKey() {
using namespace ShaderRecompiler;
static constexpr std::array<std::uint32_t, 1> code{0xbf810000u};
RecompileRequest request{};
request.shader = {ShaderStage::Compute, 0x10000u, code, 0, {}};
request.context.waveSize = 64;
const std::vector<std::uint32_t> capabilities{1u, 4448u, 5347u};
const std::vector<std::string_view> extensions{"SPV_KHR_8bit_storage", "SPV_KHR_physical_storage_buffer"};
const auto build = [&](std::span<const std::uint32_t> capabilityList, std::span<const std::string_view> extensionList) {
auto keyed = request;
keyed.target.supportedCapabilities = capabilityList;
keyed.target.supportedExtensions = extensionList;
std::vector<std::uint64_t> key;
RecompileCacheKey::Build(keyed, key);
return key;
};
const auto base = build(capabilities, extensions);
const std::vector<std::uint32_t> capabilityCopy = capabilities;
const std::vector<std::string> extensionStorage{"SPV_KHR_8bit_storage", "SPV_KHR_physical_storage_buffer"};
const std::vector<std::string_view> extensionCopy{extensionStorage[0], extensionStorage[1]};
require(build(capabilityCopy, extensionCopy) == base, "target feature key: equal capability and extension lists in other storage gave another key");
auto renamed = extensionStorage;
renamed[1].back() = 'X';
const std::vector<std::string_view> renamedViews{renamed[0], renamed[1]};
require(build(capabilities, renamedViews) != base, "target feature key: an extension name that differs in its last byte gave the same key");
const std::vector<std::string_view> moved{"SPV_KHR_8bit_storageS", "PV_KHR_physical_storage_buffer"};
require(build(capabilities, moved) != base, "target feature key: a byte moved between two extension names gave the same key");
const std::vector<std::uint32_t> more{1u, 4448u, 5347u, 11u};
require(build(more, extensions) != base, "target feature key: an added capability gave the same key");
std::vector<std::string> many;
for (std::uint32_t index = 0; index < 40u; ++index) many.push_back("SPV_EXT_descriptor_indexing_" + std::to_string(index));
const std::vector<std::string_view> manyViews(many.begin(), many.end());
require(build(capabilities, manyViews).size() == base.size(), "target feature key: the key grows with the device's extension names");
}
void verifyImageTableKeys() {
using namespace ShaderRecompiler;
namespace Abi = ImageTableAbi;
@@ -8427,7 +8525,9 @@ int main(int argc, char** argv) {
verifyPlanCopyAnswers();
verifyGuardedDescriptorSources();
verifyLaneMaskGuardedDescriptorSources();
verifyExecGuardedDescriptorSources();
verifySrtTape();
verifyTargetFeatureKey();
verifyImageTableKeys();
verifyImageTables();
verifyImageTableRegistration();
@@ -77,10 +77,18 @@ Usage Scan(std::span<const std::uint32_t> words) {
std::uint32_t laneZeroBody = 0u;
std::uint32_t lockPointer = 0u;
std::map<std::uint32_t, std::uint32_t> constants;
std::uint32_t faultFunction = 0u;
bool inFaultFunction = false;
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> spv::WordCountShift;
const auto op = static_cast<spv::Op>(words[cursor] & spv::OpCodeMask);
Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction");
if (op == spv::OpName && count >= 3u && std::string_view(reinterpret_cast<const char*>(words.data() + cursor + 2u)) == "record_bda_fault") faultFunction = words[cursor + 1u];
if (op == spv::OpFunction && count >= 3u) inFaultFunction = words[cursor + 2u] == faultFunction;
if (inFaultFunction) {
cursor += count;
continue;
}
if (op == spv::OpDecorate && count >= 4u && words[cursor + 2u] == spv::DecorationBuiltIn && words[cursor + 3u] == spv::BuiltInLocalInvocationIndex) {
localIndexVariable = words[cursor + 1u];
}
@@ -144,6 +144,57 @@ std::size_t WindowStores(std::span<const std::uint32_t> words) {
return stores;
}
constexpr std::array<std::uint32_t, 34> WindowInit{
0x34020086, 0x34060088, 0x4a140680, 0x4a160681, 0x4a180682, 0x4a1a0683, 0x4a1c0684, 0x4a1e0685,
0x4a200686, 0x4a220687, 0x4a240688, 0x4a260689, 0x4a28068a, 0x4a2a068b, 0x4a2c068c, 0x4a2e068d,
0x4a30068e, 0x4a32068f, 0x4a340690, 0x4a360691, 0x4a380692, 0x4a3a0693, 0x4a3c0694, 0x4a3e0695,
0x4a400696, 0x4a420697, 0x4a440698, 0x4a460699, 0x4a48069a, 0x4a4a069b, 0x4a4c069c, 0x4a4e069d,
0x4a50069e, 0x4a52069f,
};
constexpr std::uint32_t MoveM0BoundedS8 = 0x877c8308u;
constexpr std::uint32_t MoveM0S9 = 0xbefc0309u;
constexpr std::uint32_t MovrelsV4V10 = 0x7e08870au;
std::vector<std::uint32_t> NarrowingProgram(std::uint32_t unbounded) {
std::vector<std::uint32_t> code(WindowInit.begin(), WindowInit.end());
code.push_back(MoveM0BoundedS8);
code.push_back(MovrelsV4V10);
code.push_back(0xe0701ffcu);
code.push_back(0x80010401u);
code.push_back(MoveM0S9);
for (std::uint32_t read = 0; read < unbounded; ++read) {
const std::uint32_t destination = 42u + read % 100u;
code.push_back((MovrelsV4V10 & ~(0xffu << 17u)) | (destination << 17u));
code.push_back(0xe0701000u | ((read * 4u) & 0xfffu));
code.push_back(0x80010001u | destination << 8u);
}
code.push_back(EndProgram);
return code;
}
std::vector<std::uint32_t> RecompileNarrowing(std::uint32_t unbounded) {
std::array<std::uint32_t, 10> userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x40u, 0x31016facu, 3u, 5u};
return RecompileProgram(NarrowingProgram(unbounded), userData);
}
struct Narrowing {
std::uint32_t functionVariables = 0;
std::uint32_t switchCases = 0;
};
Narrowing ScanNarrowing(std::span<const std::uint32_t> words) {
Narrowing result;
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> spv::WordCountShift;
const auto op = static_cast<spv::Op>(words[cursor] & spv::OpCodeMask);
Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction");
if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction) ++result.functionVariables;
if (op == spv::OpSwitch) result.switchCases += (count - 3u) / 2u;
cursor += count;
}
return result;
}
IrProgram TranslateStage(ShaderStageKind stage, std::uint32_t waveSize, std::uint32_t base = 0u, std::uint32_t fetchStart = 0u, std::uint32_t fetchCount = 0u) {
ShaderComputeInputInfo compute{};
compute.threadIdsNum = 3;
@@ -358,6 +409,19 @@ int main(int argc, char** argv) {
return twice == single && single >= 3u;
});
passed &= run("a window with a few unproven relative reads keeps its unindexed slots out of the array", [] {
const auto few = ScanNarrowing(RecompileNarrowing(2));
if (few.functionVariables <= 8u || few.switchCases == 0u) std::fprintf(stderr, " two unproven reads: %u function variables, %u switch cases\n", few.functionVariables, few.switchCases);
return few.functionVariables > 8u && few.switchCases != 0u;
});
passed &= run("a window with hundreds of unproven relative reads does not grow a switch for each of them", [] {
const auto many = ScanNarrowing(RecompileNarrowing(300));
const auto few = ScanNarrowing(RecompileNarrowing(2));
if (many.switchCases > 4096u || many.functionVariables >= few.functionVariables) std::fprintf(stderr, " 300 unproven reads: %u function variables, %u switch cases (2 reads: %u, %u)\n", many.functionVariables, many.switchCases, few.functionVariables, few.switchCases);
return many.switchCases <= 4096u && many.functionVariables < few.functionVariables;
});
if (passed) std::puts("Vector window shape and SSA control tests passed");
return passed ? 0 : 1;
}
@@ -58,8 +58,36 @@ template<class TAction> void fails(TAction&& fn) {
Require(failed, "invalid vertex input was accepted");
}
void descriptorDiagnostics(const Context& ctx) {
auto input = attribute(7);
input.resource.fields = {0x89abcde0u, 0x0010abcdu, 0x12345678u, 0x0104dfacu};
Require(BuildVertexInputLayout(ctx, std::span(&input, 1)).attributes.size() == 1, "valid diagnostic control was rejected");
struct Case {
unsigned field;
std::uint32_t bits;
const char* words;
};
constexpr std::array cases{
Case{1, 0x80000000u, "V#=[89abcde0 8010abcd 12345678 0104dfac]"},
Case{3, 0x00800000u, "V#=[89abcde0 0010abcd 12345678 0184dfac]"},
Case{3, 0x40000000u, "V#=[89abcde0 0010abcd 12345678 4104dfac]"},
Case{3, 0x80000000u, "V#=[89abcde0 0010abcd 12345678 8104dfac]"},
Case{3, 0xc0800000u, "V#=[89abcde0 0010abcd 12345678 c184dfac]"}
};
for (const auto& value : cases) {
auto described = input;
described.resource.fields[value.field] |= value.bits;
const auto before = described.resource.fields;
const std::string message(VertexDescriptorFlagsMessage(described).data());
Require(message.find("unsupported vertex buffer descriptor flags at location 7") != std::string::npos && message.find(value.words) != std::string::npos,
"vertex flag message did not identify the attribute and all four descriptor words: " + message);
Require(described.resource.fields == before, "formatting changed the descriptor");
}
}
void run() {
auto ctx = context();
descriptorDiagnostics(ctx);
VertexInputCache cache(ctx, 2);
std::array attributes{attribute(0), attribute(1)};
auto first = get(cache, ctx, 1, attributes);
@@ -15,6 +15,7 @@
#ifdef __GLIBC__
#include <malloc.h>
#endif
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
@@ -35,6 +36,9 @@ constexpr std::size_t Block = 65536;
#ifdef _WIN32
constexpr wchar_t HostHoleMode[] = L"--host-hole";
constexpr wchar_t ResidentRemapMode[] = L"--resident-direct-remap";
constexpr wchar_t RewatchedSharedMode[] = L"--rewatched-direct-shared";
constexpr wchar_t ImportAliasSharedMode[] = L"--import-alias-shared";
constexpr wchar_t SharedGenerationsMode[] = L"--compared-shared-generations";
#endif
void Require(bool condition, const std::string& message) {
@@ -736,6 +740,243 @@ void CheckRewatch() {
GuestArena::GuestArenaRelease_nid_postfix(memory, 4 * Block);
}
void CheckRewatchedDirectMemoryComparedWhenShared() {
constexpr std::size_t bytes = 2 * Block;
auto* memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(2 * bytes, Block));
Require(memory != nullptr, "cannot reserve the compared sharing fixture");
struct Cleanup {
std::uint8_t* memory;
HANDLE section = nullptr;
~Cleanup() {
GuestArena::GuestArenaReset_nid_postfix(memory, 2 * bytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, 2 * bytes);
if (section == nullptr) return;
GuestArena::GuestArenaForgetSection_nid_postfix(section);
CloseHandle(section);
}
} cleanup{memory};
cleanup.section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, bytes, nullptr);
Require(cleanup.section != nullptr, "cannot create the compared sharing backing");
const auto base = reinterpret_cast<std::uint64_t>(memory);
const auto native = [](const std::uint8_t* address) {
MEMORY_BASIC_INFORMATION info{};
Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "cannot query the compared sharing fixture");
return info;
};
GuestArena::GuestArenaMapDirect_nid_postfix(memory, bytes, cleanup.section, 0, PAGE_READWRITE);
Require(native(memory).Type == MEM_PRIVATE && Watched(base, bytes), "the direct memory is not resident and watched");
Unwatch(base, Block);
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> imported{{{base, base + Block}}};
Rewatch(imported);
Require(Watched(base, Block), "the rewatched direct memory is not watched");
BumpCollectEpoch();
Require(CollectWritesUncached(base, Block) != 0, "rewatched resident direct memory is not collected");
GuestArena::GuestArenaMapDirect_nid_postfix(memory + bytes, bytes, cleanup.section, 0, PAGE_READWRITE);
Require(native(memory).Type == MEM_MAPPED && native(memory + bytes).Type == MEM_MAPPED, "a second mapping kept the direct memory resident");
BumpCollectEpoch();
Require(CollectWritesUncached(base, Block) == 0 && CollectWritesUncached(base + bytes, Block) == 0, "rewatched direct memory was collected once shared");
Require(native(memory).Protect == PAGE_READWRITE && native(memory + bytes).Protect == PAGE_READWRITE, "rewatched direct memory was armed once shared");
memory[8] = 0x5a;
Require(memory[bytes + 8] == 0x5a, "the second mapping does not see a store into the first");
Require(CollectWritesUncached(base + Block, Block) != 0, "shared direct memory that never left the watch stopped being collected");
std::cout << "rewatched direct memory: compared once shared, not armed; other shared direct memory still collected\n";
}
struct ComparedSharing {
static constexpr std::size_t bytes = 2 * Block;
std::uint8_t* memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(2 * bytes, Block));
HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, bytes, nullptr);
std::uint64_t base = reinterpret_cast<std::uint64_t>(memory);
ComparedSharing() {
Require(memory != nullptr && section != nullptr, "cannot create the compared sharing generation fixture");
GuestArena::GuestArenaForgetSection_nid_postfix(section);
GuestArena::GuestArenaMapDirect_nid_postfix(memory, bytes, section, 0, PAGE_READWRITE);
Require(Watched(base, bytes), "the compared sharing generation fixture is not watched");
Unwatch(base, Block);
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> imported{{{base, base + Block}}};
Rewatch(imported);
Require(Watched(base, Block), "the rewatched direct memory of the generation fixture is not watched");
BumpCollectEpoch();
Require(CollectWritesUncached(base, Block) != 0, "rewatched resident direct memory of the generation fixture is not collected");
}
~ComparedSharing() {
GuestArena::GuestArenaReset_nid_postfix(memory, 2 * bytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, 2 * bytes);
GuestArena::GuestArenaForgetSection_nid_postfix(section);
CloseHandle(section);
}
ComparedSharing(const ComparedSharing&) = delete;
ComparedSharing& operator=(const ComparedSharing&) = delete;
void Share() {
GuestArena::GuestArenaMapDirect_nid_postfix(memory + bytes, bytes, section, 0, PAGE_READWRITE);
MEMORY_BASIC_INFORMATION info{};
Require(VirtualQuery(memory, &info, sizeof(info)) == sizeof(info) && info.Type == MEM_MAPPED, "a second mapping kept the generation fixture resident");
}
std::uint64_t Collect(std::uint64_t address) {
BumpCollectEpoch();
return CollectWrites(address, Block);
}
};
void CheckComparedSharingGenerations() {
{
ComparedSharing fixture;
const auto g0 = TrackerGeneration();
Require(UnchangedSince(fixture.base, Block, g0) && !WrittenSince(fixture.base, Block, g0), "rewatched resident direct memory reads as written before any store");
fixture.memory[16] = 0x21;
Require(fixture.Collect(fixture.base) != 0 && !UnchangedSince(fixture.base, Block, g0) && WrittenSince(fixture.base, Block, g0), "a store into rewatched resident direct memory was missed before it was shared");
const auto cached = TrackerGeneration();
Require(UnchangedSince(fixture.base, Block, cached), "rewatched resident direct memory reads as changed after its collect");
fixture.Share();
fixture.memory[8] = 0x5a;
Require(fixture.memory[ComparedSharing::bytes + 8] == 0x5a, "the second mapping does not see the store into the generation fixture");
Require(fixture.Collect(fixture.base) == 0, "rewatched direct memory was collected once shared");
Require(!UnchangedSince(fixture.base, Block, cached), "a store into rewatched direct memory once shared reads as unchanged since a generation before the sharing");
Require(WrittenSince(fixture.base, Block, cached), "a store into rewatched direct memory once shared reads as not written since a generation before the sharing");
}
{
ComparedSharing fixture;
const auto g0 = TrackerGeneration();
fixture.Share();
Require(fixture.Collect(fixture.base) == 0, "rewatched direct memory was collected once shared, before any store");
Require(!UnchangedSince(fixture.base, Block, g0) && WrittenSince(fixture.base, Block, g0), "sharing rewatched direct memory did not report it written once");
const auto g1 = TrackerGeneration();
Require(fixture.Collect(fixture.base) == 0 && fixture.Collect(fixture.base) == 0, "rewatched direct memory was collected again once shared");
std::array<std::uint64_t, 1> generations{g1};
std::array<std::uint8_t, 1> changed{};
std::array<std::uint8_t, 1> cpu{};
Require(!WrittenSince(fixture.base, Block, g1) && ChangedBlocks(fixture.base, Block, generations, changed, cpu) && changed[0] == BlockUnchanged && cpu[0] == 0, "refused collects of shared compared memory reported it written again without a store");
Require(!UnchangedSince(fixture.base, Block, g1), "shared compared memory read as unchanged since a generation taken after a refused collect");
const auto neighbour = fixture.Collect(fixture.base + Block);
const auto spanned = std::max(fixture.Collect(fixture.base), neighbour);
Require(spanned != 0 && UnchangedSince(fixture.base + Block, Block, spanned), "shared direct memory that never left the watch is not collected beside compared memory");
fixture.memory[24] = 0x3c;
Require(fixture.Collect(fixture.base) == 0, "rewatched direct memory was collected once shared, after a store");
Require(!UnchangedSince(fixture.base, Block, g1), "a store into shared compared memory reads as unchanged since a generation taken after a refused collect");
Require(!UnchangedSince(fixture.base, Block, spanned), "a store into shared compared memory reads as unchanged since the newest generation of a collect over several ranges");
Require(fixture.Collect(fixture.base + Block) != 0 && UnchangedSince(fixture.base + Block, Block, spanned), "a refused collect of compared memory made its never-compared neighbour read as changed");
const std::array<UnchangedQuery, 2> queries{{{fixture.base + Block, Block, spanned}, {fixture.base, Block, spanned}}};
Require(!UnchangedSinceAll(queries), "a batch of queries over shared compared memory read as unchanged");
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> committed{{{fixture.base, fixture.base + Block}}};
Require(!UnchangedCommittedSince(committed, spanned), "committed ranges over shared compared memory read as unchanged");
}
std::cout << "shared compared memory: reported written once at the sharing, never unchanged since an older generation, its neighbour still trusted\n";
}
void CheckImportedDirectMemoryComparedWhenShared() {
constexpr std::size_t bytes = 2 * Block;
auto* memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(2 * bytes, Block));
Require(memory != nullptr, "cannot reserve the imported sharing fixture");
struct Cleanup {
std::uint8_t* memory;
HANDLE section = nullptr;
~Cleanup() {
GuestArena::GuestArenaReset_nid_postfix(memory, 2 * bytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, 2 * bytes);
if (section == nullptr) return;
GuestArena::GuestArenaForgetSection_nid_postfix(section);
CloseHandle(section);
}
} cleanup{memory};
cleanup.section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, bytes, nullptr);
Require(cleanup.section != nullptr, "cannot create the imported sharing backing");
const auto base = reinterpret_cast<std::uint64_t>(memory);
const auto native = [](const std::uint8_t* address) {
MEMORY_BASIC_INFORMATION info{};
Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "cannot query the imported sharing fixture");
return info;
};
GuestArena::GuestArenaMapDirect_nid_postfix(memory, bytes, cleanup.section, 0, PAGE_READWRITE);
Require(native(memory).Type == MEM_PRIVATE && Watched(base, bytes), "the imported direct memory is not resident and watched");
Require(ImportWatched(base, Block, [] { return true; }), "the watched import of resident direct memory failed");
Require(Watched(base, Block), "a watched import of resident direct memory left the write watch");
BumpCollectEpoch();
Require(CollectWritesUncached(base, Block) != 0, "imported resident direct memory is not collected");
GuestArena::GuestArenaMapDirect_nid_postfix(memory + bytes, bytes, cleanup.section, 0, PAGE_READWRITE);
Require(native(memory).Type == MEM_MAPPED && native(memory + bytes).Type == MEM_MAPPED, "a second mapping kept the imported direct memory resident");
BumpCollectEpoch();
Require(CollectWritesUncached(base, Block) == 0 && CollectWritesUncached(base + bytes, Block) == 0, "imported direct memory was collected once shared");
Require(native(memory).Protect == PAGE_READWRITE && native(memory + bytes).Protect == PAGE_READWRITE, "imported direct memory was armed once shared");
memory[8] = 0x5a;
Require(memory[bytes + 8] == 0x5a, "the second mapping does not see a store into the imported first");
Require(CollectWritesUncached(base + Block, Block) != 0, "shared direct memory that was never imported stopped being collected");
std::cout << "imported direct memory: compared once shared, not armed; unimported shared direct memory still collected\n";
}
void CheckImportAliasDemotedComparedWhenShared() {
constexpr std::size_t page = 0x4000;
auto* memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(5 * Block, Block));
Require(memory != nullptr, "cannot reserve the import alias fixture");
struct Cleanup {
std::uint8_t* memory;
std::array<HANDLE, 3> sections{};
std::array<void*, 3> aliases{};
~Cleanup() {
for (const auto alias : aliases) GuestArena::GuestArenaUnmapAlias_nid_postfix(alias);
GuestArena::GuestArenaReset_nid_postfix(memory, 5 * Block);
GuestArena::GuestArenaRelease_nid_postfix(memory, 5 * Block);
for (const auto section : sections) {
if (section == nullptr) continue;
GuestArena::GuestArenaForgetSection_nid_postfix(section);
CloseHandle(section);
}
}
} cleanup{memory};
for (auto& section : cleanup.sections) {
section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, Block, nullptr);
Require(section != nullptr, "cannot create the import alias backing");
GuestArena::GuestArenaForgetSection_nid_postfix(section);
}
const auto base = reinterpret_cast<std::uint64_t>(memory);
const auto native = [](const std::uint8_t* address) {
MEMORY_BASIC_INFORMATION info{};
Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "cannot query the import alias fixture");
return info;
};
const auto clearingCollect = [](std::uint64_t address) {
std::array<void*, 4> pages{};
std::size_t count = pages.size();
return GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast<std::uintptr_t>(address), page, pages.data(), &count, true);
};
GuestArena::GuestArenaMapDirect_nid_postfix(memory, page, cleanup.sections[0], 0, PAGE_READWRITE);
Require(native(memory).Type == MEM_PRIVATE && Watched(base, page), "the aliased direct memory is not resident and watched");
BumpCollectEpoch();
Require(CollectWritesUncached(base, page) != 0, "resident direct memory is not collected before its import");
cleanup.aliases[0] = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base), page);
Require(cleanup.aliases[0] != nullptr && native(memory).Type == MEM_MAPPED, "the import alias did not demote the resident run");
Require(ImportWatched(base, page, [] { return true; }) && Watched(base, page), "the import of aliased direct memory did not stay watched");
Require(native(memory).Protect == PAGE_READWRITE, "memory an import's alias demoted was armed by the import");
Require(!clearingCollect(base), "a clearing collect over memory an import's alias demoted was not refused");
Require(native(memory).Protect == PAGE_READWRITE, "memory an import's alias demoted was armed by a clearing collect");
BumpCollectEpoch();
Require(CollectWritesUncached(base, page) == 0, "memory an import's alias demoted was collected once shared");
memory[8] = 0x5a;
Require(static_cast<const std::uint8_t*>(cleanup.aliases[0])[8] == 0x5a, "the import alias does not see a store into the guest view");
GuestArena::GuestArenaMapDirect_nid_postfix(memory + Block, page, cleanup.sections[1], 0, PAGE_READWRITE);
GuestArena::GuestArenaMapDirect_nid_postfix(memory + 2 * Block, page, cleanup.sections[1], 0, PAGE_READWRITE);
Require(native(memory + Block).Type == MEM_MAPPED && native(memory + 2 * Block).Type == MEM_MAPPED, "a second mapping kept the control direct memory resident");
cleanup.aliases[1] = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base + Block), page);
Require(cleanup.aliases[1] != nullptr, "the control import alias was refused");
Require(ImportWatched(base + Block, page, [] { return true; }) && Watched(base + Block, page), "the control import did not stay watched");
Require(clearingCollect(base + Block), "memory shared by a second mapping before its import alias stopped being collected");
Require(native(memory + Block).Protect == PAGE_READONLY && native(memory + 2 * Block).Protect == PAGE_READONLY, "memory shared by a second mapping before its import alias was not armed");
GuestArena::GuestArenaMapDirect_nid_postfix(memory + 3 * Block, page, cleanup.sections[2], 0, PAGE_READWRITE);
auto* failed = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base + 3 * Block), page);
Require(failed != nullptr && native(memory + 3 * Block).Type == MEM_MAPPED, "the alias of a failed import did not demote the resident run");
GuestArena::GuestArenaUnmapAlias_nid_postfix(failed);
GuestArena::GuestArenaMapDirect_nid_postfix(memory + 4 * Block, page, cleanup.sections[2], 0, PAGE_READWRITE);
cleanup.aliases[2] = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base + 3 * Block), page);
Require(cleanup.aliases[2] != nullptr, "the import alias after a second mapping was refused");
Require(ImportWatched(base + 3 * Block, page, [] { return true; }) && Watched(base + 3 * Block, page), "the import after a second mapping did not stay watched");
Require(clearingCollect(base + 3 * Block), "memory a second mapping shared after an earlier alias demoted it stopped being collected");
Require(native(memory + 3 * Block).Protect == PAGE_READONLY && native(memory + 4 * Block).Protect == PAGE_READONLY, "memory a second mapping shared after an earlier alias demoted it was not armed");
std::cout << "direct memory an import's alias demoted: compared once shared, not armed; memory shared before the alias still collected\n";
}
void CheckResidentDirectMappingReuse() {
constexpr std::size_t bytes = 5 * Block;
auto* memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block));
@@ -1532,6 +1773,19 @@ int main() {
CheckResidentDirectMappingReuse();
return 0;
}
if (std::wstring_view(GetCommandLineW()).ends_with(RewatchedSharedMode)) {
CheckRewatchedDirectMemoryComparedWhenShared();
CheckImportedDirectMemoryComparedWhenShared();
return 0;
}
if (std::wstring_view(GetCommandLineW()).ends_with(ImportAliasSharedMode)) {
CheckImportAliasDemotedComparedWhenShared();
return 0;
}
if (std::wstring_view(GetCommandLineW()).ends_with(SharedGenerationsMode)) {
CheckComparedSharingGenerations();
return 0;
}
#endif
CheckSharedBlock();
CheckStoredOverUsesPageEvidence();
@@ -0,0 +1,155 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <array>
#include <cstdio>
#include <iostream>
#include <cstdint>
#include <span>
#include <string>
#include <stdexcept>
#include <string_view>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 16;
constexpr std::uint32_t WaveSize = 64;
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
};
alignas(256) constexpr std::array<std::uint32_t, 11> PixelCode{
0x7e0802ff, 0x3e4ccccd, 0x7e0a02ff, 0x3ecccccd, 0x7e0c02ff, 0x3f19999a, 0x7e0e02ff, 0x3f4ccccd,
0xf800180f, 0x07060504, 0xbf810000,
};
constexpr std::array<std::uint8_t, 4> Source{0x33, 0x66, 0x99, 0xcc};
constexpr std::array<std::array<float, 4>, 3> Vertices{{
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
}};
alignas(256) std::array<std::array<float, 4>, 3> VertexStorage = Vertices;
std::array<std::byte, Width * Height * 4> Before{};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu};
}
void Draw(AgcDriver::VulkanDevice& device, std::uint8_t rop) {
for (std::size_t index = 0; index < Pixels.size(); ++index) Before[index] = Pixels[index] = static_cast<std::byte>((index * 37u + 11u) & 0xffu);
const auto target = device.Target();
std::vector<std::uint32_t> vertexUserData(4, 0u);
const auto vertexBuffer = BufferDescriptor(VertexStorage.data(), 16u, static_cast<std::uint32_t>(VertexStorage.size()));
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
ShaderRecompiler::RecompileRequest vertex{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
{WaveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
target,
{0, 0, 0, 64}
};
vertex.useCache = false;
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
ShaderRecompiler::ShaderPixelStageInfo pixel{};
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY);
pixel.posX = true;
pixel.posY = true;
pixel.targetOutputMode[0] = 9;
pixel.targetExportMapping.fill(0xe4u);
const std::vector<std::uint32_t> pixelUserData(8, 0u);
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}};
ShaderRecompiler::RecompileRequest fragment{
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
{WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u};
state.colors = {state.color};
state.hasColorTarget = true;
state.renderExtent = {Width, Height};
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
state.negativeOneToOne = false;
state.colorRop = rop;
state.scissor = {{0, 0}, {Width, Height}};
state.cullMode = VK_CULL_MODE_NONE;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.blend.colorWriteMask = 15;
state.blends = {state.blend};
state.blendConstants = {};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(VertexStorage.size()), 0, 1, 0, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}
std::uint8_t Combine(std::uint8_t rop, std::uint8_t destination, std::uint8_t source) {
switch (rop) {
case 0xcc: return source;
case 0x5a: return static_cast<std::uint8_t>(destination ^ source);
case 0x88: return static_cast<std::uint8_t>(destination & source);
case 0xee: return static_cast<std::uint8_t>(destination | source);
default: throw std::runtime_error("the test has no expectation for this ROP");
}
}
void Check(std::uint8_t rop, std::string_view what) {
for (std::uint32_t y = 0; y < Height; ++y) {
for (std::uint32_t x = 0; x < Width; ++x) {
for (std::uint32_t channel = 0; channel < 4; ++channel) {
const auto index = (y * Width + x) * 4u + channel;
const auto expected = Combine(rop, static_cast<std::uint8_t>(Before[index]), Source[channel]);
const auto actual = static_cast<std::uint8_t>(Pixels[index]);
Require(actual == expected, std::string(what) + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected));
}
}
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
for (const std::uint8_t rop : {0xcc, 0x5a, 0x88, 0xee, 0xcc}) {
try {
Draw(*device, rop);
} catch (const std::exception& error) {
Require(std::string_view(error.what()).find("device does not support color logic operations") != std::string_view::npos, std::string("unexpected rejection: ") + error.what());
std::puts("skipped, the device has no logicOp feature");
return VulkanTestSkipped;
}
char what[48];
std::snprintf(what, sizeof(what), "ROP3 0x%02x into an 8_8_8_8 unorm target", rop);
Check(rop, what);
}
std::puts("color logic operation tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
@@ -0,0 +1,166 @@
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <initializer_list>
#include <iostream>
#include <span>
#include <string>
#include <utility>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
constexpr std::uint32_t Width = 64;
constexpr std::uint32_t Height = 4;
constexpr float DrawnDepth = 0.25f;
constexpr float MemoryDepth = 0.75f;
constexpr float Untouched = -1.0f;
alignas(256) constexpr std::array<std::uint32_t, 16> VertexCode{
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u,
};
alignas(256) constexpr std::array<std::uint32_t, 4> WhitePixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u};
alignas(256) std::array<float, Width * Height> CopyTarget{};
alignas(256) std::array<float, Width * Height> ReferenceTarget{};
constexpr std::size_t DepthFloats = 16384;
constexpr std::size_t DepthAlignment = 65536;
std::array<float, DepthFloats + DepthAlignment / sizeof(float)> DepthStorage{};
const std::span<float, DepthFloats> DepthMemory{reinterpret_cast<float*>((reinterpret_cast<std::uintptr_t>(DepthStorage.data()) + DepthAlignment - 1) & ~(DepthAlignment - 1)), DepthFloats};
struct Header {
Shader shader{};
ShaderUserData userData{};
std::array<ShaderRegister, 4> registers{};
};
alignas(8) Header VertexHeader{};
alignas(8) Header PixelHeader{};
void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
header.shader.file_header = 0x34333231u;
header.shader.version = 0x18u;
header.shader.header_size = sizeof(Header);
header.shader.shader_size = static_cast<std::uint32_t>(bytes);
header.shader.code = code;
header.shader.type = type;
header.shader.user_data = &header.userData;
if (type == 1) {
const auto address = reinterpret_cast<std::uintptr_t>(code);
header.registers = {{{0x008, static_cast<std::uint32_t>(address >> 8u)}, {0x009, static_cast<std::uint32_t>(address >> 40u)}, {0x00b, 0}, {0x00a, 0}}};
header.shader.sh_registers = header.registers.data();
header.shader.num_sh_registers = header.registers.size();
}
AgcDriverRegisterShader_nid_postfix(&header.shader);
}
void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) {
commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u));
commands.insert(commands.end(), payload);
}
struct DrawOptions {
float* color = nullptr;
std::uint32_t targetMask = 1;
std::uint32_t renderControl = 0;
std::uint32_t depthControl = 0;
};
void AppendDraw(std::vector<std::uint32_t>& commands, const DrawOptions& options) {
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCode.data());
const auto pixel = reinterpret_cast<std::uintptr_t>(WhitePixelCode.data());
const auto color = reinterpret_cast<std::uintptr_t>(options.color);
const auto depth = reinterpret_cast<std::uintptr_t>(DepthMemory.data());
Require(depth % DepthAlignment == 0, "the test depth plane is not 64 KiB aligned");
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
{0x2d5, 0x2000},
{0x1b3, 0x2}, {0x1b4, 0x2}, {0x1b6, 0}, {0x207, 0}, {0x200, options.depthControl}, {0x203, 0},
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x293, 0},
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
{0x8e, options.targetMask}, {0x8f, 0xf}, {0x202, 0xcc0010},
{0x1c4, 0}, {0x1c5, 1}, {0x1c3, 4}, {0x31c, 0x8710},
{0x31b, 0}, {0x31d, 0}, {0x3b0, ((Width - 1u) << 14u) | (Height - 1u)},
{0x3b8, 0x9000000}, {0x1e0, 0},
{0xc, 0}, {0xd, (Height << 16u) | Width},
{0x81, 0x80000000}, {0x82, (Height << 16u) | Width},
{0x90, 0x80000000}, {0x91, (Height << 16u) | Width},
{0x94, 0x80000000}, {0x95, (Height << 16u) | Width},
{0x10f, std::bit_cast<std::uint32_t>(Width / 2.0f)}, {0x110, std::bit_cast<std::uint32_t>(Width / 2.0f)},
{0x111, std::bit_cast<std::uint32_t>(-(Height / 2.0f))}, {0x112, std::bit_cast<std::uint32_t>(Height / 2.0f)},
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, std::bit_cast<std::uint32_t>(DrawnDepth)},
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)},
{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0},
{0x318, static_cast<std::uint32_t>(color >> 8u)}, {0x390, static_cast<std::uint32_t>(color >> 40u)}, {0x292, 2},
{0x000, options.renderControl}, {0x002, 0}, {0x007, ((Height - 1u) << 16u) | (Width - 1u)},
{0x008, 0}, {0x009, std::bit_cast<std::uint32_t>(1.0f)}, {0x00a, 0}, {0x00b, 0},
{0x010, 3}, {0x011, 0},
{0x012, static_cast<std::uint32_t>(depth >> 8u)}, {0x014, static_cast<std::uint32_t>(depth >> 8u)},
{0x01a, static_cast<std::uint32_t>(depth >> 40u)}, {0x01c, static_cast<std::uint32_t>(depth >> 40u)},
};
const std::vector<std::pair<std::uint32_t, std::uint32_t>> shader{
{0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 1u << 1u}, {0x8c, 0},
{0x008, static_cast<std::uint32_t>(pixel >> 8u)}, {0x009, static_cast<std::uint32_t>(pixel >> 40u)}, {0x00a, 0}, {0x00b, 0},
};
Append(commands, 0x79, {0x242, 4});
for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value});
for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value});
Append(commands, 0x2d, {3, 2});
}
void CopyCoversTheDraw() {
CopyTarget.fill(Untouched);
ReferenceTarget.fill(Untouched);
std::ranges::fill(DepthMemory, MemoryDepth);
Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2);
Registered(PixelHeader, WhitePixelCode.data(), sizeof(WhitePixelCode), 1);
std::vector<std::uint32_t> commands;
AppendDraw(commands, {.color = CopyTarget.data(), .targetMask = 0, .depthControl = 0x76});
AppendDraw(commands, {.color = CopyTarget.data(), .renderControl = 0x84});
AppendDraw(commands, {.color = ReferenceTarget.data()});
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
Require(sceAgcDriverSubmitDcb(&packet) == 0, "the depth write, DB depth copy and reference draws were not submitted");
AgcDriverWaitIdle_nid_postfix();
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(CopyTarget.data()), sizeof(CopyTarget));
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(ReferenceTarget.data()), sizeof(ReferenceTarget));
std::size_t covered = 0;
for (std::size_t index = 0; index < CopyTarget.size(); ++index) {
const bool drawn = ReferenceTarget[index] == 1.0f;
Require(drawn || ReferenceTarget[index] == Untouched, "the reference draw wrote " + std::to_string(ReferenceTarget[index]) + " at texel " + std::to_string(index));
covered += drawn;
const auto expected = drawn ? DrawnDepth : Untouched;
Require(CopyTarget[index] == expected, "the DB depth copy holds " + std::to_string(CopyTarget[index]) + " at texel " + std::to_string(index) + ", expected " + std::to_string(expected) + (drawn ? " (the triangle covers it)" : " (outside the triangle)"));
}
Require(covered != 0 && covered != CopyTarget.size(), "the triangle covered " + std::to_string(covered) + " of " + std::to_string(CopyTarget.size()) + " texels, expected an edge through the target");
}
}
int main() {
try {
{
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
}
CopyCoversTheDraw();
AgcDriverShutdown_nid_postfix();
std::puts("the DB depth copy wrote the drawn depth exactly where its draw rasterized");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
AgcDriverShutdown_nid_postfix();
return 1;
}
}
Loaded 100 of 537 files, more files were not shown because too many files have changed in this diff. Show more