Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions core/libs/prx/libSceAgcDriver/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2164,6 +2164,11 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes)
set_tests_properties(agc_driver_copy_input_lifetime PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_CAPTURE_TRACE=1;APS5_CAPTURE_GPU_COPIES=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
if(WIN32)
add_test(NAME agc_copy_input_failure_lifetime COMMAND agc_driver_graphics_tests --copy-input-failure-lifetime)
set_tests_properties(agc_copy_input_failure_lifetime PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_CAPTURE_TRACE=1;APS5_CAPTURE_GPU_COPIES=1" WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_copy_input_kept_bytes COMMAND agc_driver_graphics_tests --copy-input-kept-bytes)
add_test(NAME agc_depth_copy_kept_bytes COMMAND agc_driver_graphics_tests --depth-copy-kept-bytes)
set_tests_properties(agc_copy_input_kept_bytes agc_depth_copy_kept_bytes PROPERTIES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4917,9 +4917,11 @@ void GuestBufferMemory::recordGpuCopies(std::span<Region* const> copies, bool ad
expected.assign(snapshotBytes.begin(), snapshotBytes.end());
}
}
recorder->Keep(region->buffer);
CopyBuffer(context, commands, copySource, copyOffset, region->buffer->Handle(), 0, bytes);
if (!expected.empty()) {
auto readback = std::make_shared<Buffer>(context, expected.size(), VK_BUFFER_USAGE_TRANSFER_DST_BIT);
recorder->Keep(readback, readback->Bytes().size());
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
CopyBuffer(context, commands, region->buffer->Handle(), 0, readback->Handle(), 0, bytes);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT | VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_HOST_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
Expand All @@ -4935,11 +4937,6 @@ void GuestBufferMemory::recordGpuCopies(std::span<Region* const> copies, bool ad
});
}
if (inputSnapshot == nullptr) recorder->NotePendingRead(region->begin, static_cast<std::size_t>(bytes), Recorder::ReadKind::GpuCopy);
// Kept by the batch at record time, as every other recorded target is: the caller keeps
// the resources only after descriptor and pipeline work that may throw, and a released
// buffer would go back to the pool (reused or destroyed) under this copy command. The
// source is a live import or one retireImport already handed to this batch.
recorder->Keep(region->buffer);
region->snapshot.clear();
if (region->deviceLocal) traceStaged(region->begin, region->end, region->atomic);
if (profile) {
Expand Down
87 changes: 87 additions & 0 deletions core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5025,6 +5025,89 @@ void bufferBudgetPlacementTests() {
}
}

bool copyFailureArmed = false;

VKAPI_ATTR void VKAPI_CALL mockFailureCopy(VkCommandBuffer commands, VkBuffer source, VkBuffer destination, std::uint32_t count, const VkBufferCopy* regions) {
mockCmdCopyBuffer(commands, source, destination, count, regions);
if (copyFailureArmed && count == 1 && regions[0].size == 32 && (mock.bufferUsage.at(destination) & VK_BUFFER_USAGE_STORAGE_BUFFER_BIT) != 0) {
copyFailureArmed = false;
mock.memoryLimit = 0;
}
}

PFN_vkVoidFunction VKAPI_CALL mockCopyFailureProc(VkDevice device, const char* name) {
if (std::strcmp(name, "vkCmdCopyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(mockFailureCopy);
return mockProc(device, name);
}

void copyInputFailureLifetimeTests() {
#ifdef _WIN32
using namespace AgcDriver::Graphics;
constexpr std::size_t piece = 65536;
constexpr std::size_t bytes = 32;
constexpr auto properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
auto block = std::unique_ptr<std::byte[], void(*)(std::byte*)>(static_cast<std::byte*>(::operator new(piece, std::align_val_t{piece})), [](std::byte* value) { ::operator delete(value, std::align_val_t{piece}); });
std::fill_n(block.get(), piece, std::byte{0});
const auto base = reinterpret_cast<std::uint64_t>(block.get());
bool refused = false;
bool earlyReuse = false;
mock = MockVulkan{};
mock.deferFences = true;
{
auto context = mockContext();
context.device = makeHandle<VkDevice>();
context.deviceProc = mockCopyFailureProc;
context.bufferDeviceAddress = true;
context.hostImportAlignment = piece;
context.limits.minStorageBufferOffsetAlignment = 256;
context.limits.nonCoherentAtomSize = piece;
context.limits.maxMemoryAllocationCount = 4096;
SetImportWatch(context, ImportWatch::Unwatch);
const auto pool = GetBufferPool(context);
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block.get(), piece, true, false, true);
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
const auto unregister = [&](std::byte*) {
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block.get());
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
};
std::unique_ptr<std::byte, decltype(unregister)> registered(block.get(), unregister);
{
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
Recorder recorder(context);
recorder.Activate();
copyFailureArmed = true;
try {
GuestBufferMemory memory(context);
memory.AddReadable(base + 4, bytes);
memory.Upload(false);
} catch (const std::exception&) {
refused = true;
}
Require(refused && !copyFailureArmed && mock.bufferCopies.size() == 1, "diagnostic readback pressure was not exercised after the copy command");
const auto destination = mock.bufferCopies.front().destination;
std::vector<BufferAllocation> taken;
while (const auto allocation = pool->Take(bytes, usage, properties)) {
earlyReuse |= allocation->buffer == destination;
taken.push_back(*allocation);
}
for (const auto& allocation : taken) pool->Put(allocation);
mock.memoryLimit.reset();
recorder.Sync();
}
registered.reset();
ClearHostImports(context.device);
}
Require(mock.live == 0, "copy failure lifetime test leaked an object");
std::cout << "diagnostic pressure refused=" << refused << " destination_reusable_before_fence=" << earlyReuse << '\n';
Require(!earlyReuse, "recorded copy destination became reusable before its fence after readback pressure");
#else
throw std::runtime_error("the copy failure mock fixture requires Windows guest registration");
#endif
}

void copyInputKeptBytesTests() {
using namespace AgcDriver::Graphics;
constexpr std::size_t piece = 65536;
Expand Down Expand Up @@ -10185,6 +10268,10 @@ int main(int argc, char** argv) {
std::cout << "cached pipelines survive lazy buffer pool creation\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--copy-input-failure-lifetime") {
copyInputFailureLifetimeTests();
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--copy-input-kept-bytes") {
copyInputKeptBytesTests();
std::cout << "GPU-copy input snapshot accounting and retention passed\n";
Expand Down
155 changes: 155 additions & 0 deletions core/libs/prx/libSceAgcDriver/tests/execution/CopyInputLifetime.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#include "VulkanTestDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <memory>
#include <mutex>
#include <new>
#include <vector>

namespace {

namespace Graphics = AgcDriver::Graphics;
using Graphics::Require;

PFN_vkGetDeviceProcAddr originalProc = nullptr;
PFN_vkAllocateMemory originalAllocate = nullptr;
PFN_vkFreeMemory originalFree = nullptr;
PFN_vkCmdCopyBuffer originalCopy = nullptr;
bool refuseReadback = false;
bool copied = false;
unsigned refusals = 0;
unsigned copies = 0;
unsigned allocations = 0;
VkBuffer destination = VK_NULL_HANDLE;

VKAPI_ATTR VkResult VKAPI_CALL Allocate(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* callbacks, VkDeviceMemory* memory) {
if (refuseReadback && copied) {
++refusals;
return VK_ERROR_OUT_OF_DEVICE_MEMORY;
}
const auto result = originalAllocate(device, info, callbacks, memory);
if (result == VK_SUCCESS) ++allocations;
return result;
}

VKAPI_ATTR void VKAPI_CALL Free(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* callbacks) {
if (memory != VK_NULL_HANDLE) --allocations;
originalFree(device, memory, callbacks);
}

VKAPI_ATTR void VKAPI_CALL Copy(VkCommandBuffer commands, VkBuffer source, VkBuffer target, std::uint32_t count, const VkBufferCopy* regions) {
originalCopy(commands, source, target, count, regions);
if (!copied) {
Require(count == 1 && regions[0].size == 32 && regions[0].srcOffset == 0 && regions[0].dstOffset == 0, "expected one exact 32-byte input copy");
destination = target;
copied = true;
}
++copies;
}

PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) {
if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&Allocate);
if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&Free);
if (std::strcmp(name, "vkCmdCopyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&Copy);
return originalProc(device, name);
}

void Scenario(Graphics::Context context, bool fail) {
constexpr std::size_t blockBytes = 65536;
constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
constexpr auto properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
auto block = std::unique_ptr<std::byte[], void(*)(std::byte*)>(static_cast<std::byte*>(::operator new(blockBytes, std::align_val_t{blockBytes})), [](std::byte* value) { ::operator delete(value, std::align_val_t{blockBytes}); });
std::fill_n(block.get(), blockBytes, std::byte{0x11});
std::fill_n(block.get() + 4, 32, std::byte{0x5a});
GuestAllocations::Mutation().Add(block.get(), blockBytes, true, false, true);
const auto unregister = [&](std::byte*) { GuestAllocations::Mutation().Remove(block.get()); };
std::unique_ptr<std::byte, decltype(unregister)> registered(block.get(), unregister);
context.functions = nullptr;
context.deviceProc = &Resolve;
context.limits.minStorageBufferOffsetAlignment = 256;
context.limits.nonCoherentAtomSize = blockBytes;
context.bufferPool = std::make_shared<Graphics::BufferPool>(context);
copied = false;
copies = refusals = allocations = 0;
destination = VK_NULL_HANDLE;
refuseReadback = fail;
bool caught = false;
bool earlyReuse = false;
{
Graphics::Recorder recorder(context);
recorder.Activate();
try {
Graphics::GuestBufferMemory memory(context);
memory.AddReadable(reinterpret_cast<std::uintptr_t>(block.get()) + 4, 32);
memory.Upload(false);
} catch (const std::exception&) {
caught = true;
}
refuseReadback = false;
Require(copied && destination != VK_NULL_HANDLE, "the fixture did not record an input copy");
Require(fail ? caught && refusals != 0 && copies == 1 : !caught && refusals == 0 && copies == 2, "the diagnostic readback did not take the expected allocation path");
std::vector<Graphics::BufferAllocation> taken;
while (const auto allocation = context.bufferPool->Take(32, usage, properties)) {
earlyReuse |= allocation->buffer == destination;
taken.push_back(*allocation);
}
for (const auto& allocation : taken) context.bufferPool->Put(allocation);
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(recorder.Commands(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
recorder.Sync();
}
bool checked = false;
std::vector<Graphics::BufferAllocation> completed;
while (const auto allocation = context.bufferPool->Take(32, usage, properties)) {
if (allocation->buffer == destination) {
Require(allocation->mapping != nullptr, "the completed destination is not mapped");
const auto* bytes = static_cast<const std::byte*>(allocation->mapping);
Require(std::all_of(bytes, bytes + 32, [](std::byte value) { return value == std::byte{0x5a}; }), "GPU input copy data changed");
checked = true;
}
completed.push_back(*allocation);
}
for (const auto& allocation : completed) context.bufferPool->Put(allocation);
for (std::size_t index = 0; index < blockBytes; ++index) Require(block[index] == (index >= 4 && index < 36 ? std::byte{0x5a} : std::byte{0x11}), "the copy changed guest memory or surrounding guards");
registered.reset();
Graphics::ClearHostImports(context.device);
context.bufferPool.reset();
Require(allocations == 0, "the input copy leaked Vulkan allocations");
std::printf("copy-input lifetime: fail=%d refusals=%u early_reuse=%d GPU_data_checked=%d\n", fail, refusals, earlyReuse, checked);
Require(checked && !earlyReuse, "a recorded copy destination became reusable before its fence");
}

}

int main() {
try {
auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
auto context = device->graphicsContext();
if (context.hostImportAlignment == 0 || context.hostImportAlignment > 65536) {
Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the lifetime test requires host imports with at most 64 KiB alignment");
std::puts("skipped, suitable host imports are unavailable");
return VulkanTestSkipped;
}
Graphics::ClearHostImports(context.device);
originalProc = context.deviceProc;
originalAllocate = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory");
originalFree = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
originalCopy = context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer");
Scenario(context, false);
Scenario(context, true);
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
Loading