Merge pull request #3717 from jweidenh/codex/nvidia-pipeline-replay

feat(agc): capture and replay compute pipeline compilation
This commit is contained in:
Adrià authored and GitHub committed 2026-10-11 23:54:52 +02:00
commit 06664a44cd
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;
}
}
+1
View File
@@ -246,6 +246,7 @@ function(add_shader_recompiler target runtimeTarget)
SrgbUnormFormat
ThreadOwned
UnresolvableSwappcTarget
ValidateAndOptimizeSpirv
FindUserDataCalls
UserDataCalleeLength
LinkUserDataCalls
+18
View File
@@ -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.