mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-18 23:07:07 -05:00
Merge pull request #3717 from jweidenh/codex/nvidia-pipeline-replay
feat(agc): capture and replay compute pipeline compilation
This commit is contained in:
15 files changed
+1304
-26
No files matched your search
@@ -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
|
||||
@@ -1279,6 +1280,60 @@ 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})
|
||||
target_link_libraries(agc_compute_pipeline_capture_tests PRIVATE libc)
|
||||
set_target_properties(agc_compute_pipeline_capture_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_compute_pipeline_capture_tests)
|
||||
add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags)
|
||||
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
|
||||
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_compute_pipeline_capture PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
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,77 @@
|
||||
#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::uint32_t MaxSpirvWords = 8u * 1024u * 1024u;
|
||||
static constexpr std::size_t MaxFileBytes = 36u * 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,265 @@
|
||||
#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 36 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() <= MaxSpirvWords, "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;
|
||||
const auto highest = layout.bindings.empty() ? 0u : std::ranges::max(layout.bindings, {}, &ComputeDescriptorBinding::binding).binding;
|
||||
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");
|
||||
constexpr VkDescriptorBindingFlags supported = VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
|
||||
RequireCapture((binding.flags & ~supported) == 0, "unsupported descriptor binding flags");
|
||||
RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || (binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported update-after-bind descriptor");
|
||||
RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || binding.binding == highest, "variable descriptor count must use the highest binding");
|
||||
}
|
||||
}
|
||||
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 36 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(MaxSpirvWords, 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,50 @@ void require(bool condition, const char* reason) {
|
||||
if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason);
|
||||
}
|
||||
|
||||
bool CoreVulkanCapability(std::uint32_t capability) {
|
||||
switch (capability) {
|
||||
case spv::CapabilityMatrix:
|
||||
case spv::CapabilityShader:
|
||||
case spv::CapabilityInputAttachment:
|
||||
case spv::CapabilitySampled1D:
|
||||
case spv::CapabilityImage1D:
|
||||
case spv::CapabilitySampledBuffer:
|
||||
case spv::CapabilityImageBuffer:
|
||||
case spv::CapabilityImageQuery:
|
||||
case spv::CapabilityDerivativeControl:
|
||||
case spv::CapabilityStorageImageExtendedFormats:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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 <= ComputePipelineCapture::MaxSpirvWords, "APS5_DUMP_COMPUTE_MIN_WORDS must be an integer from 0 through 8388608");
|
||||
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 +225,7 @@ struct WorkgroupMemory {
|
||||
};
|
||||
|
||||
struct VulkanDevice::State {
|
||||
bool compileOnly = false;
|
||||
void* library = nullptr;
|
||||
PFN_vkGetInstanceProcAddr instanceProc = nullptr;
|
||||
PFN_vkGetDeviceProcAddr deviceProc = nullptr;
|
||||
@@ -220,6 +268,9 @@ struct VulkanDevice::State {
|
||||
// graphics stages, and compute workgroups wider than a wave).
|
||||
bool descriptorIndexing = false;
|
||||
bool storageBufferUpdateAfterBind = false;
|
||||
bool descriptorBindingPartiallyBound = false;
|
||||
bool descriptorBindingVariableDescriptorCount = false;
|
||||
bool runtimeDescriptorArray = false;
|
||||
VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT};
|
||||
bool imageInt64Atomics = false;
|
||||
bool bufferInt64Atomics = false;
|
||||
@@ -567,6 +618,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 +711,165 @@ 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 && (CoreVulkanCapability(capture.spirv[cursor + 1]) || 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;
|
||||
std::uint32_t variableCount = 0;
|
||||
for (const auto& binding : layout.bindings) {
|
||||
require((binding.flags & VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT) == 0 || state->descriptorBindingPartiallyBound, "captured partially-bound descriptors are unsupported");
|
||||
require((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || state->descriptorBindingVariableDescriptorCount, "captured variable descriptor count is unsupported");
|
||||
if ((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0) variableCount = binding.count;
|
||||
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 = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr;
|
||||
info.flags = layout.flags;
|
||||
info.bindingCount = static_cast<std::uint32_t>(bindings.size());
|
||||
info.pBindings = bindings.data();
|
||||
VkDescriptorSetVariableDescriptorCountLayoutSupport variableSupport{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_LAYOUT_SUPPORT};
|
||||
VkDescriptorSetLayoutSupport support{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_SUPPORT, variableCount != 0 ? &variableSupport : nullptr};
|
||||
state->DeviceFunction<PFN_vkGetDescriptorSetLayoutSupport>("vkGetDescriptorSetLayoutSupport")(state->device, &info, &support);
|
||||
require(support.supported == VK_TRUE, "captured descriptor set layout is unsupported");
|
||||
require(variableCount <= variableSupport.maxVariableDescriptorCount, "captured variable descriptor count exceeds layout support");
|
||||
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 +1179,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());
|
||||
}
|
||||
@@ -1147,13 +1360,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
|
||||
state->descriptorIndexing = descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing == VK_TRUE;
|
||||
state->storageBufferUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE;
|
||||
state->descriptorBindingPartiallyBound = compileOnly && descriptorIndexingFeatures.descriptorBindingPartiallyBound == VK_TRUE;
|
||||
state->descriptorBindingVariableDescriptorCount = compileOnly && descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount == VK_TRUE;
|
||||
state->runtimeDescriptorArray = compileOnly && descriptorIndexingFeatures.runtimeDescriptorArray == VK_TRUE;
|
||||
if (state->storageBufferUpdateAfterBind) {
|
||||
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &state->descriptorIndexingProperties};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
|
||||
}
|
||||
}
|
||||
descriptorIndexingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT};
|
||||
if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
|
||||
if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
|
||||
if (state->descriptorIndexing) {
|
||||
descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
|
||||
descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing = VK_TRUE;
|
||||
@@ -1163,6 +1379,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
||||
state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing");
|
||||
}
|
||||
descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind = state->storageBufferUpdateAfterBind ? VK_TRUE : VK_FALSE;
|
||||
descriptorIndexingFeatures.descriptorBindingPartiallyBound = state->descriptorBindingPartiallyBound ? VK_TRUE : VK_FALSE;
|
||||
descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount = state->descriptorBindingVariableDescriptorCount ? VK_TRUE : VK_FALSE;
|
||||
descriptorIndexingFeatures.runtimeDescriptorArray = state->runtimeDescriptorArray ? VK_TRUE : VK_FALSE;
|
||||
if (state->runtimeDescriptorArray) {
|
||||
state->capabilities.push_back(spv::CapabilityRuntimeDescriptorArray);
|
||||
if (!state->descriptorIndexing) state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing");
|
||||
}
|
||||
state->samplerAnisotropy = true;
|
||||
state->textureCompressionBC = true;
|
||||
deviceInfo.pEnabledFeatures = &enabled;
|
||||
@@ -1230,7 +1453,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
||||
imageRobustnessFeatures.pNext = byteFeatures.pNext;
|
||||
byteFeatures.pNext = &imageRobustnessFeatures;
|
||||
}
|
||||
if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) {
|
||||
if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) {
|
||||
descriptorIndexingFeatures.pNext = byteFeatures.pNext;
|
||||
byteFeatures.pNext = &descriptorIndexingFeatures;
|
||||
}
|
||||
@@ -1310,6 +1533,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 +4057,39 @@ 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;
|
||||
const auto layoutBindings = resources->LayoutBindings();
|
||||
const auto bindingFlags = resources->LayoutBindingFlags();
|
||||
require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout");
|
||||
for (std::size_t index = 0; index < layoutBindings.size(); ++index) {
|
||||
const auto& binding = layoutBindings[index];
|
||||
require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers");
|
||||
resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]});
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -60,7 +60,7 @@ public:
|
||||
|
||||
// The layout for `key` (binding, type, count, stage flags per binding, as ShaderResources builds
|
||||
// it from `bindings`), created on first use.
|
||||
VkDescriptorSetLayout Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind = false);
|
||||
VkDescriptorSetLayout Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind = false, std::span<const VkDescriptorBindingFlags> flags = {});
|
||||
struct SetAllocation {
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
@@ -114,6 +114,9 @@ public:
|
||||
ShaderResources(const ShaderResources&) = delete;
|
||||
ShaderResources& operator=(const ShaderResources&) = delete;
|
||||
VkDescriptorSetLayout Layout() const;
|
||||
std::vector<VkDescriptorSetLayoutBinding> LayoutBindings() const;
|
||||
bool LayoutUpdateAfterBind() const;
|
||||
std::span<const VkDescriptorBindingFlags> LayoutBindingFlags() const { return layoutBindingFlags; }
|
||||
void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const;
|
||||
struct DrawBindings {
|
||||
DrawBindings() = default;
|
||||
@@ -475,6 +478,7 @@ private:
|
||||
// still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader),
|
||||
// the descriptor counts the set was sized for, and the compute stage of a deferred build.
|
||||
std::vector<Binding> bindings;
|
||||
std::vector<VkDescriptorBindingFlags> layoutBindingFlags;
|
||||
std::vector<std::uint32_t> refreshResourceKey;
|
||||
struct DeferredImages {
|
||||
const ShaderRecompiler::DescriptorBinding* binding;
|
||||
|
||||
@@ -1146,14 +1146,13 @@ void ShaderResources::build(std::span<const CompiledShader> shaders, const Color
|
||||
|
||||
namespace {
|
||||
|
||||
VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind) {
|
||||
std::vector<VkDescriptorBindingFlags> flags;
|
||||
for (const auto& binding : bindings) flags.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0);
|
||||
VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind, std::span<const VkDescriptorBindingFlags> flags) {
|
||||
Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout");
|
||||
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.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr;
|
||||
info.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0;
|
||||
info.bindingCount = static_cast<std::uint32_t>(bindings.size());
|
||||
info.pBindings = bindings.data();
|
||||
@@ -1415,16 +1414,17 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
||||
std::vector<VkDescriptorSetLayoutBinding> description;
|
||||
for (const auto& binding : bindings) {
|
||||
description.push_back(binding.layout);
|
||||
layoutBindingFlags.push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0);
|
||||
layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast<std::uint32_t>(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags});
|
||||
}
|
||||
if (updateAfterBind) layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
|
||||
static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr;
|
||||
static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr;
|
||||
if (context.descriptorCache != nullptr && !noLayoutCache) {
|
||||
_layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind);
|
||||
_layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind, layoutBindingFlags);
|
||||
} else {
|
||||
ValidateDescriptorLayout(context, description, updateAfterBind);
|
||||
_layout = CreateSetLayout(context, description, updateAfterBind);
|
||||
_layout = CreateSetLayout(context, description, updateAfterBind, layoutBindingFlags);
|
||||
ownsLayout = true;
|
||||
}
|
||||
if (!bindings.empty()) {
|
||||
@@ -2626,16 +2626,24 @@ DescriptorCache::~DescriptorCache() {
|
||||
for (const auto& [key, layout] : layouts) destroyLayout(context.device, layout, nullptr);
|
||||
}
|
||||
|
||||
VkDescriptorSetLayout DescriptorCache::Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind) {
|
||||
VkDescriptorSetLayout DescriptorCache::Layout(std::span<const std::uint32_t> key, std::span<const VkDescriptorSetLayoutBinding> bindings, bool updateAfterBind, std::span<const VkDescriptorBindingFlags> flags) {
|
||||
std::vector<VkDescriptorBindingFlags> defaults;
|
||||
if (flags.empty()) {
|
||||
for (const auto& binding : bindings) defaults.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0);
|
||||
flags = defaults;
|
||||
}
|
||||
Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout");
|
||||
std::lock_guard lock(mutex);
|
||||
std::vector<std::uint32_t> keyCopy(key.begin(), key.end());
|
||||
keyCopy.push_back(static_cast<std::uint32_t>(flags.size()));
|
||||
keyCopy.insert(keyCopy.end(), flags.begin(), flags.end());
|
||||
if (const auto found = layouts.find(keyCopy); found != layouts.end()) {
|
||||
++stats.layoutHits;
|
||||
return found->second;
|
||||
}
|
||||
ValidateDescriptorLayout(context, bindings, updateAfterBind);
|
||||
++stats.layoutMisses;
|
||||
const auto layout = CreateSetLayout(context, bindings, updateAfterBind);
|
||||
const auto layout = CreateSetLayout(context, bindings, updateAfterBind, flags);
|
||||
layouts.emplace(std::move(keyCopy), layout);
|
||||
return layout;
|
||||
}
|
||||
@@ -3331,6 +3339,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,172 @@
|
||||
#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 CheckLargeRecord() {
|
||||
auto capture = AgcDriver::ComputePipelineFixture();
|
||||
constexpr std::size_t words = 5500000;
|
||||
capture.spirv.insert(capture.spirv.end() - 2, words - capture.spirv.size(), 0x00010000u);
|
||||
const auto encoded = capture.Encode();
|
||||
Require(encoded.size() > 20u * 1024u * 1024u, "large fixture did not exceed the former file bound");
|
||||
Require(AgcDriver::ComputePipelineCapture::Decode(encoded).spirv == capture.spirv, "large module changed during decoding");
|
||||
Directory directory;
|
||||
const auto file = capture.Write(directory.path);
|
||||
Require(AgcDriver::ComputePipelineCapture::Read(file).spirv == capture.spirv, "large module changed on disk");
|
||||
capture.spirv.resize(AgcDriver::ComputePipelineCapture::MaxSpirvWords + 1u, 0x00010000u);
|
||||
Reject([&] { capture.Encode(); }, "module beyond the word bound accepted");
|
||||
std::vector<std::byte> oversized(AgcDriver::ComputePipelineCapture::MaxFileBytes + 1u);
|
||||
Reject([&] { AgcDriver::ComputePipelineCapture::Decode(oversized); }, "record beyond the file bound accepted");
|
||||
const auto oversizedFile = directory.path / "oversized.aps5cp";
|
||||
{
|
||||
std::ofstream output(oversizedFile, std::ios::binary);
|
||||
output.seekp(static_cast<std::streamoff>(AgcDriver::ComputePipelineCapture::MaxFileBytes));
|
||||
output.put('x');
|
||||
Require(output.good(), "cannot write oversized capture fixture");
|
||||
}
|
||||
Reject([&] { AgcDriver::ComputePipelineCapture::Read(oversizedFile); }, "file beyond the file bound accepted");
|
||||
}
|
||||
|
||||
void CheckBindingFlags() {
|
||||
auto capture = AgcDriver::ComputePipelineFixture();
|
||||
capture.sets = {{0, {{7, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 4, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT},
|
||||
{3, VK_DESCRIPTOR_TYPE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT}}}};
|
||||
const auto encoded = capture.Encode();
|
||||
Require(AgcDriver::ComputePipelineCapture::Decode(encoded).Encode() == encoded, "binding flags changed without update-after-bind");
|
||||
auto changed = capture;
|
||||
changed.sets[0].bindings[0].flags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT;
|
||||
Require(changed.Identity() != capture.Identity(), "binding flags did not affect capture identity");
|
||||
changed = capture;
|
||||
changed.sets[0].bindings[1].flags |= VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
|
||||
Reject([&] { changed.Encode(); }, "non-highest variable descriptor binding accepted");
|
||||
changed = capture;
|
||||
changed.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT;
|
||||
Reject([&] { changed.Encode(); }, "unsupported binding flags accepted");
|
||||
capture.sets[0].flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT;
|
||||
capture.sets[0].bindings[0].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
|
||||
capture.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT;
|
||||
Require(AgcDriver::ComputePipelineCapture::Decode(capture.Encode()).Encode() == capture.Encode(), "combined binding flags 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();
|
||||
CheckBindingFlags();
|
||||
CheckLargeRecord();
|
||||
std::puts("compute pipeline capture: round trip, 5500000-word module, binding flags, immutable writes and bounded 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,122 @@
|
||||
#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;
|
||||
}
|
||||
auto sampled1D = decoded;
|
||||
sampled1D.spirv.insert(sampled1D.spirv.begin() + 7, {0x00020011u, 43u});
|
||||
CheckResult(device->ReplayComputePipeline(sampled1D));
|
||||
std::puts("compute pipeline replay: core Vulkan Sampled1D capability compiled");
|
||||
for (const auto capability : {6u, 0xffffffffu}) {
|
||||
auto unsupported = decoded;
|
||||
unsupported.spirv.insert(unsupported.spirv.begin() + 7, {0x00020011u, capability});
|
||||
bool refused = false;
|
||||
try {
|
||||
device->ReplayComputePipeline(unsupported);
|
||||
} catch (const std::exception& error) {
|
||||
Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "unsupported capability failed outside the replay capability guard");
|
||||
refused = true;
|
||||
}
|
||||
Require(refused, "non-Vulkan or unknown SPIR-V capability accepted");
|
||||
}
|
||||
std::puts("compute pipeline replay: non-Vulkan Kernel and unknown capabilities rejected");
|
||||
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");
|
||||
for (const auto flags : {VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, static_cast<VkDescriptorBindingFlagBits>(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT)}) {
|
||||
auto indexed = decoded;
|
||||
indexed.sets[0].bindings[0].flags = flags;
|
||||
try {
|
||||
CheckResult(device->ReplayComputePipeline(indexed));
|
||||
std::printf("compute pipeline replay: descriptor binding flags 0x%x compiled without update-after-bind\n", static_cast<unsigned>(flags));
|
||||
} catch (const std::exception& error) {
|
||||
if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw;
|
||||
const std::string_view message(error.what());
|
||||
Require(message == "Vulkan presentation: captured partially-bound descriptors are unsupported" || message == "Vulkan presentation: captured variable descriptor count is unsupported", "binding flag replay failed without an explicit feature refusal");
|
||||
std::printf("compute pipeline replay: descriptor binding flags 0x%x explicitly refused: %s\n", static_cast<unsigned>(flags), error.what());
|
||||
}
|
||||
}
|
||||
auto runtimeArray = decoded;
|
||||
runtimeArray.spirv.insert(runtimeArray.spirv.begin() + 7, {0x00020011u, 5302u, 0x0008000au, 0x5f565053u, 0x5f545845u, 0x63736564u, 0x74706972u, 0x695f726fu, 0x7865646eu, 0x00676e69u});
|
||||
try {
|
||||
CheckResult(device->ReplayComputePipeline(runtimeArray));
|
||||
std::puts("compute pipeline replay: runtime descriptor array capability compiled");
|
||||
} catch (const std::exception& error) {
|
||||
if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw;
|
||||
Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "runtime descriptor array replay failed without an explicit feature refusal");
|
||||
std::puts("compute pipeline replay: device explicitly refused runtime descriptor array capability");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2223,6 +2223,42 @@ void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, st
|
||||
Require(mock.live == 0, std::string(what) + " leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void descriptorLayoutFlagTests() {
|
||||
constexpr VkDescriptorBindingFlags indexed = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT;
|
||||
mock = MockVulkan{};
|
||||
{
|
||||
AgcDriver::Graphics::DescriptorCache cache(mockContext());
|
||||
const VkDescriptorSetLayoutBinding binding{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
|
||||
const std::array<std::uint32_t, 4> key{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT};
|
||||
const std::array<VkDescriptorBindingFlags, 1> flags{indexed};
|
||||
const auto layout = cache.Layout(key, std::span(&binding, 1), false, flags);
|
||||
Require(mock.layoutFlags == 0 && mock.layoutBindingFlags == std::vector<VkDescriptorBindingFlags>{indexed}, "native layout lost binding flags without update-after-bind");
|
||||
Require(cache.Layout(key, std::span(&binding, 1), false, flags) == layout, "identical binding flags missed the layout cache");
|
||||
Require(cache.Layout(key, std::span(&binding, 1)) != layout && mock.layoutBindingFlags.empty(), "layout cache ignored binding flags");
|
||||
}
|
||||
Require(mock.live == 0, "binding flag layout cache leaked Vulkan objects");
|
||||
for (const bool updateAfterBind : {false, true}) {
|
||||
mock = MockVulkan{};
|
||||
auto context = mockContext();
|
||||
context.emptyBuffer = reinterpret_cast<VkBuffer>(std::uintptr_t{0x1000});
|
||||
context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17;
|
||||
context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128;
|
||||
context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32;
|
||||
{
|
||||
ShaderRecompiler::RecompileResult program;
|
||||
const std::uint32_t count = updateAfterBind ? 17 : 1;
|
||||
program.bindings.push_back(makeBinding(Role::GuestBuffers, 0, count, std::vector<std::uint32_t>(4 * count)));
|
||||
const AgcDriver::Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0};
|
||||
AgcDriver::Graphics::ShaderResources resources(context, compute);
|
||||
const auto flags = resources.LayoutBindingFlags();
|
||||
Require(flags.size() == resources.LayoutBindings().size() && flags.size() == 1, "exported binding flag count differs from native layout");
|
||||
Require(flags[0] == (updateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0), "exported binding flags differ from native layout");
|
||||
Require(updateAfterBind ? mock.layoutBindingFlags == std::vector<VkDescriptorBindingFlags>(flags.begin(), flags.end()) : mock.layoutBindingFlags.empty(), "native layout did not use the exported binding flag vector");
|
||||
}
|
||||
Require(mock.live == 0, "exported binding flag fixture leaked Vulkan objects");
|
||||
}
|
||||
}
|
||||
|
||||
void descriptorPoolTests() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
mock = MockVulkan{};
|
||||
@@ -4322,6 +4358,11 @@ int main(int argc, char** argv) {
|
||||
setenv("APS5_HEAP_MIRROR_MIB", "4", 1);
|
||||
#endif
|
||||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") {
|
||||
descriptorLayoutFlagTests();
|
||||
std::cout << "Descriptor layout native and captured binding flags passed\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-reset-failure") {
|
||||
descriptorPoolResetFailureTest();
|
||||
std::cerr << "descriptor pool reset failure did not terminate\n";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -90,6 +90,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 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). 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 `8388608` 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/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Compile-only devices also enable supported partially-bound, variable-count and runtime-array descriptor features to replay captured layouts; ordinary runtime devices are unchanged. Binding flags are preserved independently of update-after-bind. Replay accepts the unconditional Vulkan 1.0 capabilities listed in the pinned [Vulkan registry](https://github.com/KhronosGroup/Vulkan-Headers/blob/2fa203425eb4af9dfc6b03f97ef72b0b5bcb8350/registry/vk.xml); other capabilities must be enabled by the device target. Non-Vulkan capabilities such as Kernel and unknown capabilities fail before pipeline creation. Unsupported features, non-highest variable bindings and counts exceeding layout support fail explicitly. 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.
|
||||
|
||||
Reference in new issue
Block a user