feat(agc): capture and replay compute pipeline compilation
This commit is contained in:
parent
0a8b1d32b1
commit
8fbd0eecc1
|
|
@ -111,6 +111,7 @@ add_custom_command(
|
|||
|
||||
set(agcDriverSources
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ComputePipelineCapture.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayFormat.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/DeviceAccess.cpp
|
||||
|
|
@ -1250,6 +1251,50 @@ endif()
|
|||
set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_shader_replay)
|
||||
|
||||
add_executable(agc_compute_pipeline_replay EXCLUDE_FROM_ALL tools/ComputePipelineReplay.cpp)
|
||||
target_include_directories(agc_compute_pipeline_replay PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/core/shader/recompiler
|
||||
${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}
|
||||
${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(agc_compute_pipeline_replay PRIVATE libSceAgcDriver libc)
|
||||
set_target_properties(agc_compute_pipeline_replay PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_compute_pipeline_replay)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(agc_compute_pipeline_capture_tests tests/ComputePipelineCapture.cpp Execution/src/ComputePipelineCapture.cpp)
|
||||
target_include_directories(agc_compute_pipeline_capture_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
set_target_properties(agc_compute_pipeline_capture_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
|
||||
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS)
|
||||
add_test_executable(agc_compute_pipeline_replay_tests tests/ComputePipelineReplay.cpp)
|
||||
target_include_directories(agc_compute_pipeline_replay_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_compute_pipeline_replay_tests PRIVATE libSceAgcDriver libc)
|
||||
set_target_properties(agc_compute_pipeline_replay_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_compute_pipeline_replay_tests)
|
||||
add_test(NAME agc_compute_pipeline_replay COMMAND agc_compute_pipeline_replay_tests)
|
||||
set_tests_properties(agc_compute_pipeline_replay PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77)
|
||||
add_dependencies(agc_compute_pipeline_replay_tests agc_compute_pipeline_replay agc_driver_branch_past_endpgm_tests)
|
||||
add_test(NAME agc_compute_pipeline_capture_run COMMAND ${CMAKE_COMMAND}
|
||||
-DFIXTURE=$<TARGET_FILE:agc_driver_branch_past_endpgm_tests>
|
||||
-DREPLAY=$<TARGET_FILE:agc_compute_pipeline_replay>
|
||||
-DOUTPUT_ROOT=${CMAKE_CURRENT_BINARY_DIR}/capture-tests
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineCaptureRun.cmake)
|
||||
set_tests_properties(agc_compute_pipeline_capture_run PROPERTIES TIMEOUT 30 SKIP_REGULAR_EXPRESSION "compute pipeline capture run: skipped, no usable Vulkan device")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(texture_format_lookup_tests ${LIBS_INCLUDE_DIR}/tests/TextureFormatLookup.cpp)
|
||||
target_include_directories(texture_format_lookup_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
struct ComputeDescriptorBinding {
|
||||
std::uint32_t binding = 0;
|
||||
VkDescriptorType type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
std::uint32_t count = 1;
|
||||
VkShaderStageFlags stages = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
VkDescriptorBindingFlags flags = 0;
|
||||
};
|
||||
|
||||
struct ComputeDescriptorSetLayout {
|
||||
VkDescriptorSetLayoutCreateFlags flags = 0;
|
||||
std::vector<ComputeDescriptorBinding> bindings;
|
||||
};
|
||||
|
||||
struct ComputeSpecialization {
|
||||
std::uint32_t id = 0;
|
||||
std::uint32_t value = 0;
|
||||
};
|
||||
|
||||
struct ComputePipelineCapture {
|
||||
static constexpr std::uint32_t Version = 1;
|
||||
static constexpr std::size_t MaxFileBytes = 20u * 1024u * 1024u;
|
||||
std::vector<std::uint32_t> spirv;
|
||||
std::vector<ComputeDescriptorSetLayout> sets;
|
||||
std::vector<VkPushConstantRange> pushConstants;
|
||||
std::vector<ComputeSpecialization> specialization;
|
||||
std::uint32_t requiredSubgroupSize = 0;
|
||||
VkPipelineCreateFlags flags = 0;
|
||||
VkPipelineShaderStageCreateFlags stageFlags = 0;
|
||||
std::string deviceName;
|
||||
std::uint32_t vendorId = 0;
|
||||
std::uint32_t deviceId = 0;
|
||||
std::uint32_t driverVersion = 0;
|
||||
std::uint32_t apiVersion = 0;
|
||||
std::array<std::uint8_t, VK_UUID_SIZE> pipelineCacheUuid{};
|
||||
|
||||
void Validate() const;
|
||||
std::vector<std::byte> Encode() const;
|
||||
static ComputePipelineCapture Decode(std::span<const std::byte> bytes);
|
||||
static ComputePipelineCapture Read(const std::filesystem::path& path);
|
||||
std::filesystem::path Write(const std::filesystem::path& directory) const;
|
||||
std::string Identity() const;
|
||||
};
|
||||
|
||||
struct ComputePipelineReplayResult {
|
||||
double moduleMilliseconds = 0;
|
||||
double layoutMilliseconds = 0;
|
||||
double pipelineMilliseconds = 0;
|
||||
double cacheMilliseconds = 0;
|
||||
double totalMilliseconds = 0;
|
||||
std::string deviceName;
|
||||
std::uint32_t vendorId = 0;
|
||||
std::uint32_t deviceId = 0;
|
||||
std::uint32_t driverVersion = 0;
|
||||
std::uint32_t apiVersion = 0;
|
||||
std::array<std::uint8_t, VK_UUID_SIZE> pipelineCacheUuid{};
|
||||
std::size_t initialCacheBytes = 0;
|
||||
std::vector<std::byte> cache;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Recipe.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
|
|
@ -31,6 +32,9 @@ struct RecordedDispatch;
|
|||
class VulkanDevice {
|
||||
public:
|
||||
explicit VulkanDevice(const PresentationWindow* window = nullptr);
|
||||
struct CompileOnly {};
|
||||
explicit VulkanDevice(CompileOnly);
|
||||
ComputePipelineReplayResult ReplayComputePipeline(const ComputePipelineCapture& capture, std::span<const std::byte> initialCache = {});
|
||||
~VulkanDevice();
|
||||
VulkanDevice(const VulkanDevice&) = delete;
|
||||
VulkanDevice& operator=(const VulkanDevice&) = delete;
|
||||
|
|
@ -296,6 +300,7 @@ public:
|
|||
void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {});
|
||||
|
||||
private:
|
||||
VulkanDevice(const PresentationWindow* window, bool compileOnly);
|
||||
// The device's Graphics::Context: a copy of the one built at setup (its instance functions
|
||||
// resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew.
|
||||
Graphics::Context graphicsContext() const;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,261 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp"
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace AgcDriver {
|
||||
namespace {
|
||||
|
||||
void RequireCapture(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(std::string("compute pipeline capture: ") + message);
|
||||
}
|
||||
|
||||
std::string Hash(std::span<const std::byte> bytes) {
|
||||
std::uint64_t hash = 14695981039346656037ull;
|
||||
for (const auto value : bytes) {
|
||||
hash ^= std::to_integer<std::uint8_t>(value);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
std::ostringstream result;
|
||||
result << std::hex << std::setfill('0') << std::setw(16) << hash;
|
||||
return result.str();
|
||||
}
|
||||
|
||||
class Encoder {
|
||||
public:
|
||||
void Word(std::uint32_t value) {
|
||||
for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast<std::byte>((value >> shift) & 0xffu));
|
||||
}
|
||||
void String(const std::string& value) {
|
||||
Word(static_cast<std::uint32_t>(value.size()));
|
||||
for (const auto character : value) bytes.push_back(static_cast<std::byte>(static_cast<unsigned char>(character)));
|
||||
}
|
||||
std::vector<std::byte> bytes;
|
||||
};
|
||||
|
||||
class Decoder {
|
||||
public:
|
||||
explicit Decoder(std::span<const std::byte> bytes) : bytes(bytes) {}
|
||||
std::uint32_t Word() {
|
||||
RequireCapture(bytes.size() - cursor >= 4, "truncated integer");
|
||||
std::uint32_t value = 0;
|
||||
for (unsigned shift = 0; shift < 32; shift += 8) value |= std::to_integer<std::uint32_t>(bytes[cursor++]) << shift;
|
||||
return value;
|
||||
}
|
||||
std::uint32_t Count(std::uint32_t maximum, std::size_t minimumBytes) {
|
||||
const auto count = Word();
|
||||
RequireCapture(count <= maximum && count <= (bytes.size() - cursor) / minimumBytes, "count exceeds record bounds");
|
||||
return count;
|
||||
}
|
||||
std::string String() {
|
||||
const auto count = Count(VK_MAX_PHYSICAL_DEVICE_NAME_SIZE - 1, 1);
|
||||
std::string result;
|
||||
for (std::uint32_t index = 0; index < count; ++index) result.push_back(static_cast<char>(std::to_integer<unsigned char>(bytes[cursor++])));
|
||||
return result;
|
||||
}
|
||||
bool Finished() const { return cursor == bytes.size(); }
|
||||
private:
|
||||
std::span<const std::byte> bytes;
|
||||
std::size_t cursor = 0;
|
||||
};
|
||||
|
||||
std::vector<std::byte> ReadBytes(const std::filesystem::path& path) {
|
||||
const auto size = std::filesystem::file_size(path);
|
||||
RequireCapture(size <= ComputePipelineCapture::MaxFileBytes, "file exceeds 20 MiB");
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
RequireCapture(input.good(), "cannot open capture");
|
||||
std::vector<std::byte> bytes(static_cast<std::size_t>(size));
|
||||
input.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
RequireCapture(input.good() && input.peek() == std::char_traits<char>::eof(), "capture changed while reading");
|
||||
return bytes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void ComputePipelineCapture::Validate() const {
|
||||
RequireCapture(spirv.size() >= 5 && spirv.size() <= 4u * 1024u * 1024u, "SPIR-V word count out of bounds");
|
||||
RequireCapture(spirv[0] == 0x07230203u && spirv[1] >= 0x00010000u && spirv[1] <= 0x00010600u && (spirv[1] & 0xffu) == 0 && spirv[3] != 0 && spirv[3] <= 0x003fffffu && spirv[4] == 0, "invalid SPIR-V header");
|
||||
bool mainEntry = false;
|
||||
for (std::size_t cursor = 5; cursor < spirv.size();) {
|
||||
const auto count = spirv[cursor] >> 16u;
|
||||
RequireCapture(count != 0 && count <= spirv.size() - cursor, "truncated SPIR-V instruction");
|
||||
if ((spirv[cursor] & 0xffffu) == 15u) {
|
||||
RequireCapture(count >= 5 && spirv[cursor + 1] == 5u && spirv[cursor + 2] != 0 && spirv[cursor + 2] < spirv[3] && spirv[cursor + 3] == 0x6e69616du && spirv[cursor + 4] == 0, "only a compute main entry point is supported");
|
||||
RequireCapture(!mainEntry, "multiple entry points are unsupported");
|
||||
mainEntry = true;
|
||||
}
|
||||
cursor += count;
|
||||
}
|
||||
RequireCapture(mainEntry, "compute main entry point missing");
|
||||
RequireCapture(deviceName.size() < VK_MAX_PHYSICAL_DEVICE_NAME_SIZE && deviceName.find('\0') == std::string::npos, "invalid device name");
|
||||
RequireCapture((flags & ~(VK_PIPELINE_CREATE_DISPATCH_BASE_BIT | VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR)) == 0, "unsupported pipeline flags");
|
||||
RequireCapture(stageFlags == 0, "unsupported shader stage flags");
|
||||
RequireCapture(requiredSubgroupSize == 0 || (requiredSubgroupSize <= 128 && (requiredSubgroupSize & (requiredSubgroupSize - 1u)) == 0), "invalid required subgroup size");
|
||||
RequireCapture(sets.size() <= 8, "too many descriptor sets");
|
||||
std::size_t totalBindings = 0;
|
||||
for (const auto& layout : sets) {
|
||||
RequireCapture((layout.flags & ~VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) == 0, "unsupported descriptor layout flags");
|
||||
RequireCapture(layout.bindings.size() <= 4096, "too many descriptor bindings");
|
||||
totalBindings += layout.bindings.size();
|
||||
std::set<std::uint32_t> used;
|
||||
for (const auto& binding : layout.bindings) {
|
||||
RequireCapture(used.insert(binding.binding).second, "duplicate descriptor binding");
|
||||
RequireCapture(binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_SAMPLER, "unsupported descriptor type");
|
||||
RequireCapture(binding.count != 0 && binding.count <= 1048576, "invalid descriptor count");
|
||||
RequireCapture(binding.stages == VK_SHADER_STAGE_COMPUTE_BIT, "unsupported descriptor stage flags");
|
||||
RequireCapture(binding.flags == 0 || (binding.flags == VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT && binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported descriptor binding flags");
|
||||
}
|
||||
}
|
||||
RequireCapture(totalBindings <= 4096, "too many total descriptor bindings");
|
||||
RequireCapture(pushConstants.size() <= 1, "too many compute push constant ranges");
|
||||
for (const auto& range : pushConstants) RequireCapture(range.stageFlags == VK_SHADER_STAGE_COMPUTE_BIT && range.size != 0 && range.size <= 4096 && range.offset <= 4096 - range.size && (range.offset % 4) == 0 && (range.size % 4) == 0, "invalid push constant range");
|
||||
RequireCapture(specialization.size() <= 4096, "too many specialization values");
|
||||
std::set<std::uint32_t> specializationIds;
|
||||
for (const auto& value : specialization) RequireCapture(specializationIds.insert(value.id).second, "duplicate specialization ID");
|
||||
}
|
||||
|
||||
std::vector<std::byte> ComputePipelineCapture::Encode() const {
|
||||
Validate();
|
||||
Encoder output;
|
||||
output.Word(0x35535041u);
|
||||
output.Word(0x504d4f43u);
|
||||
output.Word(Version);
|
||||
output.Word(flags);
|
||||
output.Word(stageFlags);
|
||||
output.Word(requiredSubgroupSize);
|
||||
output.Word(vendorId);
|
||||
output.Word(deviceId);
|
||||
output.Word(driverVersion);
|
||||
output.Word(apiVersion);
|
||||
for (const auto value : pipelineCacheUuid) output.Word(value);
|
||||
output.String(deviceName);
|
||||
output.Word(static_cast<std::uint32_t>(sets.size()));
|
||||
for (const auto& layout : sets) {
|
||||
output.Word(layout.flags);
|
||||
output.Word(static_cast<std::uint32_t>(layout.bindings.size()));
|
||||
for (const auto& binding : layout.bindings) {
|
||||
output.Word(binding.binding);
|
||||
output.Word(static_cast<std::uint32_t>(binding.type));
|
||||
output.Word(binding.count);
|
||||
output.Word(binding.stages);
|
||||
output.Word(binding.flags);
|
||||
}
|
||||
}
|
||||
output.Word(static_cast<std::uint32_t>(pushConstants.size()));
|
||||
for (const auto& range : pushConstants) {
|
||||
output.Word(range.stageFlags);
|
||||
output.Word(range.offset);
|
||||
output.Word(range.size);
|
||||
}
|
||||
output.Word(static_cast<std::uint32_t>(specialization.size()));
|
||||
for (const auto& value : specialization) {
|
||||
output.Word(value.id);
|
||||
output.Word(value.value);
|
||||
}
|
||||
output.Word(static_cast<std::uint32_t>(spirv.size()));
|
||||
for (const auto word : spirv) output.Word(word);
|
||||
RequireCapture(output.bytes.size() <= MaxFileBytes, "encoded capture exceeds size bound");
|
||||
return std::move(output.bytes);
|
||||
}
|
||||
|
||||
ComputePipelineCapture ComputePipelineCapture::Decode(std::span<const std::byte> bytes) {
|
||||
RequireCapture(bytes.size() <= MaxFileBytes, "record exceeds 20 MiB");
|
||||
Decoder input(bytes);
|
||||
RequireCapture(input.Word() == 0x35535041u && input.Word() == 0x504d4f43u, "invalid file signature");
|
||||
RequireCapture(input.Word() == Version, "unsupported file version");
|
||||
ComputePipelineCapture result;
|
||||
result.flags = input.Word();
|
||||
result.stageFlags = input.Word();
|
||||
result.requiredSubgroupSize = input.Word();
|
||||
result.vendorId = input.Word();
|
||||
result.deviceId = input.Word();
|
||||
result.driverVersion = input.Word();
|
||||
result.apiVersion = input.Word();
|
||||
for (auto& value : result.pipelineCacheUuid) {
|
||||
const auto word = input.Word();
|
||||
RequireCapture(word <= 255, "invalid cache UUID byte");
|
||||
value = static_cast<std::uint8_t>(word);
|
||||
}
|
||||
result.deviceName = input.String();
|
||||
result.sets.resize(input.Count(8, 8));
|
||||
std::size_t totalBindings = 0;
|
||||
for (auto& layout : result.sets) {
|
||||
layout.flags = input.Word();
|
||||
const auto count = input.Count(4096, 20);
|
||||
totalBindings += count;
|
||||
RequireCapture(totalBindings <= 4096, "too many total descriptor bindings");
|
||||
layout.bindings.resize(count);
|
||||
for (auto& binding : layout.bindings) {
|
||||
binding.binding = input.Word();
|
||||
binding.type = static_cast<VkDescriptorType>(input.Word());
|
||||
binding.count = input.Word();
|
||||
binding.stages = input.Word();
|
||||
binding.flags = input.Word();
|
||||
}
|
||||
}
|
||||
result.pushConstants.resize(input.Count(1, 12));
|
||||
for (auto& range : result.pushConstants) {
|
||||
range.stageFlags = input.Word();
|
||||
range.offset = input.Word();
|
||||
range.size = input.Word();
|
||||
}
|
||||
result.specialization.resize(input.Count(4096, 8));
|
||||
for (auto& value : result.specialization) {
|
||||
value.id = input.Word();
|
||||
value.value = input.Word();
|
||||
}
|
||||
result.spirv.resize(input.Count(4u * 1024u * 1024u, 4));
|
||||
for (auto& word : result.spirv) word = input.Word();
|
||||
RequireCapture(input.Finished(), "trailing bytes after capture");
|
||||
result.Validate();
|
||||
return result;
|
||||
}
|
||||
|
||||
ComputePipelineCapture ComputePipelineCapture::Read(const std::filesystem::path& path) {
|
||||
return Decode(ReadBytes(path));
|
||||
}
|
||||
|
||||
std::string ComputePipelineCapture::Identity() const {
|
||||
return Hash(Encode());
|
||||
}
|
||||
|
||||
std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path& directory) const {
|
||||
const auto bytes = Encode();
|
||||
const auto recordDirectory = directory / ("compute-" + Hash(bytes));
|
||||
const auto path = recordDirectory / "capture.aps5cp";
|
||||
static std::mutex mutex;
|
||||
std::lock_guard lock(mutex);
|
||||
std::filesystem::create_directories(directory);
|
||||
RequireCapture(!std::filesystem::is_symlink(std::filesystem::symlink_status(directory)), "capture directory is a symbolic link");
|
||||
if (std::filesystem::exists(recordDirectory)) {
|
||||
RequireCapture(std::filesystem::is_directory(recordDirectory) && !std::filesystem::is_symlink(std::filesystem::symlink_status(recordDirectory)) && !std::filesystem::is_symlink(std::filesystem::symlink_status(path)), "existing capture path is not a regular capture");
|
||||
RequireCapture(ReadBytes(path) == bytes, "existing capture differs or is incomplete");
|
||||
return path;
|
||||
}
|
||||
std::uintmax_t totalBytes = 0;
|
||||
std::size_t count = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
|
||||
RequireCapture(++count < 128, "capture directory reached 128 entries");
|
||||
RequireCapture(entry.is_directory() && !entry.is_symlink(), "capture directory contains an unexpected entry");
|
||||
const auto capture = entry.path() / "capture.aps5cp";
|
||||
RequireCapture(std::filesystem::is_regular_file(capture) && !std::filesystem::is_symlink(std::filesystem::symlink_status(capture)), "capture directory contains an incomplete record");
|
||||
const auto size = std::filesystem::file_size(capture);
|
||||
RequireCapture(size <= MaxFileBytes, "existing capture exceeds size bound");
|
||||
totalBytes += size;
|
||||
}
|
||||
RequireCapture(totalBytes + bytes.size() <= 256u * 1024u * 1024u, "capture directory exceeds 256 MiB");
|
||||
RequireCapture(std::filesystem::create_directory(recordDirectory), "capture directory was concurrently created");
|
||||
std::ofstream output(path, std::ios::binary);
|
||||
RequireCapture(output.good(), "cannot create capture");
|
||||
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
output.close();
|
||||
RequireCapture(output.good(), "cannot finish capture");
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
|
||||
#include "BdaAbi.hpp"
|
||||
#if ANYPS5_ENABLE_SPIRV_TOOLS
|
||||
#include "SpirvBackend/SpirvOptimizer.hpp"
|
||||
#endif
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
|
|
@ -70,6 +73,32 @@ void require(bool condition, const char* reason) {
|
|||
if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason);
|
||||
}
|
||||
|
||||
void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) {
|
||||
VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
|
||||
info.flags = flags;
|
||||
info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
info.stage.flags = stageFlags;
|
||||
info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
info.stage.module = module;
|
||||
info.stage.pName = "main";
|
||||
info.stage.pSpecializationInfo = specialization;
|
||||
VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT};
|
||||
subgroup.requiredSubgroupSize = subgroupSize;
|
||||
if (subgroupSize != 0) info.stage.pNext = &subgroup;
|
||||
info.layout = layout;
|
||||
check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines");
|
||||
}
|
||||
|
||||
bool CaptureComputePipeline(std::size_t words) {
|
||||
const char* minimum = std::getenv("APS5_DUMP_COMPUTE_MIN_WORDS");
|
||||
if (minimum == nullptr) return true;
|
||||
std::uint32_t threshold = 0;
|
||||
const auto end = minimum + std::strlen(minimum);
|
||||
const auto parsed = std::from_chars(minimum, end, threshold);
|
||||
require(parsed.ec == std::errc{} && parsed.ptr == end && threshold <= 4u * 1024u * 1024u, "APS5_DUMP_COMPUTE_MIN_WORDS must be an integer from 0 through 4194304");
|
||||
return words >= threshold;
|
||||
}
|
||||
|
||||
// The ShaderResources content cache dispatches share with recorded draws: Graphics::ResourceCache,
|
||||
// one process-wide instance (see SharedResourceCache) that the State references so this file keeps
|
||||
// its Find/Insert/Remove/Clear calls; the device clears it at teardown before its descriptor caches
|
||||
|
|
@ -178,6 +207,7 @@ struct WorkgroupMemory {
|
|||
};
|
||||
|
||||
struct VulkanDevice::State {
|
||||
bool compileOnly = false;
|
||||
void* library = nullptr;
|
||||
PFN_vkGetInstanceProcAddr instanceProc = nullptr;
|
||||
PFN_vkGetDeviceProcAddr deviceProc = nullptr;
|
||||
|
|
@ -567,6 +597,10 @@ struct VulkanDevice::State {
|
|||
|
||||
~State() {
|
||||
contextReady = false;
|
||||
if (compileOnly && device != VK_NULL_HANDLE) {
|
||||
reinterpret_cast<PFN_vkDestroyDevice>(deviceProc(device, "vkDestroyDevice"))(device, nullptr);
|
||||
device = VK_NULL_HANDLE;
|
||||
}
|
||||
if (device != VK_NULL_HANDLE) {
|
||||
const auto idle = reinterpret_cast<PFN_vkDeviceWaitIdle>(deviceProc(device, "vkDeviceWaitIdle"))(device);
|
||||
if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate();
|
||||
|
|
@ -656,7 +690,159 @@ struct VulkanDevice::State {
|
|||
}
|
||||
};
|
||||
|
||||
VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_unique<State>()), serial([] { static std::atomic<std::uint64_t> serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) {
|
||||
ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePipelineCapture& capture, std::span<const std::byte> initialCache) {
|
||||
require(state->compileOnly, "pipeline replay requires a compile-only device");
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
const auto elapsed = [](auto since) { return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - since).count(); };
|
||||
capture.Validate();
|
||||
require(initialCache.size() <= 64u * 1024u * 1024u, "replay pipeline cache exceeds 64 MiB");
|
||||
if (!initialCache.empty()) {
|
||||
require(initialCache.size() >= 32, "replay pipeline cache header is truncated");
|
||||
const auto word = [&](std::size_t offset) {
|
||||
std::uint32_t result = 0;
|
||||
for (unsigned shift = 0; shift < 32; shift += 8) result |= std::to_integer<std::uint32_t>(initialCache[offset++]) << shift;
|
||||
return result;
|
||||
};
|
||||
require(word(0) >= 32 && word(0) <= initialCache.size() && word(4) == VK_PIPELINE_CACHE_HEADER_VERSION_ONE, "unsupported replay pipeline cache header");
|
||||
require(word(8) == state->properties.vendorID && word(12) == state->properties.deviceID, "replay pipeline cache belongs to another device");
|
||||
for (std::size_t index = 0; index < VK_UUID_SIZE; ++index) require(std::to_integer<std::uint8_t>(initialCache[16 + index]) == state->properties.pipelineCacheUUID[index], "replay pipeline cache UUID differs from the driver");
|
||||
}
|
||||
require((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) == 0 || state->pipelineExecutableInfo, "captured pipeline statistics feature is not enabled");
|
||||
if (capture.requiredSubgroupSize != 0) {
|
||||
VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT};
|
||||
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(state->physical, &properties);
|
||||
require(state->computeWave32 && capture.requiredSubgroupSize >= subgroup.minSubgroupSize && capture.requiredSubgroupSize <= subgroup.maxSubgroupSize && (subgroup.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0, "captured required subgroup size is unsupported");
|
||||
}
|
||||
const auto target = Target();
|
||||
require(capture.spirv[1] <= target.spirvVersion, "captured SPIR-V version exceeds enabled device support");
|
||||
for (std::size_t cursor = 5; cursor < capture.spirv.size(); cursor += capture.spirv[cursor] >> 16u) {
|
||||
const auto count = capture.spirv[cursor] >> 16u;
|
||||
if ((capture.spirv[cursor] & 0xffffu) == spv::OpCapability) {
|
||||
require(count == 2 && std::ranges::find(target.supportedCapabilities, capture.spirv[cursor + 1]) != target.supportedCapabilities.end(), "captured SPIR-V capability is not enabled");
|
||||
}
|
||||
}
|
||||
#if ANYPS5_ENABLE_SPIRV_TOOLS
|
||||
static_cast<void>(ShaderRecompiler::ValidateAndOptimizeSpirv(capture.spirv, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false));
|
||||
#else
|
||||
throw std::runtime_error("compute pipeline replay requires ANYPS5_ENABLE_SPIRV_TOOLS=ON");
|
||||
#endif
|
||||
ShaderRecompiler::RecompileResult shader;
|
||||
shader.spirv = capture.spirv;
|
||||
for (const auto& value : capture.specialization) shader.specialization.push_back({value.id, value.value});
|
||||
const Graphics::PipelineSpecialization specialization(shader);
|
||||
const auto& limits = state->properties.limits;
|
||||
require(capture.sets.size() <= limits.maxBoundDescriptorSets, "captured descriptor sets exceed device limits");
|
||||
for (const auto& range : capture.pushConstants) require(range.offset + range.size <= limits.maxPushConstantsSize, "captured push constants exceed device limits");
|
||||
struct ReplayObjects {
|
||||
VkDevice device;
|
||||
PFN_vkDestroyDescriptorSetLayout destroySetLayout;
|
||||
PFN_vkDestroyPipelineCache destroyCache;
|
||||
std::vector<VkDescriptorSetLayout> sets;
|
||||
VkPipelineCache cache = VK_NULL_HANDLE;
|
||||
~ReplayObjects() {
|
||||
for (auto layout : sets) destroySetLayout(device, layout, nullptr);
|
||||
if (cache != VK_NULL_HANDLE) destroyCache(device, cache, nullptr);
|
||||
}
|
||||
} replay{state->device, state->DeviceFunction<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout"), state->DeviceFunction<PFN_vkDestroyPipelineCache>("vkDestroyPipelineCache"), {}};
|
||||
ComputePipelineObjects objects;
|
||||
objects.device = state->device;
|
||||
objects.destroyModule = state->DeviceFunction<PFN_vkDestroyShaderModule>("vkDestroyShaderModule");
|
||||
objects.destroyLayout = state->DeviceFunction<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout");
|
||||
objects.destroyPipeline = state->DeviceFunction<PFN_vkDestroyPipeline>("vkDestroyPipeline");
|
||||
ComputePipelineReplayResult result;
|
||||
result.deviceName = state->properties.deviceName;
|
||||
result.vendorId = state->properties.vendorID;
|
||||
result.deviceId = state->properties.deviceID;
|
||||
result.driverVersion = state->properties.driverVersion;
|
||||
result.apiVersion = state->properties.apiVersion;
|
||||
std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, result.pipelineCacheUuid.begin());
|
||||
result.initialCacheBytes = initialCache.size();
|
||||
auto phase = std::chrono::steady_clock::now();
|
||||
VkPipelineCacheCreateInfo cacheInfo{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO};
|
||||
cacheInfo.initialDataSize = initialCache.size();
|
||||
cacheInfo.pInitialData = initialCache.data();
|
||||
check(state->DeviceFunction<PFN_vkCreatePipelineCache>("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay");
|
||||
result.cacheMilliseconds = elapsed(phase);
|
||||
phase = std::chrono::steady_clock::now();
|
||||
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
||||
moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t);
|
||||
moduleInfo.pCode = capture.spirv.data();
|
||||
check(state->DeviceFunction<PFN_vkCreateShaderModule>("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay");
|
||||
result.moduleMilliseconds = elapsed(phase);
|
||||
phase = std::chrono::steady_clock::now();
|
||||
std::array<std::array<std::uint64_t, 4>, 2> descriptorCounts{};
|
||||
bool hasUpdateAfterBindLayout = false;
|
||||
for (const auto& layout : capture.sets) {
|
||||
const bool updateAfterBind = (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0;
|
||||
hasUpdateAfterBindLayout |= updateAfterBind;
|
||||
require(!updateAfterBind || state->storageBufferUpdateAfterBind, "captured update-after-bind descriptors are unsupported");
|
||||
std::vector<VkDescriptorSetLayoutBinding> bindings;
|
||||
std::vector<VkDescriptorBindingFlags> flags;
|
||||
for (const auto& binding : layout.bindings) {
|
||||
bindings.push_back({binding.binding, binding.type, binding.count, binding.stages, nullptr});
|
||||
flags.push_back(binding.flags);
|
||||
const std::size_t kind = binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? 0 : binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE ? 1 : binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? 2 : 3;
|
||||
descriptorCounts[updateAfterBind ? 1 : 0][kind] += binding.count;
|
||||
}
|
||||
VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO};
|
||||
bindingFlags.bindingCount = static_cast<std::uint32_t>(flags.size());
|
||||
bindingFlags.pBindingFlags = flags.data();
|
||||
VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
||||
info.pNext = updateAfterBind ? &bindingFlags : nullptr;
|
||||
info.flags = layout.flags;
|
||||
info.bindingCount = static_cast<std::uint32_t>(bindings.size());
|
||||
info.pBindings = bindings.data();
|
||||
VkDescriptorSetLayoutSupport support{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_SUPPORT};
|
||||
state->DeviceFunction<PFN_vkGetDescriptorSetLayoutSupport>("vkGetDescriptorSetLayoutSupport")(state->device, &info, &support);
|
||||
require(support.supported == VK_TRUE, "captured descriptor set layout is unsupported");
|
||||
VkDescriptorSetLayout handle = VK_NULL_HANDLE;
|
||||
check(state->DeviceFunction<PFN_vkCreateDescriptorSetLayout>("vkCreateDescriptorSetLayout")(state->device, &info, nullptr, &handle), "vkCreateDescriptorSetLayout replay");
|
||||
replay.sets.push_back(handle);
|
||||
}
|
||||
const std::array<std::uint32_t, 4> regularLimits{std::min(limits.maxDescriptorSetStorageBuffers, limits.maxPerStageDescriptorStorageBuffers), std::min(limits.maxDescriptorSetSampledImages, limits.maxPerStageDescriptorSampledImages), std::min(limits.maxDescriptorSetStorageImages, limits.maxPerStageDescriptorStorageImages), std::min(limits.maxDescriptorSetSamplers, limits.maxPerStageDescriptorSamplers)};
|
||||
const auto& indexed = state->descriptorIndexingProperties;
|
||||
const std::array<std::uint32_t, 4> indexedLimits{std::min(indexed.maxDescriptorSetUpdateAfterBindStorageBuffers, indexed.maxPerStageDescriptorUpdateAfterBindStorageBuffers), std::min(indexed.maxDescriptorSetUpdateAfterBindSampledImages, indexed.maxPerStageDescriptorUpdateAfterBindSampledImages), std::min(indexed.maxDescriptorSetUpdateAfterBindStorageImages, indexed.maxPerStageDescriptorUpdateAfterBindStorageImages), std::min(indexed.maxDescriptorSetUpdateAfterBindSamplers, indexed.maxPerStageDescriptorUpdateAfterBindSamplers)};
|
||||
std::uint64_t regularResources = 0;
|
||||
std::uint64_t indexedResources = 0;
|
||||
for (std::size_t kind = 0; kind < regularLimits.size(); ++kind) {
|
||||
require(descriptorCounts[0][kind] <= regularLimits[kind], "captured pipeline descriptors exceed device limits");
|
||||
require(!hasUpdateAfterBindLayout || descriptorCounts[0][kind] + descriptorCounts[1][kind] <= indexedLimits[kind], "captured update-after-bind pipeline descriptors exceed device limits");
|
||||
if (kind != 3) {
|
||||
regularResources += descriptorCounts[0][kind];
|
||||
indexedResources += descriptorCounts[0][kind] + descriptorCounts[1][kind];
|
||||
}
|
||||
}
|
||||
require(regularResources <= limits.maxPerStageResources && (!hasUpdateAfterBindLayout || indexedResources <= indexed.maxPerStageUpdateAfterBindResources), "captured pipeline resources exceed device limits");
|
||||
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
||||
layoutInfo.setLayoutCount = static_cast<std::uint32_t>(replay.sets.size());
|
||||
layoutInfo.pSetLayouts = replay.sets.data();
|
||||
layoutInfo.pushConstantRangeCount = static_cast<std::uint32_t>(capture.pushConstants.size());
|
||||
layoutInfo.pPushConstantRanges = capture.pushConstants.data();
|
||||
check(state->DeviceFunction<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects.layout), "vkCreatePipelineLayout replay");
|
||||
result.layoutMilliseconds = elapsed(phase);
|
||||
phase = std::chrono::steady_clock::now();
|
||||
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), replay.cache, objects.module, objects.layout, capture.flags, capture.stageFlags, specialization.Info(), capture.requiredSubgroupSize, &objects.pipeline);
|
||||
result.pipelineMilliseconds = elapsed(phase);
|
||||
phase = std::chrono::steady_clock::now();
|
||||
const auto getCache = state->DeviceFunction<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData");
|
||||
std::size_t cacheBytes = 0;
|
||||
check(getCache(state->device, replay.cache, &cacheBytes, nullptr), "vkGetPipelineCacheData size replay");
|
||||
require(cacheBytes <= 64u * 1024u * 1024u, "exported replay pipeline cache exceeds 64 MiB");
|
||||
result.cache.resize(cacheBytes);
|
||||
check(getCache(state->device, replay.cache, &cacheBytes, result.cache.data()), "vkGetPipelineCacheData replay");
|
||||
result.cache.resize(cacheBytes);
|
||||
result.cacheMilliseconds += elapsed(phase);
|
||||
result.totalMilliseconds = elapsed(start);
|
||||
return result;
|
||||
}
|
||||
|
||||
VulkanDevice::VulkanDevice(const PresentationWindow* window) : VulkanDevice(window, false) {}
|
||||
|
||||
VulkanDevice::VulkanDevice(CompileOnly) : VulkanDevice(nullptr, true) {}
|
||||
|
||||
VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state(std::make_unique<State>()), serial([] { static std::atomic<std::uint64_t> serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) {
|
||||
state->compileOnly = compileOnly;
|
||||
APS5_LOG_OUT("VulkanDevice constructor window=%p", static_cast<const void*>(window));
|
||||
#ifdef _WIN32
|
||||
state->library = SDL_LoadObject("vulkan-1.dll");
|
||||
|
|
@ -966,10 +1152,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
|
||||
state->hostImportAlignment = hostProperties.minImportedHostPointerAlignment;
|
||||
Graphics::SetHostImportWindow(8ull << 20u);
|
||||
if (!compileOnly) Graphics::SetHostImportWindow(8ull << 20u);
|
||||
#ifdef _WIN32
|
||||
// Imported pages are locked; locking is bounded by the process working-set minimum.
|
||||
if (const char* value = std::getenv("APS5_WORKING_SET_MIB")) {
|
||||
if (const char* value = std::getenv("APS5_WORKING_SET_MIB"); value != nullptr && !compileOnly) {
|
||||
const SIZE_T bytes = static_cast<SIZE_T>(std::strtoull(value, nullptr, 10)) << 20u;
|
||||
if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), bytes, bytes * 2, QUOTA_LIMITS_HARDWS_MIN_DISABLE | QUOTA_LIMITS_HARDWS_MAX_DISABLE)) std::fprintf(stderr, "[gpu] SetProcessWorkingSetSizeEx failed: %lu\n", GetLastError());
|
||||
}
|
||||
|
|
@ -1310,6 +1496,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
auto shaderProfile = std::make_unique<const ShaderDeviceProfile>(buildTarget(), deviceInfo, state->properties.limits);
|
||||
check(state->InstanceFunction<PFN_vkCreateDevice>("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice");
|
||||
state->shaderProfile = std::move(shaderProfile);
|
||||
if (compileOnly) return;
|
||||
state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, family, 0, &state->queue);
|
||||
APS5_LOG_OUT("Vulkan device ready device=%p queue=%p family=%u", reinterpret_cast<void*>(state->device), reinterpret_cast<void*>(state->queue), family);
|
||||
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
|
|
@ -3833,20 +4020,36 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
|||
layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0;
|
||||
layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr;
|
||||
check(state->DeviceFunction<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout");
|
||||
VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
|
||||
pipelineInfo.flags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT;
|
||||
pipelineInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
||||
pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
|
||||
pipelineInfo.stage.module = objects->module;
|
||||
pipelineInfo.stage.pName = "main";
|
||||
const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT;
|
||||
const Graphics::PipelineSpecialization specialization(shader);
|
||||
pipelineInfo.stage.pSpecializationInfo = specialization.Info();
|
||||
VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT requiredSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT};
|
||||
requiredSubgroup.requiredSubgroupSize = std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize);
|
||||
if (state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize)) pipelineInfo.stage.pNext = &requiredSubgroup;
|
||||
pipelineInfo.layout = objects->layout;
|
||||
const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u;
|
||||
if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) {
|
||||
ComputePipelineCapture capture;
|
||||
capture.spirv = shader.spirv;
|
||||
ComputeDescriptorSetLayout resourcesLayout;
|
||||
resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0;
|
||||
for (const auto& binding : resources->LayoutBindings()) {
|
||||
require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers");
|
||||
const VkDescriptorBindingFlags flags = resources->LayoutUpdateAfterBind() && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0;
|
||||
resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, flags});
|
||||
}
|
||||
capture.sets.push_back(std::move(resourcesLayout));
|
||||
if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}});
|
||||
if (pushStages != 0) capture.pushConstants.push_back(push);
|
||||
for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value});
|
||||
capture.requiredSubgroupSize = subgroupSize;
|
||||
capture.flags = pipelineFlags;
|
||||
capture.deviceName = state->properties.deviceName;
|
||||
capture.vendorId = state->properties.vendorID;
|
||||
capture.deviceId = state->properties.deviceID;
|
||||
capture.driverVersion = state->properties.driverVersion;
|
||||
capture.apiVersion = state->properties.apiVersion;
|
||||
std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin());
|
||||
const auto path = capture.Write(directory);
|
||||
std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str());
|
||||
}
|
||||
if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast<unsigned long long>(programAddress));
|
||||
check(state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(state->device, context.pipelineCache, 1, &pipelineInfo, nullptr, &objects->pipeline), "vkCreateComputePipelines");
|
||||
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline);
|
||||
objects->destroyModule(objects->device, objects->module, nullptr);
|
||||
objects->module = VK_NULL_HANDLE;
|
||||
Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline);
|
||||
|
|
|
|||
|
|
@ -114,6 +114,8 @@ public:
|
|||
ShaderResources(const ShaderResources&) = delete;
|
||||
ShaderResources& operator=(const ShaderResources&) = delete;
|
||||
VkDescriptorSetLayout Layout() const;
|
||||
std::vector<VkDescriptorSetLayoutBinding> LayoutBindings() const;
|
||||
bool LayoutUpdateAfterBind() const;
|
||||
void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const;
|
||||
struct DrawBindings {
|
||||
DrawBindings() = default;
|
||||
|
|
|
|||
|
|
@ -3322,6 +3322,16 @@ VkDescriptorSetLayout ShaderResources::Layout() const {
|
|||
return _layout;
|
||||
}
|
||||
|
||||
std::vector<VkDescriptorSetLayoutBinding> ShaderResources::LayoutBindings() const {
|
||||
std::vector<VkDescriptorSetLayoutBinding> result;
|
||||
for (const auto& binding : bindings) result.push_back(binding.layout);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ShaderResources::LayoutUpdateAfterBind() const {
|
||||
return updateAfterBind;
|
||||
}
|
||||
|
||||
ShaderResources::DrawBindings::~DrawBindings() {
|
||||
if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
#include "../tools/ComputePipelineFixture.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
void Reject(TAction action, const char* message) {
|
||||
bool rejected = false;
|
||||
try {
|
||||
action();
|
||||
} catch (const std::exception&) {
|
||||
rejected = true;
|
||||
}
|
||||
Require(rejected, message);
|
||||
}
|
||||
|
||||
class Directory {
|
||||
public:
|
||||
Directory() {
|
||||
path = std::filesystem::temp_directory_path() / ("anyps5-compute-capture-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
Require(std::filesystem::create_directory(path), "could not create test directory");
|
||||
}
|
||||
~Directory() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(path, error);
|
||||
}
|
||||
std::filesystem::path path;
|
||||
};
|
||||
|
||||
void CheckRoundTrip() {
|
||||
auto capture = AgcDriver::ComputePipelineFixture();
|
||||
capture.deviceName = "test GPU \"name\"";
|
||||
capture.vendorId = 0x10de;
|
||||
capture.deviceId = 0x2b85;
|
||||
capture.driverVersion = 0x12345678;
|
||||
capture.apiVersion = VK_API_VERSION_1_3;
|
||||
capture.pipelineCacheUuid[0] = 0x12;
|
||||
capture.pipelineCacheUuid[15] = 0xab;
|
||||
capture.requiredSubgroupSize = 32;
|
||||
capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT;
|
||||
capture.sets = {{VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT,
|
||||
{{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 5, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT},
|
||||
{7, VK_DESCRIPTOR_TYPE_SAMPLER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}};
|
||||
capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 32}};
|
||||
const auto encoded = capture.Encode();
|
||||
const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded);
|
||||
Require(decoded.Encode() == encoded, "capture round trip changed pipeline inputs");
|
||||
Require(decoded.Identity() == capture.Identity(), "capture identity changed after decoding");
|
||||
auto changed = decoded;
|
||||
++changed.specialization[0].value;
|
||||
Require(changed.Identity() != capture.Identity(), "specialization did not affect capture identity");
|
||||
changed = decoded;
|
||||
++changed.sets[0].bindings[0].count;
|
||||
Require(changed.Identity() != capture.Identity(), "descriptor layout did not affect capture identity");
|
||||
changed = decoded;
|
||||
changed.requiredSubgroupSize = 64;
|
||||
Require(changed.Identity() != capture.Identity(), "subgroup size did not affect capture identity");
|
||||
Directory directory;
|
||||
const auto file = capture.Write(directory.path);
|
||||
Require(AgcDriver::ComputePipelineCapture::Read(file).Encode() == encoded, "capture file changed pipeline inputs");
|
||||
Require(capture.Write(directory.path) == file, "identical capture was not deduplicated");
|
||||
std::ofstream(file, std::ios::binary | std::ios::trunc).put('x');
|
||||
Reject([&] { capture.Write(directory.path); }, "existing damaged capture was overwritten");
|
||||
Require(std::filesystem::file_size(file) == 1, "damaged capture was changed");
|
||||
}
|
||||
|
||||
void CheckMalformedRecords() {
|
||||
const auto capture = AgcDriver::ComputePipelineFixture();
|
||||
const auto encoded = capture.Encode();
|
||||
for (std::size_t length = 0; length < encoded.size(); ++length) {
|
||||
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(std::span(encoded).first(length)); }, "truncated capture accepted");
|
||||
}
|
||||
auto bytes = encoded;
|
||||
bytes.push_back(std::byte{0});
|
||||
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "trailing capture bytes accepted");
|
||||
bytes = encoded;
|
||||
bytes[0] ^= std::byte{1};
|
||||
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "invalid signature accepted");
|
||||
bytes = encoded;
|
||||
bytes[8] = std::byte{0xff};
|
||||
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unknown capture version accepted");
|
||||
bytes = encoded;
|
||||
for (std::size_t index = 104; index < 108; ++index) bytes[index] = std::byte{0xff};
|
||||
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unbounded device name accepted");
|
||||
const auto invalid = [&](auto change) {
|
||||
auto copy = capture;
|
||||
change(copy);
|
||||
Reject([&] { copy.Encode(); }, "unsupported pipeline input accepted");
|
||||
};
|
||||
invalid([](auto& copy) { copy.spirv[0] = 0; });
|
||||
invalid([](auto& copy) { copy.spirv[5] = 0; });
|
||||
invalid([](auto& copy) { copy.spirv[11] = 4; });
|
||||
invalid([](auto& copy) { copy.spirv[13] = 0; });
|
||||
invalid([](auto& copy) { copy.flags = VK_PIPELINE_CREATE_DERIVATIVE_BIT; });
|
||||
invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; });
|
||||
invalid([](auto& copy) { copy.requiredSubgroupSize = 3; });
|
||||
invalid([](auto& copy) { copy.specialization.push_back(copy.specialization[0]); });
|
||||
invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_VERTEX_BIT, 0, 4}}; });
|
||||
invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 1, 4}}; });
|
||||
invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; });
|
||||
invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, 0}}}}; });
|
||||
invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT}}}}; });
|
||||
invalid([](auto& copy) { copy.sets = {{0, {{}, {}}}}; });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
CheckRoundTrip();
|
||||
CheckMalformedRecords();
|
||||
std::puts("compute pipeline capture: round trip, immutable writes, truncated records and unsupported inputs passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "compute pipeline capture test: %s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
foreach(required IN ITEMS FIXTURE REPLAY OUTPUT_ROOT)
|
||||
if(NOT DEFINED ${required})
|
||||
message(FATAL_ERROR "Missing ${required}")
|
||||
endif()
|
||||
endforeach()
|
||||
string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef runId)
|
||||
set(runDirectory "${OUTPUT_ROOT}/compute-capture-${runId}")
|
||||
set(captureDirectory "${runDirectory}/captures")
|
||||
file(MAKE_DIRECTORY "${runDirectory}")
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}"
|
||||
WORKING_DIRECTORY "${runDirectory}"
|
||||
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20
|
||||
)
|
||||
file(WRITE "${runDirectory}/normal.log" "${output}\n${error}")
|
||||
if(result STREQUAL "77" AND NOT DEFINED ENV{ANYPS5_REQUIRE_VULKAN})
|
||||
message(STATUS "compute pipeline capture run: skipped, no usable Vulkan device")
|
||||
file(REMOVE_RECURSE "${runDirectory}")
|
||||
return()
|
||||
endif()
|
||||
if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed")
|
||||
message(FATAL_ERROR "Normal compute fixture failed: ${result}\n${output}\n${error}")
|
||||
endif()
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_REQUIRE_VULKAN=1" "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=${captureDirectory}" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}"
|
||||
WORKING_DIRECTORY "${runDirectory}"
|
||||
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20
|
||||
)
|
||||
file(WRITE "${runDirectory}/capture.log" "${output}\n${error}")
|
||||
if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed")
|
||||
message(FATAL_ERROR "Captured compute fixture failed: ${result}\n${output}\n${error}")
|
||||
endif()
|
||||
file(GLOB captures "${captureDirectory}/compute-*/capture.aps5cp")
|
||||
list(LENGTH captures count)
|
||||
if(count LESS 1 OR count GREATER 12)
|
||||
message(FATAL_ERROR "Expected 1 to 12 compute captures, found ${count}")
|
||||
endif()
|
||||
foreach(capture IN LISTS captures)
|
||||
get_filename_component(recordDirectory "${capture}" DIRECTORY)
|
||||
get_filename_component(recordName "${recordDirectory}" NAME)
|
||||
string(REPLACE "compute-" "" identity "${recordName}")
|
||||
set(report "${runDirectory}/${recordName}.json")
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "${REPLAY}" "${capture}" --output "${report}"
|
||||
WORKING_DIRECTORY "${runDirectory}"
|
||||
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20
|
||||
)
|
||||
file(WRITE "${runDirectory}/${recordName}.log" "${output}\n${error}")
|
||||
if(NOT result STREQUAL "0")
|
||||
message(FATAL_ERROR "Captured pipeline replay failed: ${result}\n${output}\n${error}")
|
||||
endif()
|
||||
file(READ "${report}" reportJson)
|
||||
string(JSON reportIdentity GET "${reportJson}" capture_identity)
|
||||
string(JSON words GET "${reportJson}" spirv_words)
|
||||
string(JSON sets GET "${reportJson}" descriptor_sets)
|
||||
string(JSON ranges GET "${reportJson}" push_constant_ranges)
|
||||
string(JSON dispatches GET "${reportJson}" dispatch_count)
|
||||
string(JSON policy GET "${reportJson}" application_cache_policy)
|
||||
string(JSON capturedDevice GET "${reportJson}" captured_device device_id)
|
||||
string(JSON replayDevice GET "${reportJson}" replay_device device_id)
|
||||
string(JSON capturedDriver GET "${reportJson}" captured_device driver_version_raw)
|
||||
string(JSON replayDriver GET "${reportJson}" replay_device driver_version_raw)
|
||||
if(NOT reportIdentity STREQUAL identity OR words LESS 54 OR sets LESS 1 OR ranges LESS 1 OR NOT dispatches EQUAL 0 OR NOT policy STREQUAL "fresh" OR NOT capturedDevice STREQUAL replayDevice OR NOT capturedDriver STREQUAL replayDriver)
|
||||
message(FATAL_ERROR "Captured pipeline metadata mismatch: ${reportJson}")
|
||||
endif()
|
||||
endforeach()
|
||||
message(STATUS "Normal and captured compute output verified; ${count} captured pipelines replayed without dispatch")
|
||||
file(REMOVE_RECURSE "${runDirectory}")
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
#include "../tools/ComputePipelineFixture.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void CheckResult(const AgcDriver::ComputePipelineReplayResult& result) {
|
||||
Require(!result.deviceName.empty() && result.vendorId != 0 && result.apiVersion != 0, "replay did not report its device");
|
||||
Require(result.cache.size() >= 32, "replay did not export a pipeline cache header");
|
||||
for (const auto duration : {result.moduleMilliseconds, result.layoutMilliseconds, result.pipelineMilliseconds, result.cacheMilliseconds, result.totalMilliseconds}) {
|
||||
Require(std::isfinite(duration) && duration >= 0, "replay reported an invalid duration");
|
||||
}
|
||||
Require(result.totalMilliseconds >= result.pipelineMilliseconds, "pipeline duration exceeds replay duration");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
auto capture = AgcDriver::ComputePipelineFixture();
|
||||
const auto encoded = capture.Encode();
|
||||
const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded);
|
||||
std::unique_ptr<AgcDriver::VulkanDevice> device;
|
||||
try {
|
||||
device = std::make_unique<AgcDriver::VulkanDevice>(AgcDriver::VulkanDevice::CompileOnly{});
|
||||
} catch (const std::exception& error) {
|
||||
const std::string_view message(error.what());
|
||||
const bool absent = (message.starts_with("Vulkan loader: ") && message != "Vulkan loader: vkGetInstanceProcAddr missing") || message == "Vulkan: no Vulkan 1.1 graphics and compute queue" || message == "vkCreateInstance: Vulkan result -9";
|
||||
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || !absent) throw;
|
||||
std::printf("skipped, no usable Vulkan device: %s\n", error.what());
|
||||
return 77;
|
||||
}
|
||||
const auto fresh = device->ReplayComputePipeline(decoded);
|
||||
CheckResult(fresh);
|
||||
Require(fresh.initialCacheBytes == 0, "fresh replay imported cache bytes");
|
||||
const auto imported = device->ReplayComputePipeline(decoded, fresh.cache);
|
||||
CheckResult(imported);
|
||||
Require(imported.initialCacheBytes == fresh.cache.size(), "replay did not import the supplied cache");
|
||||
Require(fresh.deviceId == imported.deviceId && fresh.driverVersion == imported.driverVersion && fresh.pipelineCacheUuid == imported.pipelineCacheUuid, "replay device identity changed");
|
||||
Require(capture.Encode() == encoded, "replay modified the captured pipeline");
|
||||
auto corrupt = fresh.cache;
|
||||
corrupt[8] ^= std::byte{0xff};
|
||||
bool rejected = false;
|
||||
try {
|
||||
device->ReplayComputePipeline(decoded, corrupt);
|
||||
} catch (const std::exception&) {
|
||||
rejected = true;
|
||||
}
|
||||
Require(rejected, "cache from another vendor was accepted");
|
||||
corrupt = fresh.cache;
|
||||
corrupt[16] ^= std::byte{0xff};
|
||||
rejected = false;
|
||||
try {
|
||||
device->ReplayComputePipeline(decoded, corrupt);
|
||||
} catch (const std::exception&) {
|
||||
rejected = true;
|
||||
}
|
||||
Require(rejected, "cache with another pipeline UUID was accepted");
|
||||
capture.requiredSubgroupSize = 32;
|
||||
try {
|
||||
const auto subgroup = device->ReplayComputePipeline(capture);
|
||||
CheckResult(subgroup);
|
||||
std::puts("compute pipeline replay: required subgroup size 32 compiled");
|
||||
} catch (const std::exception& error) {
|
||||
Require(std::string_view(error.what()).find("captured required subgroup size is unsupported") != std::string_view::npos, "required subgroup replay failed without an explicit support refusal");
|
||||
std::puts("compute pipeline replay: device explicitly refused required subgroup size 32");
|
||||
}
|
||||
std::printf("compute pipeline replay: fresh %.6f ms, imported %.6f ms, GPU %s; no dispatch\n", fresh.pipelineMilliseconds, imported.pipelineMilliseconds, fresh.deviceName.c_str());
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "compute pipeline replay test: %s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP
|
||||
|
||||
#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp"
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
inline ComputePipelineCapture ComputePipelineFixture() {
|
||||
ComputePipelineCapture capture;
|
||||
capture.spirv = {
|
||||
0x07230203u, 0x00010000u, 0u, 7u, 0u,
|
||||
0x00020011u, 1u,
|
||||
0x0003000eu, 0u, 1u,
|
||||
0x0005000fu, 5u, 3u, 0x6e69616du, 0u,
|
||||
0x00060010u, 3u, 17u, 1u, 1u, 1u,
|
||||
0x00040047u, 6u, 1u, 7u,
|
||||
0x00020013u, 1u,
|
||||
0x00030021u, 2u, 1u,
|
||||
0x00040015u, 5u, 32u, 0u,
|
||||
0x00040032u, 5u, 6u, 1u,
|
||||
0x00050036u, 1u, 3u, 0u, 2u,
|
||||
0x000200f8u, 4u,
|
||||
0x000100fdu,
|
||||
0x00010038u
|
||||
};
|
||||
capture.specialization.push_back({7u, 3u});
|
||||
capture.sets = {{0, {{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}};
|
||||
capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 16}};
|
||||
capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT;
|
||||
return capture;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
#include "ComputePipelineFixture.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Options {
|
||||
std::filesystem::path source;
|
||||
std::filesystem::path cacheIn;
|
||||
std::filesystem::path cacheOut;
|
||||
std::filesystem::path output;
|
||||
std::filesystem::path fixtureDirectory;
|
||||
bool fixture = false;
|
||||
};
|
||||
|
||||
Options Parse(int argc, char** argv) {
|
||||
Options result;
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
const std::string_view argument(argv[index]);
|
||||
const auto value = [&](std::filesystem::path& destination) {
|
||||
if (!destination.empty() || ++index == argc || *argv[index] == '\0') throw std::runtime_error("missing or repeated option: " + std::string(argument));
|
||||
destination = argv[index];
|
||||
};
|
||||
if (argument == "--cache-in") value(result.cacheIn);
|
||||
else if (argument == "--cache-out") value(result.cacheOut);
|
||||
else if (argument == "--output") value(result.output);
|
||||
else if (argument == "--write-fixture") value(result.fixtureDirectory);
|
||||
else if (argument == "--fixture" && !result.fixture) result.fixture = true;
|
||||
else if (argument.starts_with("--") || !result.source.empty()) throw std::runtime_error("unknown or repeated argument: " + std::string(argument));
|
||||
else result.source = argument;
|
||||
}
|
||||
const auto sources = static_cast<int>(!result.source.empty()) + static_cast<int>(result.fixture) + static_cast<int>(!result.fixtureDirectory.empty());
|
||||
if (sources != 1) throw std::runtime_error("select a capture file, --fixture, or --write-fixture DIRECTORY");
|
||||
if (!result.fixtureDirectory.empty() && (!result.cacheIn.empty() || !result.cacheOut.empty() || !result.output.empty())) throw std::runtime_error("--write-fixture cannot be combined with replay options");
|
||||
for (const auto& path : {result.cacheOut, result.output}) {
|
||||
if (!path.empty() && std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string());
|
||||
}
|
||||
if (!result.cacheOut.empty() && !result.output.empty() && std::filesystem::absolute(result.cacheOut).lexically_normal() == std::filesystem::absolute(result.output).lexically_normal()) throw std::runtime_error("cache and report must use different paths");
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::byte> ReadCache(const std::filesystem::path& path) {
|
||||
if (path.empty()) return {};
|
||||
const auto bytes = std::filesystem::file_size(path);
|
||||
if (bytes == 0 || bytes > 64u * 1024u * 1024u) throw std::runtime_error("imported pipeline cache must contain 1 byte to 64 MiB");
|
||||
std::vector<std::byte> result(static_cast<std::size_t>(bytes));
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
input.read(reinterpret_cast<char*>(result.data()), static_cast<std::streamsize>(result.size()));
|
||||
if (!input || input.peek() != std::char_traits<char>::eof()) throw std::runtime_error("could not read complete pipeline cache: " + path.string());
|
||||
return result;
|
||||
}
|
||||
|
||||
void WriteBytes(const std::filesystem::path& path, std::span<const std::byte> bytes) {
|
||||
if (std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string());
|
||||
std::ofstream output(path, std::ios::binary);
|
||||
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
output.close();
|
||||
if (!output) throw std::runtime_error("could not write complete output: " + path.string());
|
||||
}
|
||||
|
||||
std::string Quote(std::string_view text) {
|
||||
std::ostringstream result;
|
||||
result << '"';
|
||||
for (const unsigned char character : text) {
|
||||
if (character == '\\' || character == '"') result << '\\' << character;
|
||||
else if (character < 0x20u) result << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast<unsigned>(character);
|
||||
else result << character;
|
||||
}
|
||||
result << '"';
|
||||
return result.str();
|
||||
}
|
||||
|
||||
std::string Uuid(std::span<const std::uint8_t> bytes) {
|
||||
std::ostringstream result;
|
||||
for (const auto byte : bytes) result << std::hex << std::setw(2) << std::setfill('0') << static_cast<unsigned>(byte);
|
||||
return result.str();
|
||||
}
|
||||
|
||||
template<typename TDevice>
|
||||
void WriteDevice(std::ostream& output, const TDevice& device) {
|
||||
output << "{\"name\":" << Quote(device.deviceName)
|
||||
<< ",\"vendor_id\":" << device.vendorId << ",\"device_id\":" << device.deviceId
|
||||
<< ",\"driver_version_raw\":" << device.driverVersion << ",\"api_version_raw\":" << device.apiVersion
|
||||
<< ",\"pipeline_cache_uuid\":" << Quote(Uuid(device.pipelineCacheUuid)) << '}';
|
||||
}
|
||||
|
||||
std::string Report(const Options& options, const AgcDriver::ComputePipelineCapture& capture, const AgcDriver::ComputePipelineReplayResult& replay) {
|
||||
std::ostringstream output;
|
||||
output << std::setprecision(12)
|
||||
<< "{\n \"schema_version\":1,\n \"source\":" << Quote(options.fixture ? "synthetic-no-op-compute" : options.source.string())
|
||||
<< ",\n \"capture_identity\":" << Quote(capture.Identity())
|
||||
<< ",\n \"capture_identity_algorithm\":\"fnv1a64\""
|
||||
<< ",\n \"spirv_words\":" << capture.spirv.size()
|
||||
<< ",\n \"descriptor_sets\":" << capture.sets.size()
|
||||
<< ",\n \"push_constant_ranges\":" << capture.pushConstants.size()
|
||||
<< ",\n \"specialization_constants\":" << capture.specialization.size()
|
||||
<< ",\n \"required_subgroup_size\":" << capture.requiredSubgroupSize
|
||||
<< ",\n \"pipeline_flags\":" << capture.flags
|
||||
<< ",\n \"stage_flags\":" << capture.stageFlags
|
||||
<< ",\n \"captured_device\":";
|
||||
WriteDevice(output, capture);
|
||||
output << ",\n \"replay_device\":";
|
||||
WriteDevice(output, replay);
|
||||
output << ",\n \"application_cache_policy\":" << Quote(options.cacheIn.empty() ? "fresh" : "imported")
|
||||
<< ",\n \"cache_import_path\":" << Quote(options.cacheIn.string())
|
||||
<< ",\n \"cache_import_bytes\":" << replay.initialCacheBytes
|
||||
<< ",\n \"cache_export_path\":" << Quote(options.cacheOut.string())
|
||||
<< ",\n \"cache_export_bytes\":" << (options.cacheOut.empty() ? 0u : replay.cache.size())
|
||||
<< ",\n \"driver_cache_state\":\"uncontrolled\""
|
||||
<< ",\n \"dispatch_count\":0,\n \"pipeline_statistics\":false"
|
||||
<< ",\n \"milliseconds\":{\"shader_module\":" << replay.moduleMilliseconds
|
||||
<< ",\"layout\":" << replay.layoutMilliseconds << ",\"pipeline_create\":" << replay.pipelineMilliseconds
|
||||
<< ",\"cache\":" << replay.cacheMilliseconds << ",\"replay_total\":" << replay.totalMilliseconds << "}\n}\n";
|
||||
return output.str();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--help") {
|
||||
std::cout << "agc_compute_pipeline_replay CAPTURE|--fixture [--cache-in FILE] [--cache-out FILE] [--output FILE]\n"
|
||||
"agc_compute_pipeline_replay --write-fixture DIRECTORY\n"
|
||||
"Compiles one compute pipeline without dispatch. Fresh application cache does not clear driver caches.\n";
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
const auto options = Parse(argc, argv);
|
||||
if (!options.fixtureDirectory.empty()) {
|
||||
std::cout << AgcDriver::ComputePipelineFixture().Write(options.fixtureDirectory).string() << '\n';
|
||||
return 0;
|
||||
}
|
||||
const auto* statistics = std::getenv("APS5_PIPELINE_STATS");
|
||||
if (statistics != nullptr && std::string_view(statistics) != "0" && *statistics != '\0') throw std::runtime_error("unset APS5_PIPELINE_STATS before benchmarking");
|
||||
const auto capture = options.fixture ? AgcDriver::ComputePipelineFixture() : AgcDriver::ComputePipelineCapture::Read(options.source);
|
||||
if ((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) throw std::runtime_error("capture requests pipeline statistics; capture again with APS5_PIPELINE_STATS disabled");
|
||||
const auto cache = ReadCache(options.cacheIn);
|
||||
AgcDriver::VulkanDevice device(AgcDriver::VulkanDevice::CompileOnly{});
|
||||
const auto replay = device.ReplayComputePipeline(capture, cache);
|
||||
if (!options.cacheOut.empty()) WriteBytes(options.cacheOut, replay.cache);
|
||||
const auto report = Report(options, capture, replay);
|
||||
if (!options.output.empty()) WriteBytes(options.output, std::as_bytes(std::span(report.data(), report.size())));
|
||||
std::cout << report;
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "compute pipeline replay: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -246,6 +246,7 @@ function(add_shader_recompiler target runtimeTarget)
|
|||
SrgbUnormFormat
|
||||
ThreadOwned
|
||||
UnresolvableSwappcTarget
|
||||
ValidateAndOptimizeSpirv
|
||||
FindUserDataCalls
|
||||
UserDataCalleeLength
|
||||
LinkUserDataCalls
|
||||
|
|
|
|||
|
|
@ -89,6 +89,24 @@ ctest --test-dir build/write-watch --output-on-failure
|
|||
|
||||
With `CMAKE_OSX_ARCHITECTURES=x86_64` Apple silicon runs the tests through Rosetta, which is how relinked titles run; without it they run natively. The full `BUILD_TESTING` build on macOS also registers them.
|
||||
|
||||
## Compute pipeline replay
|
||||
|
||||
Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-<identity>/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 20 MiB per record. Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing.
|
||||
|
||||
Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `4194304` fail. The filter applies only when capture is enabled.
|
||||
|
||||
```sh
|
||||
cmake --build build --target agc_compute_pipeline_replay
|
||||
build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-out pipeline.cache --output fresh.json
|
||||
build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-in pipeline.cache --output imported.json
|
||||
```
|
||||
|
||||
On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's device features and compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist.
|
||||
|
||||
Each invocation creates one pipeline. With no `--cache-in`, its application pipeline cache starts empty; `--cache-in` imports a raw cache previously written by `--cache-out` (at most 64 MiB), and `--cache-out` exports the resulting cache. The title's wrapped `vk-pipelines-*.bin` format is not accepted. It does not load or save the title's usual pipeline cache. NVIDIA and other driver caches remain uncontrolled: a fresh application cache is not a cold driver cache. JSON reports the capture identity, GPU/driver identifiers, word count, cache policy and separate wall times for shader module, layouts, pipeline creation and cache work. Device initialization is outside these timings. Pipeline statistics captures and `APS5_PIPELINE_STATS` are refused because they change compilation cost.
|
||||
|
||||
`--fixture` compiles a tiny synthetic compute shader with specialization, sparse descriptor binding and push constant metadata; `--write-fixture DIRECTORY` saves it without opening Vulkan. It performs no GPU work and does not reproduce a title's compile workload. With `BUILD_TESTING=ON`, `agc_compute_pipeline_capture` checks serialization and rejected inputs. With SPIRV-Tools also enabled, `agc_compute_pipeline_replay` compiles the fixture with fresh and imported application caches, verifies cache rejection, and checks required subgroup support. `agc_compute_pipeline_capture_run` verifies the existing branch-past-endpgm compute fixture's output with capture disabled and enabled, then replays the captured pipelines and checks their identities and metadata. Each test has a 30-second timeout. Runtime tests skip when Vulkan is absent; set `ANYPS5_REQUIRE_VULKAN=1` to require execution. There is no performance pass threshold.
|
||||
|
||||
## Pipeline statistics
|
||||
|
||||
Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newly created graphics or compute pipeline. This requires `VK_KHR_pipeline_executable_properties` and `pipelineExecutableInfo`; an unsupported device fails with an error. Statistic names and units are driver-specific. Capturing statistics can increase pipeline compilation cost. The setting is disabled by default.
|
||||
|
|
|
|||
Loading…
Reference in New Issue