Merge pull request #4152 from adriadam10/feat/phase-b

perf(agc): build shader pipeline libraries when shaders are registered
This commit is contained in:
Adrià 2026-10-12 10:31:42 +02:00 committed by GitHub
commit 2979ae37a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 857 additions and 170 deletions

View File

@ -1010,6 +1010,8 @@ target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestSh
target_include_directories(agc_driver_prepared_exports_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_prepared_publication_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)

View File

@ -37,6 +37,9 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
struct PreparedGraphicsStage {
std::shared_ptr<const ShaderSnapshot> snapshot;
PreparedShaders::Entry entry;
ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex;
std::uint32_t pushOffset = 0;
std::shared_ptr<const ShaderRecompiler::RecompileResult> registration;
};
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target);

View File

@ -103,6 +103,7 @@ std::shared_ptr<const ShaderSnapshot> RegisteredHeader(const ShaderRegistry& reg
std::shared_ptr<const ShaderSnapshot> RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list<std::uint8_t> types);
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);
void SubmitRegistrationPreparation(std::function<void()> task);
void SubmitLibraryPrebuild(std::function<void()> task);
void CloseRegistrationPreparation();
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target);

View File

@ -299,6 +299,7 @@ public:
};
IndirectDrawSupport DrawIndirectSupport() const;
void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {});
bool PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span<const Graphics::CompiledShader> shaders);
private:
VulkanDevice(const PresentationWindow* window, bool compileOnly);

View File

@ -21,6 +21,7 @@
#include <deque>
#include <functional>
#include <list>
#include <atomic>
#include <pthread.h>
#include <stdexcept>
#include <thread>
@ -30,7 +31,8 @@ namespace AgcDriver::DriverDetail {
std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation) {
try {
return ShaderRecompiler::PrepareShader(request, preparation);
auto handle = ShaderRecompiler::PrepareShader(request, preparation);
return handle;
} catch (...) {
try {
if (std::getenv("APS5_DUMP_SHADERS") != nullptr) static_cast<void>(Driver::dumpRequest(request.shader.codeAddress, request));
@ -571,6 +573,7 @@ struct RegisteredPreparation {
std::array<ShaderRecompiler::MemoryRegion, 2> memory{};
std::optional<ShaderRecompiler::RecompileRequest> request;
std::vector<std::uint64_t> abiKey;
std::shared_ptr<VulkanDevice> prebuildDevice;
};
std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration, bool* deferred = nullptr) {
@ -669,6 +672,18 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
return plan;
}
void PrebuildStageLibrary(const std::shared_ptr<VulkanDevice>& device, const Graphics::ShaderStages& stages, ShaderRecompiler::ShaderStage stage, std::shared_ptr<const ShaderRecompiler::RecompileResult> result, std::uint32_t pushOffset) {
if (result == nullptr || (stage != ShaderRecompiler::ShaderStage::Vertex && stage != ShaderRecompiler::ShaderStage::Fragment)) return;
SubmitLibraryPrebuild([device, stages, stage, result = std::move(result), pushOffset] {
const std::array<Graphics::CompiledShader, 1> shaders{{{stage, result.get(), pushOffset}}};
try {
static_cast<void>(device->PrebuildGraphicsLibraries(stages, shaders));
} catch (const std::exception& error) {
APS5_LOG_OUT("shader library prebuild skipped: %s", error.what());
}
});
}
std::vector<PreparedShaders::Entry> PrepareRegistered(RegisteredPreparation& plan) {
auto& request = *plan.request;
std::vector<PreparedShaders::Entry> entries;
@ -678,6 +693,13 @@ std::vector<PreparedShaders::Entry> PrepareRegistered(RegisteredPreparation& pla
entries.push_back({plan.codeOffset, PrepareShaderWithDiagnostics(request, &preparation)});
};
append();
if (plan.prebuildDevice != nullptr && request.target.registrationModules && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment) {
try {
PrebuildStageLibrary(plan.prebuildDevice, {Graphics::ShaderPath::Vertex, 0u, 32u, request.context.waveSize, {}, {}}, request.shader.stage, ShaderRecompiler::RegistrationResult(request, *entries.front().handle), request.layout.pushConstantOffsetBytes);
} catch (const std::exception& error) {
APS5_LOG_OUT("shader library prebuild skipped: %s", error.what());
}
}
if (request.context.compute) {
request.context.compute->partialThreads = {1, 1, 1};
append();
@ -696,10 +718,32 @@ std::vector<PreparedShaders::Entry> PrepareRegistered(const ShaderSnapshot& snap
class RegistrationPreparePool {
public:
static RegistrationPreparePool& Instance() {
static auto* pool = new RegistrationPreparePool();
static auto* pool = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
return *pool;
}
static RegistrationPreparePool& Prebuild() {
static auto* pool = [] {
const char* text = std::getenv("APS5_PREBUILD_THREADS");
auto* created = new RegistrationPreparePool(text != nullptr ? std::max(1u, static_cast<unsigned>(std::strtoul(text, nullptr, 10))) : 1u);
PrebuildSlot().store(created);
return created;
}();
return *pool;
}
static RegistrationPreparePool* ExistingPrebuild() {
return PrebuildSlot().load();
}
private:
static std::atomic<RegistrationPreparePool*>& PrebuildSlot() {
static std::atomic<RegistrationPreparePool*> slot{nullptr};
return slot;
}
public:
void Submit(std::function<void()> task) {
{
std::unique_lock lock(mutex);
@ -720,8 +764,7 @@ public:
}
private:
RegistrationPreparePool() {
const auto count = std::max(2u, std::thread::hardware_concurrency() / 2u);
explicit RegistrationPreparePool(unsigned count) {
pthread_attr_t attributes;
pthread_attr_init(&attributes);
pthread_attr_setstacksize(&attributes, 64u << 20u);
@ -802,8 +845,13 @@ void SubmitRegistrationPreparation(std::function<void()> task) {
RegistrationPreparePool::Instance().Submit(std::move(task));
}
void SubmitLibraryPrebuild(std::function<void()> task) {
RegistrationPreparePool::Prebuild().Submit(std::move(task));
}
void CloseRegistrationPreparation() {
RegistrationPreparePool::Instance().Close();
if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close();
}
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) {
@ -860,7 +908,15 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
}
const auto bytes = handle->artifact->bindings.pushConstantSizeBytes;
require(bytes <= room, "prepared graphics stages exceed the push constant block");
prepared.push_back({program.snapshot, {program.codeOffset, handle}});
std::shared_ptr<const ShaderRecompiler::RecompileResult> registration;
if (target.registrationModules) {
try {
registration = ShaderRecompiler::RegistrationResult(request, *handle);
} catch (const std::exception& error) {
APS5_LOG_OUT("shader library prebuild skipped: %s", error.what());
}
}
prepared.push_back({program.snapshot, {program.codeOffset, handle}, program.binary.stage, offset, std::move(registration)});
pushOffset += bytes;
}
transaction.Commit();
@ -930,6 +986,11 @@ void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std
if (stage.snapshot->type == 1) continue;
ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target());
}
if (decoded.state.stages.path == Graphics::ShaderPath::Vertex) {
for (const auto& stage : prepared) {
if (stage.registration != nullptr) PrebuildStageLibrary(localDevice, decoded.state.stages, stage.stage, stage.registration, stage.pushOffset);
}
}
PreparedShaderState::GraphicsAbi abi{key, registry, {}};
for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot);
auto& graphicsAbis = transaction.Edit(*owner).graphicsAbis;
@ -970,14 +1031,20 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegist
struct RegisteredAbiKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, RegisteredAbiKeyStorage>();
BuildRegisteredAbiKey(state, *localDevice, key);
const auto prepare = [&] {
const auto plan = PlanRegistered(*snapshot, *localDevice, state, false);
if (plan == nullptr) return std::vector<PreparedShaders::Entry>{};
plan->prebuildDevice = localDevice;
return PrepareRegistered(*plan);
};
const bool committedAbi = CommittedAbi(*snapshot, key);
auto entries = committedAbi ? std::vector<PreparedShaders::Entry>{} : PrepareRegistered(*snapshot, *localDevice, state, false);
auto entries = committedAbi ? std::vector<PreparedShaders::Entry>{} : prepare();
const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis;
if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) {
transaction.Commit();
return;
}
if (committedAbi) entries = PrepareRegistered(*snapshot, *localDevice, state, false);
if (committedAbi) entries = prepare();
auto& prepared = transaction.Edit(*snapshot);
for (auto& entry : entries) {
const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; });
@ -1164,6 +1231,7 @@ void Driver::RegisterShader(const Shader* shader) {
std::shared_ptr<RegisteredPreparation> preparation;
if (AsyncRegistrationPrepare()) {
preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred);
if (preparation != nullptr) preparation->prebuildDevice = localDevice;
if (preparation != nullptr && (snapshot.type == 0 || snapshot.type == 1)) BuildRegisteredAbiKey(registered, *localDevice, preparation->abiKey);
snapshot.prepared->pending = preparation != nullptr;
} else {

View File

@ -2937,6 +2937,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
target.narrowSubgroupClock = state->narrowSubgroupClock;
target.float64DenormPreserve = state->float64DenormPreserve;
target.fixedPushSlots = state->graphicsPipelineLibrary;
target.registrationModules = state->graphicsPipelineLibrary;
target.srgbDecodeFormats = state->srgbDecodeFormats;
if (state->meshShader) {
const auto& mesh = state->meshLimits;
@ -3068,6 +3069,44 @@ void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) {
Graphics::RunColorMetadataPass(graphicsContext(), pass);
}
bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stages, std::span<const Graphics::CompiledShader> shaders) {
PerformanceTimer timing("Vulkan.GraphicsPipelinePrebuild");
const auto context = graphicsContext();
if (!context.graphicsPipelineLibrary || context.descriptorCache == nullptr) return false;
Graphics::State state{};
state.stages = stages;
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
state.color.format = VK_FORMAT_R8G8B8A8_UNORM;
state.color.exportIndex = 0;
state.colors.push_back(state.color);
state.hasColorTarget = true;
state.blend.colorWriteMask = 15;
state.blends.push_back(state.blend);
state.renderExtent = {1, 1};
std::vector<Graphics::CompiledShader> group(shaders.begin(), shaders.end());
if (group.size() == 1u) {
const bool vertex = group.front().stage == ShaderRecompiler::ShaderStage::Fragment;
static const auto stub = [](bool vertexStub) {
auto result = std::make_shared<ShaderRecompiler::RecompileResult>();
const std::uint32_t model = vertexStub ? 0u : 4u;
std::vector<std::uint32_t> words{0x07230203u, 0x00010000u, 0u, 6u, 0u, 0x00020011u, 1u, 0x0003000eu, 0u, 1u, 0x0005000fu, model, 4u, 0x6e69616du, 0u};
if (!vertexStub) words.insert(words.end(), {0x00030010u, 4u, 7u});
words.insert(words.end(), {0x00020013u, 2u, 0x00030021u, 3u, 2u, 0x00050036u, 2u, 4u, 0u, 3u, 0x000200f8u, 5u, 0x000100fdu, 0x00010038u});
result->spirv = std::move(words);
result->variantId = vertexStub ? ~std::uint64_t{0} - 1u : ~std::uint64_t{0} - 2u;
result->runtimeAbiVersion = ShaderRecompiler::RuntimeAbi::Version;
return result;
};
static const auto vertexStub = stub(true);
static const auto fragmentStub = stub(false);
if (vertex) group.insert(group.begin(), {ShaderRecompiler::ShaderStage::Vertex, vertexStub.get(), 0u});
else group.push_back({ShaderRecompiler::ShaderStage::Fragment, fragmentStub.get(), Graphics::PipelinePushSlotBytes});
}
const auto layouts = Graphics::DescribeSetLayouts(context, group);
return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), group);
}
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe) {
for (const auto& shader : shaders) {
require(shader.program != nullptr, "missing compiled shader");

View File

@ -41,7 +41,9 @@ public:
// before, during and after the pass (GENERAL for resident targets, which then need no
// transitions).
Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
~Pipeline();
static bool Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders);
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
VkPipelineLayout Layout() const;
@ -98,6 +100,15 @@ private:
// only evicted once no recorded draw holds them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per
// draw as before.
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
struct DescribedSetLayouts {
PipelineSetLayouts layouts;
std::vector<std::uint32_t> mainKey;
std::vector<std::uint32_t> fragmentKey;
PipelineSetLayouts View() const { return {layouts.main, layouts.fragment, layouts.split, mainKey, fragmentKey}; }
};
DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span<const CompiledShader> shaders);
bool SplitsSets(const Context& context, std::span<const CompiledShader> shaders);
// Destroys the cached pipelines of a device; to be called before the device goes away. Without it,
// entries of a gone device are recognised by their buffer pool (made and reset with the device, so
// it tells device instances apart when the loader reuses a VkDevice handle) and forgotten unused.

View File

@ -19,6 +19,8 @@ struct PipelineLibraryKeys {
std::vector<std::byte> fragmentOutput;
std::vector<std::byte> renderPass;
std::vector<std::byte> layout;
std::vector<std::byte> preRasterizationLayout;
std::vector<std::byte> fragmentLayout;
};
std::span<const VkDynamicState> PipelineLibraryDynamicStates();
@ -31,7 +33,7 @@ struct OptimizedPipeline {
void Release() noexcept;
};
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized = nullptr, std::span<VkShaderModule> modules = {});
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized = nullptr, bool partsOnly = false);
void WaitForOptimizedPipelines(VkDevice device);

View File

@ -51,6 +51,14 @@ bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddres
// a storage image other than `except` has results pending in [address, address + bytes).
bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const StorageTexture* except);
struct PipelineSetLayouts {
VkDescriptorSetLayout main = VK_NULL_HANDLE;
VkDescriptorSetLayout fragment = VK_NULL_HANDLE;
bool split = false;
std::span<const std::uint32_t> mainKey;
std::span<const std::uint32_t> fragmentKey;
};
class DescriptorCache {
public:
explicit DescriptorCache(const Context& context);
@ -117,6 +125,9 @@ public:
std::vector<VkDescriptorSetLayoutBinding> LayoutBindings() const;
bool LayoutUpdateAfterBind() const;
std::span<const VkDescriptorBindingFlags> LayoutBindingFlags() const { return layoutBindingFlags; }
VkDescriptorSetLayout FragmentLayout() const { return fragment.layout; }
bool SplitSets() const { return splitSets; }
PipelineSetLayouts SetLayouts() const { return {_layout, fragment.layout, splitSets, layoutKey, fragmentLayoutKey}; }
void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const;
struct DrawBindings {
DrawBindings() = default;
@ -127,6 +138,7 @@ public:
std::shared_ptr<Buffer> buffer;
};
DescriptorCache* cache = nullptr;
DescriptorCache::SetAllocation fragmentAllocation;
DescriptorCache::SetAllocation allocation;
std::vector<Snapshot> snapshots;
~DrawBindings();
@ -165,6 +177,7 @@ public:
bool ReadsImage(const StorageTexture* image) const;
bool HasCachedStorageImages() const;
const std::vector<std::uint32_t>& LayoutKey() const { return layoutKey; }
const std::vector<std::uint32_t>& FragmentLayoutKey() const { return fragmentLayoutKey; }
// Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero.
std::string Describe() const;
@ -428,6 +441,17 @@ private:
VkDescriptorPool pool = VK_NULL_HANDLE;
// The cache pool the set was allocated from, freed back to it on release.
DescriptorCache::SetAllocation cacheAllocation;
struct SetObjects {
VkDescriptorSetLayout layout = VK_NULL_HANDLE;
bool ownsLayout = false;
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
DescriptorCache::SetAllocation cacheAllocation;
};
bool splitSets = false;
std::vector<std::uint32_t> fragmentLayoutKey;
SetObjects fragment;
std::uint32_t setOf(const Binding& binding) const;
std::vector<Allocation> allocations;
std::vector<VkDescriptorBufferInfo> descriptorBuffers;
std::vector<VkDescriptorImageInfo> descriptorImages;
@ -479,6 +503,7 @@ private:
// 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<VkDescriptorBindingFlags> fragmentBindingFlags;
std::vector<std::uint32_t> refreshResourceKey;
struct DeferredImages {
const ShaderRecompiler::DescriptorBinding* binding;

View File

@ -1764,7 +1764,9 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK");
if (drawBindings != nullptr) {
const auto set = drawBindings->allocation.set;
context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr);
const auto fragmentSet = drawBindings->fragmentAllocation.set;
if (set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), 0, 1, &set, 0, nullptr);
if (fragmentSet != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout(), ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragmentSet, 0, nullptr);
} else {
resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout());
}
@ -2116,6 +2118,12 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
masked->blends.at(state.colors[index].exportIndex).colorWriteMask &= VK_COLOR_COMPONENT_R_BIT;
}
const State& pipelineState = masked.has_value() ? *masked : state;
static const bool verifySetLayouts = std::getenv("APS5_VERIFY_SET_LAYOUTS") != nullptr;
if (verifySetLayouts && context.descriptorCache != nullptr) {
const auto described = DescribeSetLayouts(context, shaders);
const auto built = resources->SetLayouts();
Require(described.layouts.main == built.main && described.layouts.fragment == built.fragment && described.layouts.split == built.split && std::ranges::equal(described.mainKey, built.mainKey) && std::ranges::equal(described.fragmentKey, built.fragmentKey), "set layouts described from the shaders differ from the draw's");
}
auto pipeline = CachedPipeline(context, pipelineState, inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
// Resident targets' views are stable while their storage image lives, so the framebuffer is
// reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own.

View File

@ -152,6 +152,9 @@ bool ShadersWriteMemory(std::span<const CompiledShader> shaders) {
namespace {
thread_local bool prebuilding = false;
thread_local bool prebuilt = false;
VkShaderStageFlags pushStagesOf(const Context& context) {
return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader);
}
@ -163,7 +166,7 @@ void appendKey(std::vector<std::byte>& key, const TValue& value) {
key.insert(key.end(), bytes.begin(), bytes.end());
}
bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span<const CompiledShader> shaders) {
bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders) {
using Stage = ShaderRecompiler::ShaderStage;
if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false;
keys = {};
@ -217,14 +220,36 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State&
}
appendKey(keys.renderPass, state.depth.has_value());
if (state.depth) appendKey(keys.renderPass, state.depth->format);
appendKey(keys.layout, pushStagesOf(context));
for (const auto word : resources.LayoutKey()) appendKey(keys.layout, word);
if (!resources.split) {
appendKey(keys.layout, pushStagesOf(context));
for (const auto word : resources.mainKey) appendKey(keys.layout, word);
keys.preRasterizationLayout = keys.fragmentLayout = keys.layout;
return true;
}
const auto splitKey = [&](std::vector<std::byte>& key, std::uint32_t sets) {
appendKey(key, ~0u);
appendKey(key, sets);
appendKey(key, pushStagesOf(context));
if ((sets & 1u) != 0u) {
appendKey(key, resources.mainKey.size());
for (const auto word : resources.mainKey) appendKey(key, word);
}
if ((sets & 2u) != 0u) {
appendKey(key, resources.fragmentKey.size());
for (const auto word : resources.fragmentKey) appendKey(key, word);
}
};
splitKey(keys.layout, 3u);
splitKey(keys.preRasterizationLayout, 1u);
splitKey(keys.fragmentLayout, 2u);
return true;
}
}
Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) {
Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) : Pipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout) {}
Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) {
PerformanceTimer timing("Vulkan.GraphicsPipeline");
sampleLocationsEnabled = ValidateSampleLocations(context, state);
// A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep
@ -280,12 +305,13 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
}
// A descriptor set layout with the same bindings as this one is compatible with the pipeline
// layout, so later draws bind their own ShaderResources' set under it.
const auto setLayout = resources.Layout();
const std::array<VkDescriptorSetLayout, 2> setLayouts{resources.main, resources.fragment};
const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes};
const std::array<VkPushConstantRange, 2> slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}};
VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
layoutInfo.setLayoutCount = 1;
layoutInfo.pSetLayouts = &setLayout;
layoutInfo.flags = resources.split ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0;
layoutInfo.setLayoutCount = resources.split ? 2u : 1u;
layoutInfo.pSetLayouts = setLayouts.data();
layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast<std::uint32_t>(slots.size()) : pushStages != 0 ? 1 : 0;
layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &layout), "vkCreatePipelineLayout graphics");
@ -446,7 +472,19 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
pipelineInfo.renderPass = renderPass;
if (dynamicRendering) pipelineInfo.pNext = &rendering;
timing.Mark("modules_and_state");
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, _modules);
if (prebuilding && !libraries) {
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
_modules.clear();
return;
}
if (prebuilding) {
static_cast<void>(LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized, true));
for (const auto module : _modules) context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule")(context.device, module, nullptr);
_modules.clear();
prebuilt = true;
return;
}
if (libraries) pipeline = LinkPipelineFromLibraries(context, pipelineInfo, rendering, layoutInfo, keys, &optimized);
else Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
if (alternateAlpha) {
blend.pAttachments = secondChannels.data();
@ -466,6 +504,15 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
}
}
bool Pipeline::Prebuild(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders) {
struct Scope {
Scope() { prebuilding = true; prebuilt = false; }
~Scope() { prebuilding = false; }
} scope;
const Pipeline pipeline(context, state, vertexInput, resources, shaders);
return prebuilt;
}
Pipeline::~Pipeline() {
release();
}
@ -675,7 +722,7 @@ void append(std::vector<std::byte>& key, const TValue& value) {
// (the recompiler could not identify it, so nothing else may share its pipeline). The rect-list
// control and evaluation stages are generated from the vertex and fragment results, which the key
// already names, so they carry no id of their own.
void pipelineKey(std::vector<std::byte>& key, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) {
void pipelineKey(std::vector<std::byte>& key, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) {
using Stage = ShaderRecompiler::ShaderStage;
key.clear();
append(key, context.device);
@ -707,8 +754,10 @@ void pipelineKey(std::vector<std::byte>& key, const Context& context, const Stat
append(key, attribute.format);
append(key, attribute.offset);
}
append(key, resources.LayoutKey().size());
for (const auto word : resources.LayoutKey()) append(key, word);
append(key, resources.mainKey.size());
for (const auto word : resources.mainKey) append(key, word);
append(key, resources.fragmentKey.size());
for (const auto word : resources.fragmentKey) append(key, word);
append(key, state.hasColorTarget);
append(key, state.rectList);
append(key, state.topology);
@ -828,6 +877,10 @@ void reportPipelines(PipelineStore& store) {
}
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) {
return CachedPipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout);
}
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) {
static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr;
if (disabled) return std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders, attachmentLayout);
auto& store = Pipelines();

View File

@ -8,6 +8,7 @@
#include <thread>
#include <map>
#include <mutex>
#include <optional>
#include <string_view>
namespace AgcDriver::Graphics {
@ -37,10 +38,8 @@ using Key = std::vector<std::byte>;
struct DeviceLibraries {
PFN_vkDestroyPipeline destroyPipeline = nullptr;
PFN_vkDestroyPipelineLayout destroyLayout = nullptr;
PFN_vkDestroyShaderModule destroyModule = nullptr;
std::map<Key, VkPipelineLayout> layouts;
std::array<std::map<Key, VkPipeline>, 4> parts;
std::vector<VkShaderModule> modules;
PipelineLibraryCounters counters;
};
@ -101,22 +100,31 @@ std::span<const VkDynamicState> PipelineLibraryDynamicStates() {
return dynamicStates;
}
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized, std::span<VkShaderModule> modules) {
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized, bool partsOnly) {
Require(info.renderPass == VK_NULL_HANDLE && info.pDynamicState != nullptr, "pipeline library parts need dynamic rendering and the library dynamic states");
auto& store = Libraries();
std::lock_guard lock(store.mutex);
std::unique_lock lock(store.mutex);
auto& device = store.devices[context.device];
if (device.destroyPipeline == nullptr) {
device.destroyPipeline = context.Function<PFN_vkDestroyPipeline>("vkDestroyPipeline");
device.destroyLayout = context.Function<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout");
device.destroyModule = context.Function<PFN_vkDestroyShaderModule>("vkDestroyShaderModule");
}
auto pipelineLayout = device.layouts.find(keys.layout);
if (pipelineLayout == device.layouts.end()) {
const auto layoutFor = [&](const Key& key, std::optional<std::uint32_t> set) {
auto found = device.layouts.find(key);
if (found != device.layouts.end()) return found->second;
auto info = layout;
std::array<VkDescriptorSetLayout, 2> sets{VK_NULL_HANDLE, VK_NULL_HANDLE};
if (set) {
sets.at(*set) = layout.pSetLayouts[*set];
info.pSetLayouts = sets.data();
}
VkPipelineLayout handle = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layout, nullptr, &handle), "vkCreatePipelineLayout library");
pipelineLayout = device.layouts.emplace(keys.layout, handle).first;
}
Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &info, nullptr, &handle), "vkCreatePipelineLayout library");
return device.layouts.emplace(key, handle).first->second;
};
const bool split = (layout.flags & VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT) != 0;
const auto fullLayout = layoutFor(keys.layout, std::nullopt);
const std::array<VkPipelineLayout, 4> partLayouts{VK_NULL_HANDLE, split ? layoutFor(keys.preRasterizationLayout, 0u) : fullLayout, split ? layoutFor(keys.fragmentLayout, 1u) : fullLayout, VK_NULL_HANDLE};
std::vector<VkPipelineShaderStageCreateInfo> preRasterization;
const VkPipelineShaderStageCreateInfo* fragment = nullptr;
bool mesh = false;
@ -135,7 +143,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
};
const std::array<const Key*, 4> partKeys{&keys.vertexInput, &keys.preRasterization, &keys.fragmentShader, &keys.fragmentOutput};
const Key none;
const std::array<const Key*, 4> contextKeys{&none, &keys.layout, &keys.layout, &keys.renderPass};
const std::array<const Key*, 4> contextKeys{&none, &keys.preRasterizationLayout, &keys.fragmentLayout, &keys.renderPass};
const VkPipelineRenderingCreateInfoKHR shaderRendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR};
VkPipelineDepthStencilStateCreateInfo dynamicDepth{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
static const bool backgroundLinking = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr;
@ -155,7 +163,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
VkGraphicsPipelineCreateInfo create{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, &library};
create.flags = VK_PIPELINE_CREATE_LIBRARY_BIT_KHR | VK_PIPELINE_CREATE_RETAIN_LINK_TIME_OPTIMIZATION_INFO_BIT_EXT;
create.pDynamicState = info.pDynamicState;
create.layout = pipelineLayout->second;
create.layout = partLayouts[part];
switch (flags[part]) {
case VK_GRAPHICS_PIPELINE_LIBRARY_VERTEX_INPUT_INTERFACE_BIT_EXT:
create.pVertexInputState = info.pVertexInputState;
@ -182,20 +190,20 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
break;
}
VkPipeline handle = VK_NULL_HANDLE;
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle), "vkCreateGraphicsPipelines library");
cache.emplace(key, handle);
if (optimize) {
for (std::uint32_t stage = 0; stage < create.stageCount; ++stage) {
for (auto& module : modules) {
if (module == VK_NULL_HANDLE || module != create.pStages[stage].module) continue;
device.modules.push_back(module);
module = VK_NULL_HANDLE;
}
}
if (partsOnly) lock.unlock();
const auto created = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &create, nullptr, &handle);
if (partsOnly) lock.lock();
Check(created, "vkCreateGraphicsPipelines library");
if (const auto raced = cache.find(key); raced != cache.end()) {
device.destroyPipeline(context.device, handle, nullptr);
libraries.push_back(raced->second);
continue;
}
cache.emplace(key, handle);
++device.counters.built[part];
libraries.push_back(handle);
}
if (partsOnly) return VK_NULL_HANDLE;
PerformanceTimer timing("Vulkan.GraphicsPipelineLink");
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
linked.libraryCount = static_cast<std::uint32_t>(libraries.size());
@ -212,7 +220,7 @@ VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPip
*optimized = slot;
const auto createPipelines = context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines");
const auto cache = context.pipelineCache;
const auto libraryLayout = pipelineLayout->second;
const auto libraryLayout = fullLayout;
enqueue(store, context.device, [slot, createPipelines, cache, libraryLayout, libraries, &store, devicePointer = &device] {
if (slot->released.load()) return;
VkPipelineLibraryCreateInfoKHR linked{VK_STRUCTURE_TYPE_PIPELINE_LIBRARY_CREATE_INFO_KHR};
@ -255,7 +263,6 @@ void ClearPipelineLibraries(VkDevice device) {
for (auto& part : libraries.parts) {
for (const auto& [key, handle] : part) libraries.destroyPipeline(device, handle, nullptr);
}
for (const auto module : libraries.modules) libraries.destroyModule(device, module, nullptr);
for (const auto& [key, handle] : libraries.layouts) libraries.destroyLayout(device, handle, nullptr);
store.devices.erase(found);
}

View File

@ -7,6 +7,7 @@
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include <algorithm>
#include <atomic>
@ -1166,6 +1167,14 @@ VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span<const Vk
return layout;
}
std::vector<VkDescriptorPoolSize> PoolSizes(std::span<const VkDescriptorSetLayoutBinding> bindings) {
std::map<VkDescriptorType, std::uint32_t> counts;
for (const auto& binding : bindings) counts[binding.descriptorType] += binding.descriptorCount;
std::vector<VkDescriptorPoolSize> sizes;
for (const auto& [type, count] : counts) sizes.push_back({type, count});
return sizes;
}
VkDescriptorPool CreateSetPool(const Context& context, std::span<const VkDescriptorPoolSize> sizes, bool updateAfterBind) {
VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
info.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0;
@ -1339,7 +1348,7 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
updateAfterBind = true;
};
for (const auto& binding : shader.program->bindings) {
Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero");
Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shader.stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "unexpected descriptor set: a shader resource must use its stage's descriptor set");
Require(occupied.insert(binding.binding).second, "duplicate shader binding");
const bool addressRole = binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer;
const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt || binding.role == ShaderRecompiler::DescriptorRole::Gds;
@ -1416,23 +1425,62 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
if (drawBuild && !usesBda && std::all_of(allocations.begin(), allocations.end(), [&](const Allocation& allocation) { return !allocation.guest || allocation.pushByte >= 0 || (allocation.dataAllocation >= 0 && allocation.dataByte < allocations[static_cast<std::size_t>(allocation.dataAllocation)].size); })) guestMemory.AllowAdjustedRegions();
guestMemory.UploadPrepare(usesBda);
timing.uploadMs = phase(BuildPhase::Upload);
std::vector<VkDescriptorSetLayoutBinding> description;
splitSets = SplitsSets(context, shaders);
std::array<std::vector<VkDescriptorSetLayoutBinding>, 2> descriptions;
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});
const auto set = setOf(binding);
descriptions[set].push_back(binding.layout);
(set == 0 ? layoutBindingFlags : fragmentBindingFlags).push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0);
auto& key = set == 0 ? layoutKey : fragmentLayoutKey;
key.insert(key.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);
if (splitSets) fragmentLayoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
}
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, layoutBindingFlags);
} else {
const auto createLayout = [&](const std::vector<std::uint32_t>& key, const std::vector<VkDescriptorSetLayoutBinding>& description, const std::vector<VkDescriptorBindingFlags>& flags, bool& owns) {
if (context.descriptorCache != nullptr && !noLayoutCache) return context.descriptorCache->Layout(key, description, updateAfterBind, flags);
ValidateDescriptorLayout(context, description, updateAfterBind);
_layout = CreateSetLayout(context, description, updateAfterBind, layoutBindingFlags);
ownsLayout = true;
owns = true;
return CreateSetLayout(context, description, updateAfterBind, flags);
};
_layout = createLayout(layoutKey, descriptions[0], layoutBindingFlags, ownsLayout);
if (splitSets) {
fragment.layout = createLayout(fragmentLayoutKey, descriptions[1], fragmentBindingFlags, fragment.ownsLayout);
{
const auto sizes = PoolSizes(descriptions[1]);
if (context.descriptorCache != nullptr && !noPoolCache) {
fragment.cacheAllocation = context.descriptorCache->Allocate(fragment.layout, sizes, updateAfterBind);
fragment.set = fragment.cacheAllocation.set;
}
if (fragment.set == VK_NULL_HANDLE) {
fragment.pool = CreateSetPool(context, sizes, updateAfterBind);
VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
allocation.descriptorPool = fragment.pool;
allocation.descriptorSetCount = 1;
allocation.pSetLayouts = &fragment.layout;
Check(context.Function<PFN_vkAllocateDescriptorSets>("vkAllocateDescriptorSets")(context.device, &allocation, &fragment.set), "vkAllocateDescriptorSets");
}
}
}
if (!bindings.empty()) {
if (splitSets) {
const auto sizes = PoolSizes(descriptions[0]);
if (context.descriptorCache != nullptr && !noPoolCache) {
cacheAllocation = context.descriptorCache->Allocate(_layout, sizes, updateAfterBind);
_set = cacheAllocation.set;
}
if (_set == VK_NULL_HANDLE) {
pool = CreateSetPool(context, sizes, updateAfterBind);
VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
allocation.descriptorPool = pool;
allocation.descriptorSetCount = 1;
allocation.pSetLayouts = &_layout;
Check(context.Function<PFN_vkAllocateDescriptorSets>("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets");
}
}
if (!splitSets && !bindings.empty()) {
// The set is sized from the plan: every image element becomes exactly one descriptor
// when stage B looks it up.
std::vector<VkDescriptorPoolSize> sizes;
@ -1465,6 +1513,48 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
}
}
bool SplitsSets(const Context& context, std::span<const CompiledShader> shaders) {
const auto fragmentSet = [](const CompiledShader& shader) { return shader.program != nullptr && std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const auto& binding) { return binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet; }); };
const auto fragmentStage = [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Fragment; };
return std::any_of(shaders.begin(), shaders.end(), fragmentSet) || (context.graphicsPipelineLibrary && std::any_of(shaders.begin(), shaders.end(), fragmentStage));
}
DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span<const CompiledShader> shaders) {
Require(context.descriptorCache != nullptr, "described set layouts need the descriptor cache");
DescribedSetLayouts result;
std::array<std::vector<VkDescriptorSetLayoutBinding>, 2> descriptions;
const bool split = SplitsSets(context, shaders);
bool updateAfterBind = false;
std::uint64_t storageBuffers = 0;
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
const VkShaderStageFlags flags = VulkanStage(shader.stage);
std::uint64_t stageStorageBuffers = 0;
std::uint64_t stageResources = 0;
for (const auto& binding : shader.program->bindings) {
const bool image = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers;
const auto type = !image ? VK_DESCRIPTOR_TYPE_STORAGE_BUFFER : binding.kind == ShaderRecompiler::DescriptorKind::StorageImage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE : binding.kind == ShaderRecompiler::DescriptorKind::SampledImage ? VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE : VK_DESCRIPTOR_TYPE_SAMPLER;
if (type != VK_DESCRIPTOR_TYPE_SAMPLER) stageResources += binding.count;
if (type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) {
stageStorageBuffers += binding.count;
storageBuffers += binding.count;
}
if (stageStorageBuffers > context.limits.maxPerStageDescriptorStorageBuffers || stageResources > context.limits.maxPerStageResources) updateAfterBind = true;
descriptions[binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet ? 1 : 0].push_back({binding.binding, type, binding.count, flags, nullptr});
}
}
if (storageBuffers > context.limits.maxDescriptorSetStorageBuffers) updateAfterBind = true;
for (std::size_t set = 0; set < descriptions.size(); ++set) {
auto& key = set == 0 ? result.mainKey : result.fragmentKey;
for (const auto& binding : descriptions[set]) key.insert(key.end(), {binding.binding, static_cast<std::uint32_t>(binding.descriptorType), binding.descriptorCount, binding.stageFlags});
if (updateAfterBind && (set == 0 || split)) key.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
}
result.layouts.split = split;
result.layouts.main = context.descriptorCache->Layout(result.mainKey, descriptions[0], updateAfterBind);
if (split) result.layouts.fragment = context.descriptorCache->Layout(result.fragmentKey, descriptions[1], updateAfterBind);
return result;
}
void ShaderResources::buildComplete() {
PerformanceTimer frameTiming("Resources.Complete");
Require(!completed, "cannot update a published descriptor heap");
@ -1492,7 +1582,7 @@ void ShaderResources::buildComplete() {
if (usesBda) bda = std::make_unique<BdaResources>(context, guestMemory);
else if (usesFaultBuffer) bda = std::make_unique<BdaResources>(context);
phase(BuildPhase::Bda);
if (_set != VK_NULL_HANDLE) {
if (_set != VK_NULL_HANDLE || fragment.set != VK_NULL_HANDLE) {
// One update call for the whole set: the info arrays are sized up front so every write's
// pointer into them stays valid until the call.
std::size_t bufferCount = 0;
@ -1514,7 +1604,7 @@ void ShaderResources::buildComplete() {
};
for (const auto& binding : bindings) {
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = _set;
write.dstSet = setOf(binding) == 0 ? _set : fragment.set;
write.dstBinding = binding.layout.binding;
write.descriptorCount = binding.layout.descriptorCount;
write.descriptorType = binding.layout.descriptorType;
@ -3334,10 +3424,19 @@ void ShaderResources::release() noexcept {
if (cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(cacheAllocation);
if (pool) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, pool, nullptr);
if (_layout && ownsLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, _layout, nullptr);
if (fragment.cacheAllocation.pool && context.descriptorCache != nullptr) context.descriptorCache->Free(fragment.cacheAllocation);
if (fragment.pool) context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")(context.device, fragment.pool, nullptr);
if (fragment.layout && fragment.ownsLayout) context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")(context.device, fragment.layout, nullptr);
cacheAllocation = {};
pool = VK_NULL_HANDLE;
_set = VK_NULL_HANDLE;
_layout = VK_NULL_HANDLE;
fragment = {};
}
std::uint32_t ShaderResources::setOf(const Binding& binding) const {
constexpr auto count = static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Count);
return splitSets && binding.layout.binding / count == static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Stage::Fragment) ? 1u : 0u;
}
VkDescriptorSetLayout ShaderResources::Layout() const {
@ -3356,11 +3455,12 @@ bool ShaderResources::LayoutUpdateAfterBind() const {
ShaderResources::DrawBindings::~DrawBindings() {
if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation);
if (cache != nullptr && fragmentAllocation.set != VK_NULL_HANDLE) cache->Free(fragmentAllocation);
}
std::optional<std::vector<ShaderResources::MovedBuffer>> ShaderResources::MovedReadOnlyBuffers(std::span<const CompiledShader> shaders, Recorder& recorder) const {
std::vector<MovedBuffer> moved;
if (_set == VK_NULL_HANDLE || usesBda) return moved;
if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return moved;
for (const auto& shader : shaders) {
if (shader.program == nullptr) return std::nullopt;
for (const auto& binding : shader.program->bindings) {
@ -3413,7 +3513,7 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() {
}
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved) const {
if (_set == VK_NULL_HANDLE || usesBda) return {};
if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {};
const auto reads = guestMemory.InPlaceReads();
const auto maxSnapshotBytes = MaxDrawSnapshotBytes();
std::size_t totalSnapshotBytes = 0;
@ -3460,13 +3560,17 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
}
if (selected.empty()) return {};
Require(context.descriptorCache != nullptr, "draw snapshots require a descriptor cache");
std::map<VkDescriptorType, std::uint32_t> counts;
for (const auto& binding : bindings) counts[binding.layout.descriptorType] += binding.layout.descriptorCount;
std::vector<VkDescriptorPoolSize> sizes;
for (const auto& [type, count] : counts) sizes.push_back({type, count});
std::array<std::vector<VkDescriptorSetLayoutBinding>, 2> descriptions;
for (const auto& binding : bindings) descriptions[setOf(binding)].push_back(binding.layout);
result->cache = context.descriptorCache;
result->allocation = result->cache->Allocate(_layout, sizes);
Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed");
if (!descriptions[0].empty() || splitSets) {
result->allocation = result->cache->Allocate(_layout, PoolSizes(descriptions[0]));
Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed");
}
if (!descriptions[1].empty() || splitSets) {
result->fragmentAllocation = result->cache->Allocate(fragment.layout, PoolSizes(descriptions[1]));
Require(result->fragmentAllocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed");
}
auto buffers = descriptorBuffers;
auto writes = descriptorWrites;
std::vector<std::size_t> snapshotIndices(allocations.size(), selected.size());
@ -3474,7 +3578,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
for (std::size_t index = 0; index < bindings.size(); ++index) {
const auto& binding = bindings[index];
auto& write = writes[index];
write.dstSet = result->allocation.set;
write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set;
if (binding.allocations.empty()) continue;
const auto offset = static_cast<std::size_t>(write.pBufferInfo - descriptorBuffers.data());
write.pBufferInfo = buffers.data() + offset;
@ -3492,8 +3596,8 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
}
void ShaderResources::Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const {
if (_set == VK_NULL_HANDLE) return;
context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr);
if (_set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, 0, 1, &_set, 0, nullptr);
if (fragment.set != VK_NULL_HANDLE) context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, bindPoint, layout, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 1, &fragment.set, 0, nullptr);
}
namespace {

View File

@ -583,7 +583,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex;
Require(shaders[i].program != nullptr, "missing compiled shader");
Require(shaders[i].stage == expected, "graphics stage order disagrees");
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero");
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's");
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance);
if (i != 0) {
for (const auto& [location, signature] : current.inputs) {

View File

@ -19,8 +19,8 @@ void Check(bool condition, const std::string& message) {
}
SpirvTarget Target() {
static constexpr std::array<std::uint32_t, 3> capabilities{
spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess,
static constexpr std::array<std::uint32_t, 4> capabilities{
spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance,
};
static constexpr std::array<std::string_view, 4> extensions{
"SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer",
@ -35,6 +35,7 @@ struct Counts {
std::size_t calls = 0;
std::size_t functions = 0;
std::size_t notInlined = 0;
std::size_t compareExchanges = 0;
};
Counts Count(std::span<const std::uint32_t> words) {
@ -46,6 +47,7 @@ Counts Count(std::span<const std::uint32_t> words) {
const auto opcode = words[offset] & spv::OpCodeMask;
if (opcode == spv::OpSwitch) ++counts.switches;
if (opcode == spv::OpFunctionCall) ++counts.calls;
if (opcode == spv::OpAtomicCompareExchange) ++counts.compareExchanges;
if (opcode == spv::OpFunction) {
++counts.functions;
if (count > 3 && (words[offset + 3] & spv::FunctionControlDontInlineMask) != 0) ++counts.notInlined;
@ -55,24 +57,27 @@ Counts Count(std::span<const std::uint32_t> words) {
return counts;
}
Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false) {
Counts Compile(std::size_t loads, bool formattedStores = false, bool gpu = false, bool vertex = false, bool registration = false) {
std::vector<std::uint32_t> code;
if (gpu) code.push_back(0x7e160500u);
for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {0xe0000000u | (load * 4u), 0x80020000u | ((10u + load) << 8u)});
code.push_back(0xbf8c3f70u);
for (std::uint32_t load = 0; load < loads; ++load) code.insert(code.end(), {(formattedStores ? 0xe0100000u : 0xe0700000u) | (load * 4u), 0x80010000u | ((10u + load) << 8u)});
if (vertex) code.insert(code.end(), {0xf80008cfu, 0x03020100u});
code.push_back(0xbf810000u);
const std::span<const std::uint32_t> words(code);
const std::array<MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(words)}}};
const std::array<std::uint32_t, 12> userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu, 0x20000000u, 0u, 0x1000u, 0x31038facu};
RecompileRequest request{};
request.shader = {ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), words, 0, {}};
request.shader = {vertex ? ShaderStage::Vertex : ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), words, 0, {}};
request.target = Target();
request.target.registrationModules = registration;
request.layout = {0, 0, 0, 128};
request.context.waveSize = 32;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
if (vertex) request.context.vertex = ShaderVertexStageInfo{};
else request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.context.memory = memory;
request.useCache = false;
return Count(Recompile(request).spirv.Words());
@ -85,6 +90,12 @@ int main() {
const auto none = Compile(0);
const auto one = Compile(1);
const auto three = Compile(3);
const auto gpuOne = Compile(1, false, true);
const auto vertexSpecialized = Compile(1, false, false, true, false);
const auto vertexRegistration = Compile(1, false, false, true, true);
Check(vertexRegistration.compareExchanges == vertexSpecialized.compareExchanges, "formatted buffer once: in registration form a store through an aligned V# kept the unaligned compare-exchange path (" + std::to_string(vertexRegistration.compareExchanges) + " compare-exchanges, " + std::to_string(vertexSpecialized.compareExchanges) + " specialized)");
Check(vertexRegistration.words < vertexSpecialized.words + vertexSpecialized.words / 2u, "formatted buffer once: in registration form a formatted load from a known descriptor kept every format's decoder (" + std::to_string(vertexRegistration.words) + " words, " + std::to_string(vertexSpecialized.words) + " specialized)");
Check(one.words < gpuOne.words / 2u, "formatted buffer once: a formatted load from a known descriptor kept every format's decoder (" + std::to_string(one.words) + " words, a GPU-selected descriptor needs " + std::to_string(gpuOne.words) + ")");
Check(one.calls == none.calls + 1 && one.functions == none.functions + 1, "formatted buffer once: one formatted load emitted " + std::to_string(one.calls - none.calls) + " calls and " + std::to_string(one.functions - none.functions) + " functions more than none, expected one call to one shared function");
Check(three.calls == one.calls + 2 && three.functions == one.functions, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.calls - one.calls) + " calls and " + std::to_string(three.functions - one.functions) + " functions more, expected two calls and no function");
Check(three.switches == one.switches, "formatted buffer once: two more formatted loads from the same descriptor emitted " + std::to_string(three.switches - one.switches) + " more format switches");
@ -94,12 +105,11 @@ int main() {
Check(storeOne.functions == none.functions + 2 && storeOne.calls == none.calls + 2, "formatted buffer once: a formatted load and a formatted store to another resource emitted " + std::to_string(storeOne.functions - none.functions) + " functions and " + std::to_string(storeOne.calls - none.calls) + " calls more than none, expected one function and one call each");
Check(storeThree.functions == storeOne.functions && storeThree.calls == storeOne.calls + 4 && storeThree.switches == storeOne.switches, "formatted buffer once: two more formatted loads and stores emitted " + std::to_string(storeThree.functions - storeOne.functions) + " functions, " + std::to_string(storeThree.calls - storeOne.calls) + " calls and " + std::to_string(storeThree.switches - storeOne.switches) + " switches more, expected four calls only");
Check(three.words - one.words < one.words / 10u, "formatted buffer once: 2 more formatted loads added " + std::to_string(three.words - one.words) + " SPIR-V words to a " + std::to_string(one.words) + "-word module");
const auto gpuOne = Compile(1, false, true);
const auto gpuThree = Compile(3, false, true);
Check(gpuThree.functions == gpuOne.functions && gpuThree.calls > gpuOne.calls, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.functions - gpuOne.functions) + " more functions, expected calls to the shared one");
Check(gpuThree.switches == gpuOne.switches, "formatted buffer once: two more formatted loads from a GPU-selected descriptor emitted " + std::to_string(gpuThree.switches - gpuOne.switches) + " more format switches");
Check(gpuThree.words - gpuOne.words < gpuOne.words / 10u, "formatted buffer once: 2 more formatted loads from a GPU-selected descriptor added " + std::to_string(gpuThree.words - gpuOne.words) + " SPIR-V words to a " + std::to_string(gpuOne.words) + "-word module");
std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu)\n", one.words, three.words, gpuOne.words, gpuThree.words);
std::printf("formatted buffer once tests passed (%zu words for 1 load, %zu for 3; GPU-selected %zu and %zu; vertex %zu specialized, %zu registration)\n", one.words, three.words, gpuOne.words, gpuThree.words, vertexSpecialized.words, vertexRegistration.words);
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());

View File

@ -4580,7 +4580,7 @@ void validationTests() {
{
auto vertex = pushed({.push = true, .bufferArray = 2});
vertex.bindings.front().descriptorSet = 1;
expectFailure([&] { validate(vertex, 8); }, "descriptor set other than zero");
expectFailure([&] { validate(vertex, 8); }, "descriptor set other than its stage's");
}
{
auto vertex = pushed({.push = true, .bufferArray = 2});

View File

@ -184,13 +184,13 @@ void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
{64, 0, {}, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
{0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush},
target.fixedPushSlots ? PixelPushLayout(meshPush, target) : ShaderRecompiler::BindingLayout{0, 0, meshPush, ShaderRecompiler::MeshDrawPushOffsetBytes - meshPush},
std::nullopt
};
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Mesh, &meshResult, 0},
{ShaderStage::Fragment, &pixelResult, meshPush}
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(meshPush, target)}
}};
AgcDriver::Graphics::State state{};

View File

@ -71,6 +71,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade
require(left.hostSubgroupSize == right.hostSubgroupSize, prefix + "host subgroup size differs");
require(left.vertexInputs == right.vertexInputs, prefix + "vertex inputs differ");
require(left.vertexInputPatches == right.vertexInputPatches, prefix + "vertex type patches differ");
require(left.registrationModule == right.registrationModule, prefix + "registration module flags differ");
require(left.vertexOffsetSgpr == right.vertexOffsetSgpr && left.instanceOffsetSgpr == right.instanceOffsetSgpr, prefix + "offset SGPRs differ");
require(left.vertexOffsetShared == right.vertexOffsetShared && left.instanceOffsetShared == right.instanceOffsetShared && left.vertexOffsetConflict == right.vertexOffsetConflict && left.instanceOffsetConflict == right.instanceOffsetConflict, prefix + "offset flags differ");
require(left.parameterExports == right.parameterExports, prefix + "parameter exports differ");
@ -153,6 +154,7 @@ RecompileResult sampleResult() {
result.workgroupMemoryDwords = 32769u;
result.vertexInputs = {{1, 4, 2}, {5, 2, 0}};
result.vertexInputPatches = {{1, 20, {7, 8, 9}}, {5, 40, {10, 11, 12}}};
result.registrationModule = true;
result.specialization = {{512, 4}, {516, 0x3f800000u}, {517, 0}};
result.vertexAttributes = {{1, 4, {{0x1000u, 0x20000u, 0x30u, 0x4u}}, 2}, {5, 2, {{9u, 8u, 7u, 6u}}, 0}};
result.vertexInputs[0].outputMask = 5u;

View File

@ -1731,7 +1731,7 @@ void verifyLegacyPixelRequests() {
require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader");
require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned");
require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned");
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve, "legacy request trailer was misread");
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules, "legacy request trailer was misread");
const auto upgraded = serializer.Deserialize(serializer.Serialize(request));
require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping");
}
@ -3224,6 +3224,16 @@ int main(int argc, char** argv) {
auto float64Denormals = uncached;
float64Denormals.target.float64DenormPreserve = true;
require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float64Denormals)).request.target.float64DenormPreserve, "f64 denormal preservation was lost in serialization");
auto registrationModules = uncached;
registrationModules.target.registrationModules = true;
registrationModules.target.registrationWordLimit = 1234u;
const auto roundTrip = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(registrationModules)).request.target;
require(roundTrip.registrationModules && roundTrip.registrationWordLimit == 1234u, "registration modules were lost in serialization");
std::vector<std::uint64_t> plainKey;
std::vector<std::uint64_t> registrationKey;
RecompileCacheKey::Build(uncached, plainKey);
RecompileCacheKey::Build(registrationModules, registrationKey);
require(plainKey != registrationKey, "registration modules did not change the cache key");
auto changedLayout = request;
changedLayout.layout.pushConstantSizeBytes = 64;
require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant");

View File

@ -0,0 +1,111 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "VulkanTestDevice.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <span>
#include <string>
#include <vector>
namespace {
using AgcDriver::Graphics::Require;
using ShaderRecompiler::ShaderStage;
constexpr std::uint32_t Lanes = 32;
constexpr std::uint32_t InputBytes = 256;
constexpr std::uint32_t Fill = 0xdeadbeefu;
alignas(256) std::array<std::uint8_t, InputBytes> Input{};
alignas(256) std::array<std::uint32_t, Lanes> Output{};
alignas(256) constexpr std::array<std::uint32_t, 7> Code{
0x34020082, 0xe0301000, 0x80000201, 0xbf8c3f70, 0xe0701000, 0x80010201, 0xbf810000,
};
struct Case {
const char* name;
std::uint32_t baseOffset;
std::uint32_t records;
bool present;
};
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
const auto address = reinterpret_cast<std::uintptr_t>(data);
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
}
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
ShaderRecompiler::RecompileRequest Request(std::span<const std::uint32_t> userData, const ShaderRecompiler::SpirvTarget& target, std::span<const ShaderRecompiler::MemoryRegion> memory) {
const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1};
return {
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(Code.data()), Code, 0, {}},
{Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory},
target,
{0, 0, 0, 128}
};
}
void Run(AgcDriver::VulkanDevice& device, bool registrationModules) {
const std::string mode = registrationModules ? "registration modules" : "specialized modules";
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(Code.data()), std::as_bytes(std::span(Code))}}};
std::array<std::uint32_t, 8> userData{};
auto target = device.ComputeTarget(Lanes);
target.registrationModules = registrationModules;
const auto request = Request(userData, target, memory);
const auto handle = ShaderRecompiler::PrepareShader(request);
const std::array<Case, 7> cases{{
{"aligned", 0, 128, true},
{"base +1", 1, 128, true},
{"base +2", 2, 128, true},
{"base +3", 3, 128, true},
{"records 37", 0, 37, true},
{"records 64 base +2", 2, 64, true},
{"absent", 0, 128, false},
}};
for (const auto& test : cases) {
Output.fill(Fill);
const auto input = test.present ? BufferDescriptor(Input.data() + test.baseOffset, test.records) : std::array<std::uint32_t, 4>{0u, 0u, 0u, 0x31016facu};
const auto output = BufferDescriptor(Output.data(), sizeof(Output));
std::copy(input.begin(), input.end(), userData.begin());
std::copy(output.begin(), output.end(), userData.begin() + 4);
ShaderRecompiler::SrtRuntime runtime{};
runtime.userData = userData;
const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle);
const auto result = ShaderRecompiler::MaterializeShader(request, *capture, *handle);
Require(!result->registrationModule, "buffer descriptor runtime, " + mode + ", " + test.name + ": a compute shader was emitted in registration form");
device.Dispatch(*result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
device.WaitIdle();
for (std::uint32_t lane = 0; lane < Lanes; ++lane) {
std::uint32_t expected = 0;
if (test.present && lane * 4u + 4u <= test.records) std::memcpy(&expected, Input.data() + test.baseOffset + lane * 4u, sizeof(expected));
Require(Output[lane] == expected, "buffer descriptor runtime, " + mode + ", " + test.name + ": lane " + std::to_string(lane) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected));
}
}
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
for (std::uint32_t index = 0; index < InputBytes; ++index) Input[index] = static_cast<std::uint8_t>(index * 41u + 3u + (index >> 3u));
Run(*device, false);
Run(*device, true);
std::puts("buffer descriptor runtime tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "Recompiler.hpp"
#include "VulkanTestDevice.hpp"
#include <algorithm>
#include <array>
#include <cstddef>
@ -96,7 +97,7 @@ inline Shaders Compile(AgcDriver::VulkanDevice& device) {
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
{32, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
{0, 0, shaders.vertexPush, 128 - shaders.vertexPush}
PixelPushLayout(shaders.vertexPush, target)
};
fragment.useCache = false;
shaders.pixel = ShaderRecompiler::Recompile(fragment);
@ -107,7 +108,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil
Pixels.fill(std::byte{0});
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &compiled.vertex, 0},
{ShaderStage::Fragment, &compiled.pixel, compiled.vertexPush}
{ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())}
}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt};

View File

@ -29,7 +29,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) {
const std::array<std::uint32_t, 4> values = integer ? std::array<std::uint32_t, 4>{11, 22, 33, 44} : std::array<std::uint32_t, 4>{0, 0x3e800000u, 0x3f000000u, 0x3f800000u};
const std::array<std::uint32_t, 13> code{0x7e0002ffu, values[0], 0x7e0202ffu, values[1], 0x7e0402ffu, values[2], 0x7e0602ffu, values[3], 0xf800100fu, 0x03020100u, 0xf800183fu, 0x03020100u, 0xbf810000u};
AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, integer ? 0x7007u : 0x9009u}};
RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), {0, 0, 0, 128}};
RecompileRequest request{{ShaderStage::Fragment, 0x20000, code, 0, {}}, {32, 0, {}, {}, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), {}, {}}, device.Target(), PixelPushLayout(0u, device.Target())};
const auto handle = PrepareShader(request);
const auto& artifact = GetPreparedArtifact(*handle);
std::uint32_t scalarType = 0;
@ -85,6 +85,10 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) {
request.useCache = cached;
const auto materialized = MaterializeShader(request, *capture, *handle);
Require(materialized->spirv.data() == pixel->spirv.data() && materialized->PipelineVariantId() == pixel->PipelineVariantId(), "export mapping did not reuse its specialized module");
if (device.Target().registrationModules) {
Require(materialized->PipelineVariantId() == first->PipelineVariantId(), "registration modules: the export mapping specialized the fragment module");
continue;
}
for (std::size_t cursor = 5; cursor < materialized->spirv.size();) {
const auto count = materialized->spirv[cursor] >> 16u;
Require(count != 0u && count <= materialized->spirv.size() - cursor, "invalid specialized fragment instruction");
@ -94,7 +98,7 @@ void Run(AgcDriver::VulkanDevice& device, bool integer) {
}
request.useCache = true;
if (integer) continue;
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), 0}}};
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::Fragment, pixel.get(), PixelPushOffset(0u, device.Target())}}};
pixels.fill(0xdeadbeef);
otherPixels.fill(0xdeadbeef);
device.Draw(state, draw, shaders);

View File

@ -1,3 +1,4 @@
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
#include "VulkanTestDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp"
#include "SceShaders.hpp"
@ -130,6 +131,30 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path
}
const auto stages = PrepareGraphicsStages(prepared, target);
Require(stages.size() == (tessellation ? 4u : 2u), "graphics preparation compiled the wrong stages");
if (!mesh && !tessellation && target.registrationModules) {
const auto prebuild = [&](const std::vector<PreparedGraphicsStage>& group, std::initializer_list<std::size_t> picked) {
std::vector<AgcDriver::Graphics::CompiledShader> shaders;
for (const auto index : picked) {
Require(group.at(index).registration != nullptr, "registration modules: a prepared stage has no registration result");
shaders.push_back({group[index].stage, group[index].registration.get(), group[index].pushOffset});
}
const auto before = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device());
Require(device.PrebuildGraphicsLibraries(prepared.state.stages, shaders), "registration modules: the shader libraries were not prebuilt");
const auto after = AgcDriver::Graphics::PipelineLibraryCountersOf(device.Device());
Require(after.linked == before.linked, "registration modules: a prebuild linked a pipeline");
return std::pair{after.built[1] - before.built[1], after.built[2] - before.built[2]};
};
const auto vertexOnly = prebuild(stages, {0u});
Require(vertexOnly.first == 1u, "registration modules: the vertex stage alone did not build its pre-rasterization library");
const auto pixelOnly = prebuild(stages, {1u});
Require(pixelOnly.second == 1u, "registration modules: the pixel stage alone did not build its fragment library");
const auto pair = prebuild(stages, {0u, 1u});
Require(pair.first == 0u && pair.second == 0u, "registration modules: the pair of stages did not find the libraries prebuilt one stage at a time");
const auto again = PrepareGraphicsStages(prepared, target);
Require(again.front().registration != stages.front().registration, "registration modules: the second preparation reused the first result object");
const auto repeated = prebuild(again, {0u, 1u});
Require(repeated.first == 0u && repeated.second == 0u, "registration modules: another materialization of the same stages did not find the prebuilt libraries");
}
for (const auto& stage : stages) stage.snapshot->prepared->entries.push_back(stage.entry);
const auto& pixelArtifact = GetPreparedArtifact(*stages.back().entry.handle);
Require(!pixelArtifact.fragmentParameters.empty() && pixelArtifact.fragmentParameters.front().sourceLocation == 3u, "prepared fragment lost interpolant mapping");

View File

@ -50,7 +50,7 @@ ShaderRecompiler::RecompileResult GuestPixel(const ShaderRecompiler::SpirvTarget
request.context.waveSize = 64;
request.context.pixel = pixel;
request.target = target;
request.layout.pushConstantSizeBytes = 128;
request.layout = PixelPushLayout(0u, target);
request.useCache = false;
auto result = ShaderRecompiler::Recompile(request);
result.variantId = 3;
@ -130,7 +130,7 @@ int main() {
const auto guestPixel = GuestPixel(device->Target());
const std::array<AgcDriver::Graphics::CompiledShader, 2> guestShaders{{
{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0},
{ShaderRecompiler::ShaderStage::Fragment, &guestPixel, 0}
{ShaderRecompiler::ShaderStage::Fragment, &guestPixel, PixelPushOffset(0u, device->Target())}
}};
for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP}) Run(*device, topology, guestShaders, true, "guest P0");
std::puts("provoking vertex rendering tests passed: GLSL and guest P0, triangle list and strip, first/last/first");

View File

@ -200,13 +200,13 @@ void Draw(AgcDriver::VulkanDevice& device, GuestBuffer& pixels, GuestBuffer& ver
{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(PixelCode.data()), PixelCode, 0, {}},
{64, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory},
target,
{0, 0, vertexPush, 128 - vertexPush}
PixelPushLayout(vertexPush, target)
};
fragment.useCache = false;
const auto pixelResult = ShaderRecompiler::Recompile(fragment);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, &pixelResult, vertexPush}
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
}};
AgcDriver::Graphics::State state{};

View File

@ -53,8 +53,9 @@ void Run(AgcDriver::VulkanDevice& device) {
std::uint32_t format;
bool moved;
std::uint32_t offset;
bool specializedPixel = false;
};
constexpr std::array<Case, 4> cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}}};
constexpr std::array<Case, 5> cases{{{16u, 77u, false, 1u}, {32u, 77u, true, 0u}, {8u, 71u, false, 1u}, {16u, 71u, true, 0u}, {16u, 77u, false, 1u, true}}};
for (const auto& value : cases) {
auto& storage = value.moved ? movedVertices : vertices;
storage.fill(std::byte{0});
@ -85,9 +86,11 @@ void Run(AgcDriver::VulkanDevice& device) {
pixelInfo.wave32 = true;
pixelInfo.targetOutputMode[0] = 9u;
pixelInfo.targetExportMapping.fill(0xe4u);
ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}};
ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())};
if (value.specializedPixel) pixelRequest.target.registrationWordLimit = 1u;
const auto pixel = ShaderRecompiler::Recompile(pixelRequest);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}};
if (device.Target().registrationModules) Require(vertex.registrationModule && pixel.registrationModule != value.specializedPixel, "registration modules: the word limit did not choose the module mode");
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u};

View File

@ -53,9 +53,9 @@ Counts RunCase(AgcDriver::VulkanDevice& device, const char* name, std::uint32_t
pixelInfo.wave32 = true;
pixelInfo.targetOutputMode[0] = 9u;
pixelInfo.targetExportMapping.fill(0xe4u);
ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), {0u, 0u, pushBytes, 128u - pushBytes}};
ShaderRecompiler::RecompileRequest pixelRequest{{ShaderStage::Fragment, reinterpret_cast<std::uintptr_t>(pixelCode.data()), pixelCode, 0, {}}, {32u, 0u, {}, std::nullopt, pixelInfo, std::nullopt, {}}, device.Target(), PixelPushLayout(pushBytes, device.Target())};
const auto pixel = ShaderRecompiler::Recompile(pixelRequest);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, pushBytes}}};
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(pushBytes, device.Target())}}};
AgcDriver::Graphics::State state{};
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt};
state.color = {reinterpret_cast<std::uintptr_t>(pixels.data()), {Extent, Extent}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u};

View File

@ -37,11 +37,14 @@ struct Case {
std::uint32_t records;
bool inRange;
const char* what;
std::uint32_t selectors = 0xfacu;
};
std::vector<std::uint32_t> registrationModule;
std::array<std::uint32_t, 4> VertexBufferDescriptor(const Case& value) {
const auto address = reinterpret_cast<std::uintptr_t>(Element.data());
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), value.records, 0x0104dfacu | (value.select << 28u)};
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), value.records, (0x0104dfacu & ~0xfffu) | value.selectors | (value.select << 28u)};
}
std::array<std::uint32_t, 4> OutputDescriptor() {
@ -74,6 +77,10 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) {
vertexRequest.useCache = false;
const auto vertexResult = ShaderRecompiler::Recompile(vertexRequest);
Require(vertexResult.vertexAttributes.size() == 1u, std::string(value.what) + ": the fetch did not become a vertex attribute");
if (device.Target().registrationModules) {
if (registrationModule.empty()) registrationModule = vertexResult.spirv.Words();
Require(vertexResult.spirv.Words() == registrationModule, std::string(value.what) + ": registration modules: the V# specialized the vertex module");
}
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
ShaderRecompiler::ShaderPixelStageInfo pixel{};
@ -120,7 +127,9 @@ void Draw(AgcDriver::VulkanDevice& device, const Case& value) {
void Check(const Case& value) {
for (std::uint32_t vertex = 0; vertex < Vertices; ++vertex) {
for (std::uint32_t component = 0; component < 4u; ++component) {
const auto expected = value.inRange ? std::bit_cast<std::uint32_t>(Element[component]) : 0u;
const auto selector = (value.selectors >> (component * 3u)) & 7u;
const auto selected = selector >= 4u ? std::bit_cast<std::uint32_t>(Element[selector - 4u]) : selector == 1u ? std::bit_cast<std::uint32_t>(1.0f) : 0u;
const auto expected = value.inRange || selector < 4u ? selected : 0u;
const auto actual = Output[vertex * 4u + component];
Require(actual == expected, std::string(value.what) + ": vertex " + std::to_string(vertex) + " v" + std::to_string(8u + component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected));
}
@ -133,12 +142,14 @@ int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
constexpr std::array<Case, 5> cases{{
constexpr std::array<Case, 7> cases{{
{2u, 1u, true, "OOB_SELECT 2, one record"},
{2u, 0u, false, "OOB_SELECT 2, no records"},
{3u, 16u, true, "OOB_SELECT 3, whole element in range"},
{3u, 0u, false, "OOB_SELECT 3, no records"},
{0u, 16u, true, "OOB_SELECT 0, whole element in range"},
{3u, 16u, true, "OOB_SELECT 3, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)},
{3u, 0u, false, "OOB_SELECT 3, no records, selectors W Z 1 0", 7u | (6u << 3u) | (1u << 6u) | (0u << 9u)},
}};
for (const auto& value : cases) {
Draw(*device, value);

View File

@ -236,6 +236,7 @@ function(add_shader_recompiler target runtimeTarget)
MatchesPreparedShader
GetPreparedArtifact
MaterializeShader
RegistrationResult
ResolveSource
ResourceMaterializer::SpecializationNanoseconds
RuntimeAbi::RequireVersion

View File

@ -241,6 +241,8 @@ private:
append(key, value.srgbDecodeFormats);
append(key, value.narrowSubgroupClock);
append(key, value.float64DenormPreserve);
append(key, value.registrationModules);
append(key, value.registrationWordLimit);
}
};

View File

@ -756,6 +756,8 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
if (request.context.compute.has_value()) writer.WriteU32(request.context.compute->scratchDwords);
writer.WriteBool(request.target.narrowSubgroupClock);
writer.WriteBool(request.target.float64DenormPreserve);
writer.WriteBool(request.target.registrationModules);
writer.WriteU32(request.target.registrationWordLimit);
return base64Encode(buffer);
}
@ -785,6 +787,8 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
if (version >= 10u && result.request.context.compute.has_value()) result.request.context.compute->scratchDwords = reader.ReadU32();
if (version >= 12u) result.request.target.narrowSubgroupClock = reader.ReadBool();
if (version >= 16u) result.request.target.float64DenormPreserve = reader.ReadBool();
if (version >= 17u) result.request.target.registrationModules = reader.ReadBool();
if (version >= 17u) result.request.target.registrationWordLimit = reader.ReadU32();
return result;
}

View File

@ -226,7 +226,7 @@ int main(int argc, char** argv) {
std::cout << "ShaderInfoCollector::Collect OK\n";
BindingAllocator bindingAllocator;
const auto bindings = bindingAllocator.Allocate(program, request.layout);
const auto bindings = bindingAllocator.Allocate(program, request.layout, request.target.registrationModules);
std::cout << "BindingAllocator::Allocate OK, bindings=" << bindings.bindings.size() << "\n";
std::cout << "ALL STAGES OK\n";

View File

@ -43,6 +43,7 @@ struct IrBindingLayout {
std::uint32_t memoryOffsetCount = 0;
bool dispatchThreadLimit = false;
std::uint32_t runtimeImageCount = 0;
std::uint32_t runtimeStateDwords = 0;
std::vector<std::uint32_t> userDataRegisters;
std::vector<IrDescriptorBinding> descriptors;
@ -52,9 +53,12 @@ struct IrBindingLayout {
[[nodiscard]] std::uint32_t ImageMetadataDword() const {
return DispatchThreadLimitDword() + (dispatchThreadLimit ? 3u : 0u);
}
[[nodiscard]] std::uint32_t ShaderDataDwords() const {
[[nodiscard]] std::uint32_t RuntimeStateDword() const {
return ImageMetadataDword() + runtimeImageCount * (sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t));
}
[[nodiscard]] std::uint32_t ShaderDataDwords() const {
return RuntimeStateDword() + runtimeStateDwords;
}
[[nodiscard]] bool UsesPushData() const {
return pushDataStartDword != PushData::NoStart;
}

View File

@ -23,7 +23,7 @@ struct BindingAllocationResult : CompiledBindingLayout {
class BindingAllocator {
public:
[[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout) const;
[[nodiscard]] BindingAllocationResult Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules = false) const;
[[nodiscard]] const IrDescriptorBinding& FindBinding(const IrBindingLayout& layout, DescriptorBindingKind kind) const;
};

View File

@ -14,6 +14,7 @@ struct PreparedDescriptorBinding {
std::vector<std::uint32_t> resources;
std::vector<bool> identityImageSwizzle;
std::vector<std::uint32_t> samplerFilterElements;
std::vector<bool> inactive;
std::size_t writtenBuffers = 0;
std::size_t readOnlyBuffers = 0;
};
@ -28,12 +29,13 @@ struct DescriptorBindingPlan {
std::vector<PreparedDescriptorBinding> bindings;
std::vector<PreparedImageMetadata> imageMetadata;
std::vector<PipelineSpecializationConstant> specialization;
std::vector<std::uint32_t> runtimeState;
bool storageData = false;
};
class DescriptorBindingBuilder {
public:
DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span<const std::uint8_t> exportMappings = {}) const;
DescriptorBindingPlan Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span<const std::uint8_t> exportMappings = {}, bool registrationModules = false, bool fragmentDescriptorSet = false) const;
DescriptorBindingPlan Select(const DescriptorBindingPlan& plan, std::span<const std::uint32_t> bindings) const;
void Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array<std::uint32_t, 3>& partialThreads) const;
void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array<std::uint32_t, 3>& partialThreads, std::span<const std::uint8_t> exportMappings = {}) const;

View File

@ -70,6 +70,7 @@ private:
void BuildPreparedShaderKey(const RecompileRequest& request, std::vector<std::uint64_t>& key);
[[nodiscard]] bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle, std::span<const std::uint64_t> key);
[[nodiscard]] const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle);
[[nodiscard]] std::shared_ptr<const RecompileResult> RegistrationResult(const RecompileRequest& request, const SourceHandle& handle);
[[nodiscard]] std::shared_ptr<const RecompileResult> MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle);
[[nodiscard]] std::shared_ptr<const SourceHandle> ResolveSource(const RecompileRequest& request);
[[nodiscard]] std::shared_ptr<const ResourceCapture> CaptureResources(const RecompileRequest& request, const SrtRuntime& runtime, const SourceHandle& handle);

View File

@ -68,7 +68,7 @@ bool usesGds(const IrProgram& program) {
}
BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout) const {
BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const BindingLayout& layout, bool registrationModules) const {
IrProgramMetadata& metadata = program.Metadata();
if (!metadata.shaderInfoComplete || metadata.bindingLayoutComplete) {
fail(metadata.shaderInfoComplete ? "shader binding layout failed: binding layout already allocated"
@ -102,7 +102,17 @@ BindingAllocationResult BindingAllocator::Allocate(IrProgram& program, const Bin
next.dispatchThreadLimit = info.dispatchThreadLimit;
if (info.buffers.size() > RuntimeAbi::BufferCapacity || info.images.size() > RuntimeAbi::ImageCapacity || info.samplers.size() > RuntimeAbi::SamplerHeapCapacity) fail("shader binding layout exceeds runtime metadata capacity");
if (std::ranges::any_of(next.userDataRegisters, [](auto reg) { return reg >= RuntimeAbi::UserDataCapacity; })) fail("shader user data exceeds runtime ABI capacity");
if (std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast<std::uint32_t>(info.images.size());
if (registrationModules || std::ranges::any_of(info.images, [](const auto& image) { return image.indirectRoot != ImageResource::NoIndirectImage; })) next.runtimeImageCount = static_cast<std::uint32_t>(info.images.size());
if (registrationModules && program.Resources().stage == IrShaderStage::Pixel) next.runtimeStateDwords = RuntimeAbi::StateDwords;
if (registrationModules && program.Resources().stage != IrShaderStage::Pixel) {
std::uint32_t attributes = 0u;
for (const auto& block : program.Blocks()) {
for (const IrValue* inst : block->Instructions()) {
if (inst->Opcode() == IrOpcode::GetAttribute && inst->Flags<std::uint32_t>() == 1u) attributes = std::max(attributes, inst->Argument(0)->Resolve()->ImmediateU32() + 1u);
}
}
next.runtimeStateDwords = (attributes + RuntimeAbi::VertexSelectorsPerDword - 1u) / RuntimeAbi::VertexSelectorsPerDword;
}
const auto pushSize = layout.pushConstantSizeBytes / 4u;
next.pushDataStartDword = next.runtimeImageCount == 0u && next.ShaderDataDwords() != 0u && next.ShaderDataDwords() <= pushSize ? layout.pushConstantOffsetBytes / 4u : PushData::NoStart;

View File

@ -208,7 +208,7 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh
return proof;
}
std::vector<std::uint32_t> ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array<std::uint32_t, 3>& partialThreads, const std::vector<PreparedImageMetadata>& imageMetadata) {
std::vector<std::uint32_t> ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array<std::uint32_t, 3>& partialThreads, const std::vector<PreparedImageMetadata>& imageMetadata, std::span<const std::uint32_t> runtimeState) {
std::vector<std::uint32_t> result(layout.ShaderDataDwords(), 0u);
for (std::size_t i = 0; i < layout.userDataRegisters.size(); ++i) {
const auto reg = layout.userDataRegisters[i];
@ -226,6 +226,8 @@ std::vector<std::uint32_t> ShaderDataDwordsFor(const IrBindingLayout& layout, st
std::copy_n(descriptor.dwords.begin(), metadata.descriptor.size(), metadata.descriptor.begin());
std::memcpy(result.data() + entry.offset, &metadata, sizeof(metadata));
}
if (runtimeState.size() != layout.runtimeStateDwords) fail("runtime state does not match its shader data layout");
std::copy(runtimeState.begin(), runtimeState.end(), result.begin() + layout.RuntimeStateDword());
return result;
}
@ -250,7 +252,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con
allocation.specialization = std::move(plan.specialization);
}
DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span<const std::uint8_t> exportMappings) const {
DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& layout, const ShaderInfo& info, IrShaderStage stage, const ResourceSnapshot& snapshot, std::span<const std::uint8_t> exportMappings, bool registrationModules, bool fragmentDescriptorSet) const {
if (stage == IrShaderStage::Pixel && exportMappings.size() != 8u) fail("fragment runtime export mappings are missing");
if (info.images.size() > ShaderInfo::MaxImages) fail("runtime image count exceeds the static capacity");
DescriptorBindingPlan plan;
@ -264,7 +266,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
const auto compare = compareStates[index] = ResourceMaterializer::EmulatedCompareState(info, snapshot, index);
const auto first = PipelineSpecialization::CompareBase + index * PipelineSpecialization::CompareWords;
const std::array<std::uint32_t, 6> values{EmulatedCompare::Function(compare), (compare & EmulatedCompare::Linear) != 0u, EmulatedCompare::AddressX(compare), EmulatedCompare::AddressY(compare), EmulatedCompare::Reference(compare), (compare & EmulatedCompare::BorderWhite) != 0u};
for (std::uint32_t word = 0; word < values.size(); ++word) plan.specialization.push_back({first + word, values[word]});
for (std::uint32_t word = 0; word < values.size() && !registrationModules; ++word) plan.specialization.push_back({first + word, values[word]});
}
std::vector<std::uint32_t> imageModes(info.images.size());
for (std::size_t index = 0; index < info.images.size(); ++index) imageModes[index] = ResourceMaterializer::RuntimeImageMode(info.images[index], snapshot.images.at(index), info.runtimeImageModes.at(index));
@ -280,11 +282,15 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
auto alignment = static_cast<std::uint32_t>(buffer.typedAlignment);
if (buffer.descriptorFormatted && format != 0u) alignment = std::max(alignment, std::min(GetFormatInfo(static_cast<IrBufferFormat>(format)).byteSize, 4u));
if (alignment == 0u || misalignment % alignment != 0u) fail("is a formatted buffer access through a V# whose base is not aligned to its element");
const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords;
plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u});
plan.specialization.push_back({first + 1u, descriptor.dwords[3]});
plan.specialization.push_back({first + 2u, present ? 1u : 0u});
plan.specialization.push_back({first + 3u, misalignment});
if (registrationModules && buffer.descriptorFormatted) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 1u, descriptor.dwords[3]});
if (registrationModules) plan.specialization.push_back({PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords + 3u, misalignment});
if (!registrationModules) {
const auto first = PipelineSpecialization::BufferBase + index * PipelineSpecialization::BufferWords;
plan.specialization.push_back({first, descriptor.dwords[1] & 0xffff0000u});
plan.specialization.push_back({first + 1u, descriptor.dwords[3]});
plan.specialization.push_back({first + 2u, present ? 1u : 0u});
plan.specialization.push_back({first + 3u, misalignment});
}
if (buffer.formattedReadMask != 0u) {
for (std::uint32_t component = 0; component < 4u; ++component) {
if ((buffer.formattedReadMask & (1u << component)) == 0u) continue;
@ -293,13 +299,13 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
}
}
}
for (std::uint32_t index = 0; index < info.images.size(); ++index) {
for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) {
if (info.images[index].indirectRoot != ImageResource::NoIndirectImage) continue;
const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords;
plan.specialization.push_back({first, imageModes[index]});
for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u});
}
for (std::uint32_t target = 0; target < exportMappings.size(); ++target) {
for (std::uint32_t target = 0; target < exportMappings.size() && !registrationModules; ++target) {
for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u});
}
std::vector<std::uint32_t> samplerModes(info.samplers.size(), 0u);
@ -325,7 +331,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
const auto first = (word >> 12u) & 0xfu;
const auto last = (word >> 16u) & 0xfu;
if (last < first || last - first >= RuntimeAbi::StorageMipSlots) fail("invalid dynamic storage mip range");
plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u});
if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::MipCountBase + index, last - first + 1u});
}
for (std::uint32_t resource = 0; resource < info.images.size(); ++resource) {
const auto& image = info.images[resource];
@ -338,7 +344,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
if (mode != imageModes[slot]) fail("indirect image mode numbering disagrees with its root");
active[mode] = true;
}
for (std::uint32_t mode = 0; mode < modes.size(); ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u});
for (std::uint32_t mode = 0; mode < modes.size() && !registrationModules; ++mode) plan.specialization.push_back({PipelineSpecialization::ImageModeBase + resource * PipelineSpecialization::ImageModeStride + mode, active[mode] ? 1u : 0u});
}
plan.bindings.reserve(layout.descriptors.size());
for (const auto& logical : layout.descriptors) {
@ -346,6 +352,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
const bool samplerHeap = logical.kind == DescriptorBindingKind::Samplers;
IrDescriptorBinding compact{logical.kind, {}};
std::vector<std::uint32_t> originals;
std::vector<bool> inactive;
std::uint32_t previous = UINT32_MAX;
std::uint32_t mip = 0;
for (std::uint32_t element = 0; element < logical.resources.size(); ++element) {
@ -363,17 +370,18 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
} else if (samplerHeap) {
active = (samplerModes.at(resource) & (1u << (element & 1u))) != 0u;
}
if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast<std::uint32_t>(logical.kind), element), active ? static_cast<std::uint32_t>(compact.resources.size()) : 0u});
if (active) {
if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::DescriptorIndex(static_cast<std::uint32_t>(logical.kind), element), active ? static_cast<std::uint32_t>(compact.resources.size()) : 0u});
if (active || ((imageHeap || samplerHeap) && registrationModules)) {
compact.resources.push_back(resource);
originals.push_back(element);
inactive.push_back(!active);
}
}
if (imageHeap || samplerHeap) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast<std::uint32_t>(logical.kind), std::max(1u, static_cast<std::uint32_t>(compact.resources.size()))});
if ((imageHeap || samplerHeap) && !registrationModules) plan.specialization.push_back({PipelineSpecialization::HeapCountBase + static_cast<std::uint32_t>(logical.kind), std::max(1u, static_cast<std::uint32_t>(compact.resources.size()))});
if ((imageHeap || samplerHeap) && compact.resources.empty()) continue;
PreparedDescriptorBinding entry{};
auto& physical = entry.descriptor;
physical.descriptorSet = RuntimeAbi::DescriptorSet;
physical.descriptorSet = fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet;
physical.binding = NativeBinding(stage, compact.kind);
physical.count = compact.resources.empty() ? 1u : static_cast<std::uint32_t>(compact.resources.size());
physical.kind = PhysicalKindFor(compact.kind);
@ -404,7 +412,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
physical.imageUnnormalizedBaseLevel.push_back(unnormalized.imagesAtBaseLevel.at(resource));
physical.imageSamplers.push_back(0u);
physical.imageAtomic64.push_back(image.atomic64);
if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage) continue;
if (layout.runtimeImageCount == 0u || image.indirectRoot == ImageResource::NoIndirectImage || inactive[element]) continue;
if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout");
const auto& descriptor = snapshot.images.at(resource);
RuntimeAbi::ResourceMetadata metadata{};
@ -412,7 +420,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
metadata.binding = static_cast<std::uint32_t>(compact.kind);
metadata.firstElement = static_cast<std::uint32_t>(element);
metadata.elementCount = 1u;
metadata.flags = imageModes[resource] << 1u;
metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u);
if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u;
const auto offset = layout.ImageMetadataDword() + resource * static_cast<std::uint32_t>(sizeof(metadata) / sizeof(std::uint32_t));
plan.imageMetadata.push_back({resource, offset, metadata});
@ -426,20 +434,39 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l
physical.samplerUnnormalized.push_back(unnormalized.samplers.at(resource));
const auto mode = 1u << (originals[element] & 1u);
physical.samplerIntegerBorder.push_back((samplerIntegerModes.at(resource) & mode) != 0u && (samplerFloatModes.at(resource) & mode) == 0u);
if ((originals[element] & 1u) != 0u) entry.samplerFilterElements.push_back(element);
if ((originals[element] & 1u) != 0u && !inactive[element]) entry.samplerFilterElements.push_back(element);
}
} else if (physical.role == DescriptorRole::ShaderData && layout.UsesPushData()) {
fail("DescriptorBindingBuilder::Populate shader-data binding must not exist when push data is used");
}
plan.storageData |= physical.role == DescriptorRole::ShaderData;
entry.resources = std::move(compact.resources);
if (registrationModules && (imageHeap || samplerHeap)) entry.inactive = std::move(inactive);
plan.bindings.push_back(std::move(entry));
}
for (std::uint32_t resource = 0; resource < info.images.size() && registrationModules && layout.runtimeImageCount != 0u; ++resource) {
if (info.images[resource].indirectRoot != ImageResource::NoIndirectImage) continue;
if (resource >= layout.runtimeImageCount) fail("runtime image metadata exceeds compact layout");
const auto& descriptor = snapshot.images.at(resource);
RuntimeAbi::ResourceMetadata metadata{};
if (descriptor.dwordCount != metadata.descriptor.size()) fail("invalid runtime image descriptor width");
metadata.elementCount = 1u;
if (imageModes[resource] > RuntimeAbi::ImageModeMask) fail("runtime image mode exceeds its metadata field");
metadata.flags = imageModes[resource] << 1u | (registrationModules ? (compareStates[resource] & RuntimeAbi::CompareStateMask) << RuntimeAbi::CompareStateShift : 0u);
if (descriptor.dwords[0] == 0u && (descriptor.dwords[1] & 0xffu) == 0u) metadata.flags |= 1u;
plan.imageMetadata.push_back({resource, layout.ImageMetadataDword() + resource * static_cast<std::uint32_t>(sizeof(metadata) / sizeof(std::uint32_t)), metadata});
}
if (layout.runtimeStateDwords != 0u) {
plan.runtimeState.assign(layout.runtimeStateDwords, 0u);
for (std::uint32_t target = 0; target < exportMappings.size(); ++target) plan.runtimeState.at(RuntimeAbi::StateExportMappings + target / 4u) |= static_cast<std::uint32_t>(exportMappings[target]) << (target % 4u * 8u);
}
std::vector<std::uint32_t> samplerMasks(info.samplers.size());
for (const auto& binding : plan.bindings) {
if (binding.descriptor.role != DescriptorRole::GuestSamplers) continue;
if (binding.resources.size() > ShaderInfo::MaxSamplers) fail("prepared sampler mask capacity exceeded");
for (std::uint32_t element = 0; element < binding.resources.size(); ++element) samplerMasks.at(binding.resources[element]) |= 1u << element;
for (std::uint32_t element = 0; element < binding.resources.size(); ++element) {
if (binding.inactive.empty() || !binding.inactive[element]) samplerMasks.at(binding.resources[element]) |= 1u << element;
}
}
for (auto& binding : plan.bindings) {
if (binding.descriptor.role != DescriptorRole::GuestImages) continue;
@ -470,13 +497,14 @@ DescriptorBindingPlan DescriptorBindingBuilder::Select(const DescriptorBindingPl
}
}
selected.imageMetadata = plan.imageMetadata;
selected.runtimeState = plan.runtimeState;
return selected;
}
void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const CompiledBindingLayout& compiled, const DescriptorBindingPlan& plan, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array<std::uint32_t, 3>& partialThreads) const {
const auto& layout = compiled.layout;
const bool needsShaderData = layout.UsesPushData() || plan.storageData;
auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata) : std::vector<std::uint32_t>{};
auto shaderData = needsShaderData ? ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads, plan.imageMetadata, plan.runtimeState) : std::vector<std::uint32_t>{};
std::vector<DescriptorBinding> bindings;
bindings.reserve(plan.bindings.size());
std::size_t writtenHere = 0;
@ -494,6 +522,10 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con
for (std::size_t element = 0; element < entry.resources.size(); ++element) {
const auto& descriptor = snapshot.images.at(entry.resources[element]);
if (descriptor.dwordCount != 8u) fail("invalid runtime image descriptor width");
if (!entry.inactive.empty() && entry.inactive[element]) {
physical.guestDescriptor.insert(physical.guestDescriptor.end(), 8u, 0u);
continue;
}
const auto offset = physical.guestDescriptor.size();
physical.guestDescriptor.insert(physical.guestDescriptor.end(), descriptor.dwords.begin(), descriptor.dwords.begin() + 8);
if (entry.identityImageSwizzle[element]) physical.guestDescriptor[offset + 3u] = (physical.guestDescriptor[offset + 3u] & ~0xfffu) | ShaderImageIdentitySwizzle;

View File

@ -1,5 +1,6 @@
#include "Recompiler.hpp"
#include <algorithm>
#include <cstring>
#include <atomic>
#include <chrono>
#include <cstdlib>
@ -12,6 +13,7 @@
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
#include "ShaderDiskCache.hpp"
#include <list>
#include <functional>
#include <map>
#include <set>
#include <mutex>
@ -406,7 +408,8 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra
shaderInfoCollector.Collect(program, inputInfo);
constexpr BindingAllocator bindingAllocator;
auto bindings = bindingAllocator.Allocate(program, request.layout);
bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment);
auto bindings = bindingAllocator.Allocate(program, request.layout, registration);
SpirvTargetOptions targetOptions {};
targetOptions.vulkanVersion = request.target.vulkanVersion;
@ -418,11 +421,21 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra
targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets;
targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock;
targetOptions.float64DenormPreserve = request.target.float64DenormPreserve;
targetOptions.registrationModules = registration;
targetOptions.fragmentDescriptorSet = request.target.registrationModules;
constexpr SpirvEmitter spirvEmitter;
CompiledShaderArtifact result;
result.variantId = nextVariantId();
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
if (registration && result.spirv.size() > request.target.registrationWordLimit) {
registration = false;
program.Metadata().bindingLayoutComplete = false;
bindings = bindingAllocator.Allocate(program, request.layout, false);
targetOptions.registrationModules = false;
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
}
result.registrationModule = registration;
result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u;
result.memoryOffsetDword = bindings.layout.memoryOffsetDword;
@ -601,7 +614,39 @@ struct PreparedBindingPlan {
std::map<std::vector<std::uint32_t>, std::shared_ptr<PreparedModuleEntry>> modules;
};
std::shared_ptr<PreparedBindingPlan> preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span<const std::uint8_t> exports) {
struct RegistrationModuleEntry {
std::once_flag ready;
std::exception_ptr failure;
std::shared_ptr<const SpecializedModule> module;
};
std::shared_ptr<const SpecializedModule> registrationModule(std::uint64_t variantId, std::span<const std::uint32_t> moduleKey, const std::function<std::shared_ptr<const SpecializedModule>()>& specialize) {
std::vector<std::uint64_t> key{variantId};
key.insert(key.end(), moduleKey.begin(), moduleKey.end());
static std::shared_mutex mutex;
static std::map<std::vector<std::uint64_t>, std::shared_ptr<RegistrationModuleEntry>> modules;
std::shared_ptr<RegistrationModuleEntry> entry;
{
std::shared_lock lock(mutex);
if (const auto found = modules.find(key); found != modules.end()) entry = found->second;
}
if (entry == nullptr) {
std::unique_lock lock(mutex);
const auto found = modules.find(key);
entry = found != modules.end() ? found->second : modules.emplace(key, std::make_shared<RegistrationModuleEntry>()).first->second;
}
std::call_once(entry->ready, [&] {
try {
entry->module = specialize();
} catch (...) {
entry->failure = std::current_exception();
}
});
if (entry->failure) std::rethrow_exception(entry->failure);
return entry->module;
}
std::shared_ptr<PreparedBindingPlan> preparedBindingPlan(const CompiledVariant& variant, const ResourceSnapshot& snapshot, std::span<const std::uint8_t> exports, bool fragmentDescriptorSet) {
struct BindingPlanKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, BindingPlanKeyStorage>();
key.clear();
@ -642,7 +687,7 @@ std::shared_ptr<PreparedBindingPlan> preparedBindingPlan(const CompiledVariant&
}
std::call_once(plan->ready, [&] {
try {
plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports);
plan->bindings = DescriptorBindingBuilder{}.Prepare(variant.bindings.layout, variant.info.info, variant.info.stage, snapshot, exports, variant.artifact.registrationModule, fragmentDescriptorSet);
} catch (...) {
plan->failure = std::current_exception();
}
@ -680,7 +725,7 @@ std::shared_ptr<const SpecializedModule> specializeModule(const CompiledShaderAr
return entry->module;
}
RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot) {
RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) {
RecompileResult result;
const auto& artifact = variant.artifact;
static_cast<CompiledShaderArtifact&>(result) = {
@ -689,6 +734,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
.imageMetadataDword = artifact.imageMetadataDword,
.runtimeImageCount = artifact.runtimeImageCount,
.runtimeImageResources = artifact.runtimeImageResources,
.registrationModule = artifact.registrationModule,
.bdaAbiVersion = artifact.bdaAbiVersion,
.runtimeAbiVersion = artifact.runtimeAbiVersion,
.vertexInputs = artifact.vertexInputs,
@ -704,7 +750,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
.barycentricEmulation = artifact.barycentricEmulation,
.variantId = artifact.variantId
};
const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span<const std::uint8_t>(request.context.pixel->targetExportMapping) : std::span<const std::uint8_t>{});
const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span<const std::uint8_t>(request.context.pixel->targetExportMapping) : std::span<const std::uint8_t>{}, request.target.registrationModules);
const DescriptorBindingPlan* bindingPlan = &plan->bindings;
std::shared_ptr<PreparedModuleEntry> entry;
struct LocalModuleKeyStorage {};
@ -717,6 +763,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
}
result.vertexAttributes.reserve(result.vertexInputs.size());
std::array<std::uint32_t, ShaderVertexStageInfo::MaxResources> vertexClasses{};
std::array<std::uint32_t, ShaderVertexStageInfo::MaxResources / RuntimeAbi::VertexSelectorsPerDword> runtimeSelectors{};
for (const auto& input : result.vertexInputs) {
if (!request.context.vertex || input.location >= request.context.vertex->resourcesNum || input.location >= request.context.vertex->resources.size()) throw std::runtime_error("Shader cache: invalid vertex attribute metadata");
const auto& vertex = *request.context.vertex;
@ -724,7 +771,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
const auto fetchIndex = vertex.fetchEmbedded ? vertex.resourcesDst[input.location].fetchIndex : input.fetchIndex;
result.vertexAttributes.push_back({input.location, input.components, resource, fetchIndex});
if (!artifact.vertexInputPatches.empty()) {
const auto numeric = VertexInputNumericClass(static_cast<IrBufferFormat>((resource.fields[3] >> 12u) & 0x7fu));
const auto format = static_cast<IrBufferFormat>((resource.fields[3] >> 12u) & 0x7fu);
const auto numeric = registrationDefaults && format == IrBufferFormat::Invalid ? IrTextureNumericClass::Float : VertexInputNumericClass(format);
if (numeric == IrTextureNumericClass::Unsupported) throw std::runtime_error("unsupported prepared vertex format");
const auto kind = numeric == IrTextureNumericClass::Float ? 0u : numeric == IrTextureNumericClass::Sint ? 1u : 2u;
vertexClasses.at(input.location) = kind;
@ -737,7 +785,8 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
if (selector >= 4u) formatComponents = std::max(formatComponents, selector - 3u);
selectors |= selector << (component * 3u);
}
moduleKey.push_back(selectors);
moduleKey.push_back(artifact.registrationModule ? selectors & ~0xfffu : selectors);
if (artifact.registrationModule) runtimeSelectors.at(input.location / RuntimeAbi::VertexSelectorsPerDword) |= (selectors & 0xfffu) << (input.location % RuntimeAbi::VertexSelectorsPerDword * 16u);
}
}
if (!plan->bindings.specialization.empty() || variant.bindings.layout.UsesPushData() || !artifact.vertexInputPatches.empty() || request.context.pixel) {
@ -768,7 +817,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
constants.push_back({first + 5u, kind});
}
}
const auto module = specializeModule(variant.artifact, vertexClasses, constants, request.target);
const auto module = artifact.registrationModule && plan->bindings.specialization.empty() ? registrationModule(artifact.variantId, moduleKey, [&] { return specializeModule(variant.artifact, vertexClasses, constants, request.target); }) : specializeModule(variant.artifact, vertexClasses, constants, request.target);
if (variant.bindings.layout.UsesPushData() && !module->pushData) throw std::runtime_error("specialization removed the prepared push constant interface");
auto selected = DescriptorBindingBuilder{}.Select(plan->bindings, module->bindings);
entry->bindings = std::move(selected);
@ -794,6 +843,19 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil
result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info);
result.bindings = std::move(bindings.bindings);
result.pushConstants = std::move(bindings.pushConstants);
const auto& layout = variant.bindings.layout;
if (artifact.registrationModule && request.shader.stage != ShaderStage::Fragment && layout.runtimeStateDwords != 0u) {
const auto first = layout.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors;
if (layout.runtimeStateDwords > runtimeSelectors.size()) throw std::runtime_error("vertex selector state exceeds its capacity");
if (layout.UsesPushData()) {
if ((first + layout.runtimeStateDwords) * sizeof(std::uint32_t) > result.pushConstants.size()) throw std::runtime_error("vertex selector state lies outside the push data");
std::memcpy(result.pushConstants.data() + first * sizeof(std::uint32_t), runtimeSelectors.data(), layout.runtimeStateDwords * sizeof(std::uint32_t));
} else {
const auto data = std::ranges::find_if(result.bindings, [](const DescriptorBinding& binding) { return binding.role == DescriptorRole::ShaderData; });
if (data == result.bindings.end() || first + layout.runtimeStateDwords > data->guestDescriptor.size()) throw std::runtime_error("vertex selector state lies outside the shader data");
std::copy_n(runtimeSelectors.begin(), layout.runtimeStateDwords, data->guestDescriptor.begin() + first);
}
}
result.poisonedSrtReads = static_cast<std::uint32_t>(snapshot.srtPoison.size()) + snapshot.nullRootReads;
return result;
}
@ -1182,6 +1244,20 @@ const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) {
return handle.artifact->artifact;
}
std::shared_ptr<const RecompileResult> RegistrationResult(const RecompileRequest& request, const SourceHandle& handle) {
if (handle.artifact == nullptr || !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI");
if (!request.target.registrationModules || !handle.artifact->artifact.registrationModule) return nullptr;
const auto& info = handle.artifact->info.info;
if (std::ranges::any_of(info.buffers, [](const BufferResource& buffer) { return buffer.descriptorFormatted; }) || std::ranges::any_of(info.samplers, [](const SamplerResource& sampler) { return FoldsTexelOffsets(sampler); })) return nullptr;
ResourceSnapshot snapshot;
snapshot.buffers.assign(info.buffers.size(), {{}, 4u});
snapshot.images.assign(info.images.size(), {{}, 8u});
snapshot.samplers.assign(info.samplers.size(), {{}, 4u});
snapshot.userData.assign(RuntimeAbi::UserDataCapacity, 0u);
snapshot.flattenedSrt.assign(1u, 0u);
return std::make_shared<RecompileResult>(materializeResult(*handle.artifact, request, snapshot, true));
}
std::shared_ptr<const RecompileResult> MaterializeShader(const RecompileRequest& request, const ResourceCapture& capture, const SourceHandle& handle) {
if (!MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI");
if (capture.source != handle.source || capture.plan != handle.source->plan) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader");

View File

@ -230,6 +230,8 @@ struct SpirvTarget {
bool narrowSubgroupClock = false;
bool fixedPushSlots = false;
bool float64DenormPreserve = false;
bool registrationModules = false;
std::uint32_t registrationWordLimit = 200000u;
};
struct BindingLayout {
@ -476,6 +478,7 @@ struct CompiledShaderArtifact {
std::uint32_t imageMetadataDword = 0;
std::uint32_t runtimeImageCount = 0;
std::vector<std::uint32_t> runtimeImageResources;
bool registrationModule = false;
std::uint32_t bdaAbiVersion = 0;
std::uint32_t runtimeAbiVersion = RuntimeAbi::Version;
std::vector<VertexInput> vertexInputs;

View File

@ -11,6 +11,7 @@ namespace ShaderRecompiler::RuntimeAbi {
inline constexpr std::uint32_t Version = 11u;
inline constexpr std::uint32_t DescriptorSet = 0u;
inline constexpr std::uint32_t FragmentDescriptorSet = 1u;
inline constexpr std::uint32_t StageCount = 4u;
inline constexpr std::uint32_t PushConstantDwords = 32u;
inline constexpr std::uint32_t FirstImageBinding = 1u;
@ -39,6 +40,13 @@ inline constexpr std::uint32_t BindlessTableSlots = 16u;
inline constexpr std::uint32_t StorageMipSlots = 4u;
inline constexpr std::uint32_t StorageHeapCapacity = 16u;
inline constexpr std::uint32_t SamplerHeapCapacity = 32u;
inline constexpr std::uint32_t StateExportMappings = 0u;
inline constexpr std::uint32_t StateDwords = 2u;
inline constexpr std::uint32_t StateVertexSelectors = 0u;
inline constexpr std::uint32_t VertexSelectorsPerDword = 2u;
inline constexpr std::uint32_t ImageModeMask = 0xffu;
inline constexpr std::uint32_t CompareStateShift = 9u;
inline constexpr std::uint32_t CompareStateMask = 0x1fffu;
struct ResourceMetadata {
std::uint32_t binding;

View File

@ -64,9 +64,9 @@ std::filesystem::path ShaderCacheDirectory() {
namespace ShaderRecompiler::ShaderDiskCache {
#if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__)
static_assert(sizeof(CompiledShaderArtifact) == 192, "CompiledShaderArtifact changed: update the artifact encoder");
static_assert(sizeof(CompiledShaderArtifact) == 200, "CompiledShaderArtifact changed: update the artifact encoder");
static_assert(sizeof(ShaderInvocation) == 112, "ShaderInvocation changed: update the invocation encoder");
static_assert(sizeof(RecompileResult) == 312, "RecompileResult changed: update EncodeResult and DecodeResult");
static_assert(sizeof(RecompileResult) == 320, "RecompileResult changed: update EncodeResult and DecodeResult");
static_assert(sizeof(DescriptorBinding) == 568, "DescriptorBinding changed: update the binding encoder");
static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder");
static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder");
@ -297,6 +297,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) {
writer.Value(result.imageMetadataDword);
writer.Value(result.runtimeImageCount);
writer.Values(std::span<const std::uint32_t>(result.runtimeImageResources));
writer.Value(result.registrationModule);
writer.List(result.vertexInputPatches, [](Writer& out, const VertexInputPatch& patch) {
out.Value(patch.location);
out.Value(patch.word);
@ -339,6 +340,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) {
reader.Value(result.imageMetadataDword);
reader.Value(result.runtimeImageCount);
reader.Values(result.runtimeImageResources);
reader.Value(result.registrationModule);
reader.List(result.vertexInputPatches, 20, [](Reader& in, VertexInputPatch& patch) {
in.Value(patch.location);
in.Value(patch.word);
@ -416,6 +418,7 @@ void encodeLayout(Writer& writer, const IrBindingLayout& layout) {
writer.Value(layout.memoryOffsetCount);
writer.Value(layout.dispatchThreadLimit);
writer.Value(layout.runtimeImageCount);
writer.Value(layout.runtimeStateDwords);
writer.Values(std::span<const std::uint32_t>(layout.userDataRegisters));
writer.List(layout.descriptors, [](Writer& out, const IrDescriptorBinding& descriptor) {
out.Value(descriptor.kind);
@ -429,6 +432,7 @@ void decodeLayout(Reader& reader, IrBindingLayout& layout) {
reader.Value(layout.memoryOffsetCount);
reader.Value(layout.dispatchThreadLimit);
reader.Value(layout.runtimeImageCount);
reader.Value(layout.runtimeStateDwords);
reader.Values(layout.userDataRegisters);
reader.List(layout.descriptors, 12, [](Reader& in, IrDescriptorBinding& descriptor) {
in.Value(descriptor.kind);

View File

@ -21,6 +21,8 @@ struct SpirvTargetOptions {
bool nonConstantImageOffsets = false;
bool narrowSubgroupClock = false;
bool float64DenormPreserve = false;
bool registrationModules = false;
bool fragmentDescriptorSet = false;
};
class SpirvEmitter {

View File

@ -91,6 +91,8 @@ struct SpirvEmitterState {
bool splitSubgroup = false;
bool narrowSubgroupClock = false;
bool float64DenormPreserve = false;
bool registrationModules = false;
bool fragmentDescriptorSet = false;
std::unordered_set<const IrValue*> sharedLaneValues;
std::uint32_t laneHalf = 0;
// The target's SPIR-V version and what the device accepts, for capabilities an emitter adds

View File

@ -7,6 +7,7 @@
namespace ShaderRecompiler {
std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member);
std::uint32_t ResourceForDescriptor(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource);
std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t resultPtrType, std::uint32_t variableId, std::uint32_t arrayIndex, DescriptorBindingKind kind, std::uint32_t resource, const char* variableName);
std::uint32_t ImageScalarType(SpirvEmitterState& state, IrTextureNumericClass numericClass);

View File

@ -250,6 +250,8 @@ std::vector<std::uint32_t> SpirvEmitter::Emit(const IrProgram& program, const Sh
state.nonConstantImageOffsets = target.nonConstantImageOffsets;
state.narrowSubgroupClock = target.narrowSubgroupClock;
state.float64DenormPreserve = target.float64DenormPreserve;
state.registrationModules = target.registrationModules;
state.fragmentDescriptorSet = target.fragmentDescriptorSet;
state.hostSubgroupSize = target.subgroupSize;
state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u;
if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true;

View File

@ -4,6 +4,7 @@
#include "SpirvBackend/SpirvBda.hpp"
#include "SpirvBackend/SpirvBufferFormat.hpp"
#include "SpirvBackend/SpirvEmitterInstructions.hpp"
#include "SpirvBackend/SpirvMemory/SpirvDescriptors.hpp"
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
#include "RdnaDecoder/RdnaImageOpDecoder.hpp"
#include <span>
@ -46,18 +47,13 @@ struct SampleSetup {
std::uint32_t coord;
};
std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) {
const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource);
const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t))));
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword);
const auto value = state.module.AllocateId();
state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
return value;
std::uint32_t RuntimeImageMode(SpirvEmitterState& state, std::uint32_t resource) {
const auto mode = Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u));
return state.registrationModules ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), mode, ConstantU32(state, RuntimeAbi::ImageModeMask)) : mode;
}
std::uint32_t RuntimeImageSwizzle(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t component) {
if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u);
if (state.program.Info().images.at(resource).indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + resource * PipelineSpecialization::ImageWords + 1u + component, component + 4u);
const auto descriptor = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u);
return Binary(state, spv::OpBitwiseAnd, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeU32(state), descriptor, ConstantU32(state, component * 3u)), ConstantU32(state, 7u));
}
@ -1273,7 +1269,12 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess
auto& state = ctx.state;
const auto& mem = access.mem;
const auto& image = access.image;
const auto parameter = [&](std::uint32_t word) { return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u); };
const auto compare = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, mem.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, RuntimeAbi::CompareStateShift)), RuntimeAbi::CompareStateMask) : 0u;
const auto parameter = [&](std::uint32_t word) {
if (!state.registrationModules) return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::CompareBase + mem.resource * PipelineSpecialization::CompareWords + word, 0u);
constexpr std::array<std::pair<std::uint32_t, std::uint32_t>, 6> fields{{{EmulatedCompare::FunctionShift, 7u}, {4u, 1u}, {EmulatedCompare::ClampXShift, 3u}, {EmulatedCompare::ClampYShift, 3u}, {EmulatedCompare::ReferenceShift, 3u}, {12u, 1u}}};
return EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), compare, ConstantU32(state, fields.at(word).first)), fields.at(word).second);
};
const auto equal = [&](std::uint32_t value, std::uint32_t literal) { return Binary(state, spv::OpIEqual, TypeBool(state), value, ConstantU32(state, literal)); };
if (access.slot != 0) ctx.Fail(access.inst, "compares against a color texture through a bindless image table, which is not implemented");
if (HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod)) ctx.Fail(access.inst, "compares against a color texture with gradients or an explicit LOD, which is not implemented");
@ -1653,7 +1654,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) {
state.runtimeImageMetadata = 0u;
return;
}
const auto selector = base.indirectRoot == ImageResource::NoIndirectImage ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : Binary(state, spv::OpShiftRightLogical, TypeU32(state), RuntimeImageDword(state, memory.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, 1u));
const auto selector = base.indirectRoot == ImageResource::NoIndirectImage && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageBase + memory.resource * PipelineSpecialization::ImageWords, 0u) : RuntimeImageMode(state, memory.resource);
const bool returnsValue = inst.Opcode() != IrOpcode::ImageWrite;
const auto resultId = returnsValue ? ctx.Result(inst) : 0u;
ctx.definitions.erase(&inst);
@ -1677,7 +1678,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) {
for (std::uint32_t index = 0u; index < modes.size(); ++index) {
EmitLabel(state, labels[index]);
std::uint32_t modeMerge = 0u;
if (base.indirectRoot != ImageResource::NoIndirectImage) {
if (base.indirectRoot != ImageResource::NoIndirectImage && !state.registrationModules) {
const auto enabled = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ImageModeBase + memory.resource * PipelineSpecialization::ImageModeStride + index, 1u);
const auto active = Binary(state, spv::OpINotEqual, TypeBool(state), enabled, ConstantU32(state, 0u));
const auto activeLabel = state.module.AllocateId();

View File

@ -50,11 +50,28 @@ std::uint32_t DescriptorElementPointer(SpirvEmitterState& state, std::uint32_t r
}
const auto pointer = state.module.AllocateId();
const bool heap = kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(kind) != ImageResourceClass::None;
const auto index = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast<std::uint32_t>(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex);
const auto index = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::DescriptorIndex(static_cast<std::uint32_t>(kind), arrayIndex), arrayIndex) : ConstantU32(state, arrayIndex);
state.module.AddFunction(spv::OpAccessChain, resultPtrType, pointer, variableId, index);
return pointer;
}
std::uint32_t RuntimeImageDword(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t member) {
const auto index = state.runtimeImageMetadata != 0u ? state.runtimeImageMetadata : ConstantU32(state, resource);
const auto dword = Binary(state, spv::OpIAdd, TypeU32(state), ConstantU32(state, state.program.Metadata().bindings.ImageMetadataDword() + member), Binary(state, spv::OpIMul, TypeU32(state), index, ConstantU32(state, sizeof(RuntimeAbi::ResourceMetadata) / sizeof(std::uint32_t))));
const auto pointer = state.module.AllocateId();
state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.shaderDataStorageVariable, ConstantU32(state, 0u), dword);
const auto value = state.module.AllocateId();
state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
return value;
}
std::uint32_t RuntimeMipCount(SpirvEmitterState& state, std::uint32_t resource) {
const auto word = RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 3u);
const auto first = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 12u)), 0xfu);
const auto last = EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), word, ConstantU32(state, 16u)), 0xfu);
return Binary(state, spv::OpIAdd, TypeU32(state), Binary(state, spv::OpISub, TypeU32(state), last, first), ConstantU32(state, 1u));
}
std::uint32_t TableImageIndex(SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource) {
const bool storage = ImageBindingResourceClass(kind) == ImageResourceClass::Storage;
const auto supported = [&](std::uint32_t capability) {
@ -240,6 +257,7 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std
if (image.mipCount == 0u) {
ExitDescriptorBindingFailure(state, kind, resource, "dynamic storage image has no mip descriptors");
}
const auto count = state.registrationModules ? RuntimeMipCount(state, resource) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount);
const auto mergeLabel = state.module.AllocateId();
std::vector<std::uint32_t> labels(image.mipCount);
std::vector<std::uint32_t> words{spv::OpSwitch, mipLod, mergeLabel};
@ -252,7 +270,6 @@ void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std
state.module.AddFunction(std::span<const std::uint32_t>(words));
for (std::uint32_t mip = 0; mip < image.mipCount; mip++) {
EmitLabel(state, labels[mip]);
const auto count = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::MipCountBase + resource, image.mipCount);
const auto active = Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, mip), count);
const auto writeLabel = state.module.AllocateId();
const auto nextLabel = state.module.AllocateId();

View File

@ -394,14 +394,14 @@ void DefineDescriptors(SpirvEmitterState& state) {
const auto Define = [&](std::uint32_t type, const char* name, std::uint32_t storage = spv::StorageClassStorageBuffer) {
const auto variable = state.module.DefineGlobalVariable(TypePointer(state, storage, type), storage);
state.module.AddName(variable, name);
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u);
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, state.fragmentDescriptorSet && stage == IrShaderStage::Pixel ? RuntimeAbi::FragmentDescriptorSet : RuntimeAbi::DescriptorSet);
state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, NativeBinding(stage, binding.kind));
return variable;
};
const auto ArrayType = [&](std::uint32_t type) {
const bool heap = binding.kind == DescriptorBindingKind::Samplers || ImageBindingResourceClass(binding.kind) != ImageResourceClass::None;
const auto count = static_cast<std::uint32_t>(binding.resources.size());
const auto length = heap ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast<std::uint32_t>(binding.kind), count) : ConstantU32(state, count);
const auto length = heap && !state.registrationModules ? state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::HeapCountBase + static_cast<std::uint32_t>(binding.kind), count) : ConstantU32(state, count);
return state.module.Type(spv::OpTypeArray, type, length);
};
switch (binding.kind) {

View File

@ -1250,7 +1250,7 @@ std::uint32_t EmitGetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst)
const auto component = inst.Argument(1)->ImmediateU32();
if (state.inputInfo.vertex == nullptr || !state.inputInfo.vertex->fetchEmbedded || component >= 4u) ctx.Fail(inst, "invalid prepared vertex attribute");
const auto first = PipelineSpecialization::VertexBase + attribute * PipelineSpecialization::VertexWords;
const auto selector = state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u);
const auto selector = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateVertexSelectors + attribute / RuntimeAbi::VertexSelectorsPerDword), ConstantU32(state, attribute % RuntimeAbi::VertexSelectorsPerDword * 16u + component * 3u)), 7u) : state.module.SpecializationConstant(TypeU32(state), first + component, component + 4u);
const auto one = state.module.SpecializationConstant(TypeU32(state), first + 4u, 0x3f800000u);
auto value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, 1u)), one, ConstantU32(state, 0u));
for (std::uint32_t channel = 0; channel < 4u; ++channel) value = Select(state, TypeU32(state), Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, channel + 4u)), EmitAttributeValue(state, attribute, channel), value);
@ -1325,7 +1325,7 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) {
if (exp.index >= 8u) throw std::runtime_error("fragment export target exceeds the runtime mapping table");
std::array<std::uint32_t, 4> components{};
for (std::uint32_t component = 0; component < components.size(); ++component) {
const auto index = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component);
const auto index = state.registrationModules ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, TypeU32(state), EmitShaderDataDwordLoad(state, state.program.Metadata().bindings.RuntimeStateDword() + RuntimeAbi::StateExportMappings + exp.index / 4u), ConstantU32(state, exp.index % 4u * 8u + component * 2u)), 3u) : state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportBase + exp.index * 4u + component, component);
components[component] = state.module.AllocateId();
state.module.AddFunction(spv::OpVectorExtractDynamic, uintOutput ? TypeU32(state) : TypeF32(state), components[component], value, index);
}

View File

@ -57,9 +57,11 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst
const auto* handle = instruction.Argument(0)->Resolve();
if (handle->Opcode() != IrOpcode::GetBufferResource || handle->ArgumentCount() != 4u) context.Fail(instruction, "requires a runtime four-dword V#");
const auto& memory = context.Memory(instruction);
const bool runtime = memory.gpuDescriptor || state.registrationModules;
const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords;
const auto word1 = memory.gpuDescriptor ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u);
const auto word3 = memory.gpuDescriptor ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u);
const auto word1 = runtime ? context.Arg(*handle, 1u) : state.module.SpecializationConstant(TypeU32(state), first, 0u);
const bool runtimeFormat = memory.gpuDescriptor || (state.registrationModules && !state.program.Info().buffers.at(memory.resource).descriptorFormatted);
const auto word3 = runtimeFormat ? context.Arg(*handle, 3u) : state.module.SpecializationConstant(TypeU32(state), first + 1u, 0u);
const auto u32 = TypeU32(state);
auto base = ConstantU32(state, 0u);
if (memory.gpuDescriptor || BufferAccessOf(instruction.Opcode()) == BufferAccess::Atomic) {
@ -82,7 +84,8 @@ RuntimeDescriptor descriptor(SpirvValueEmitContext& context, const IrValue& inst
}
if (memory.gpuDescriptor) faultIf(context, instruction, invalid, base, 0u, BdaAbi::FaultReason::InvalidDescriptor);
const auto stride = field(state, word1, 16u, 14u);
const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u));
const auto present = runtime ? both(state, nonzero(state, context.Arg(*handle, 2u)), Binary(state, spv::OpLogicalOr, TypeBool(state), nonzero(state, context.Arg(*handle, 0u)), nonzero(state, field(state, word1, 0u, 16u)))) : nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u));
const auto valid = memory.gpuDescriptor ? both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), invalid), Binary(state, spv::OpINotEqual, TypeBool(state), base, BdaConstant(state, 0u))) : present;
return {base, context.Arg(*handle, 2u), stride, word3, both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u))), index, valid};
}
@ -140,8 +143,8 @@ MemoryResourceAccess directAccess(SpirvValueEmitContext& context, const IrValue&
std::uint32_t baseMisalignment(SpirvValueEmitContext& context, const IrValue& instruction) {
const auto& memory = context.Memory(instruction);
if (memory.kind == ResourceKind::ScalarBuffer) return ConstantU32(context.state, 0u);
const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords;
return context.state.module.SpecializationConstant(TypeU32(context.state), first + 3u, 0u);
auto& state = context.state;
return state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords + 3u, 0u);
}
std::uint32_t directWordIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t address) {
@ -401,7 +404,7 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr
auto function = state.formattedBufferFunctions.find(key);
if (function == state.formattedBufferFunctions.end()) {
const auto id = state.module.AllocateId();
const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32);
const auto functionType = store ? state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32, valueType) : state.module.Type(spv::OpTypeFunction, resultType, u32, u32, u32, u32, u32, u32, u32, u32, u32);
const auto label = state.currentLabel;
const auto byteOffset = state.memoryByteOffsets.at(element);
struct Restore {
@ -418,21 +421,26 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr
state.module.BeginHelperFunction();
const Restore restore{state, element, label, byteOffset};
state.module.AddFunction(spv::OpFunction, resultType, id, spv::FunctionControlDontInlineMask, functionType);
std::array<std::uint32_t, 6> parameters{};
for (std::uint32_t index = 0; index < (store ? 6u : 5u); ++index) {
std::array<std::uint32_t, 10> parameters{};
for (std::uint32_t index = 0; index < (store ? 10u : 9u); ++index) {
parameters[index] = state.module.AllocateId();
state.module.AddFunction(spv::OpFunctionParameter, index == 5u ? valueType : u32, parameters[index]);
state.module.AddFunction(spv::OpFunctionParameter, index == 9u ? valueType : u32, parameters[index]);
}
EmitLabel(state, state.module.AllocateId());
state.memoryByteOffsets.at(element) = parameters[4];
const auto first = PipelineSpecialization::BufferBase + memory.resource * PipelineSpecialization::BufferWords;
const auto word1 = state.module.SpecializationConstant(u32, first, 0u);
const auto word3 = state.module.SpecializationConstant(u32, first + 1u, 0u);
const auto stride = field(state, word1, 16u, 14u);
const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u)));
const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u));
const RuntimeDescriptor shared{ConstantU32(state, 0u), parameters[3], stride, word3, swizzle, parameters[0], valid, parameters[1], parameters[2]};
const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[5] : 0u);
RuntimeDescriptor shared{};
if (state.registrationModules) {
const auto word3 = state.program.Info().buffers.at(memory.resource).descriptorFormatted ? state.module.SpecializationConstant(u32, first + 1u, 0u) : parameters[6];
shared = {ConstantU32(state, 0u), parameters[3], parameters[5], word3, nonzero(state, parameters[7]), parameters[0], nonzero(state, parameters[8]), parameters[1], parameters[2]};
} else {
const auto word1 = state.module.SpecializationConstant(u32, first, 0u);
const auto stride = field(state, word1, 16u, 14u);
const auto swizzle = both(state, nonzero(state, stride), nonzero(state, field(state, word1, 31u, 1u)));
const auto valid = nonzero(state, state.module.SpecializationConstant(u32, first + 2u, 0u));
shared = {ConstantU32(state, 0u), parameters[3], stride, state.module.SpecializationConstant(u32, first + 1u, 0u), swizzle, parameters[0], valid, parameters[1], parameters[2]};
}
const auto value = formatSwitch(context, instruction, shared, components, store, store ? parameters[9] : 0u);
if (store) state.module.AddFunction(spv::OpReturn);
else state.module.AddFunction(spv::OpReturnValue, value);
state.module.AddFunction(spv::OpFunctionEnd);
@ -440,8 +448,11 @@ std::uint32_t formattedCall(SpirvValueEmitContext& context, const IrValue& instr
}
const auto offset = Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), ConstantU32(state, memory.offset));
const auto result = state.module.AllocateId();
if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), data);
else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element));
const auto flag = [&](std::uint32_t condition) { return Select(state, u32, condition, ConstantU32(state, 1u), ConstantU32(state, 0u)); };
const auto swizzle = flag(resource.swizzle);
const auto valid = flag(resource.valid);
if (store) state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid, data);
else state.module.AddFunction(spv::OpFunctionCall, resultType, result, function->second, resource.index, offset, context.Arg(instruction, 3u), resource.records, state.memoryByteOffsets.at(element), resource.stride, resource.word3, swizzle, valid);
return store ? ConstantU32(state, 0u) : result;
}

View File

@ -91,6 +91,7 @@ flowchart LR
- `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks).
- The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers.
- The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead.
- On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links.
- A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use.
- A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry.
- A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines.

View File

@ -409,6 +409,8 @@ Throughout the project, every function at every stage either **does exactly what
- [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware.
- [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request.
- [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline
- [Shader modules built at registration](../../core/shader/recompiler/Recompiler.cpp) (shader recompiler): on devices with graphics pipeline libraries a vertex or pixel module reads buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors at run time instead of specializing on them per draw, so its libraries can be built when the shader is registered. A module that would exceed `SpirvTarget::registrationWordLimit` (200000 SPIR-V words) is emitted specialized as before: on RADV (Radeon 660M, Mesa 26.2.4) a sample of 60 Astro's Playroom pixel shaders compiled 1.9 to 2.2 times slower in registration form below 141k words and 3.8 to 5.7 times slower above 280k words (none in between), so the limit sits in that gap; it was not measured on other drivers. Texel-offset folding, the format word of a buffer accessed with the descriptor's format (`buffer_load_format_*`; as runtime values every format's decoder is compiled, which made CI's lavapipe JIT ten times slower) and the vertex attribute numeric class stay specialized, so shaders with the first two, and pixel shaders whose image needs a multisampled descriptor, get no registration result; these, and every draw whose fixed-function state differs from the prebuild's, still build their libraries at the draw. Compute and the other stages stay specialized: in registration form the `agc_driver_dword_pattern_fill` compute shader hung CI's lavapipe (Mesa 25.2.8) when built with `ANYPS5_ENABLE_SPIRV_TOOLS=ON`, not on RADV, and the cause was not found.
- [Prebuilt pipeline libraries](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver): each registered vertex or pixel shader in registration form gets its pre-rasterization or fragment-shader library built on one background thread, with a fixed state the draw may not use: triangle list, counter-clockwise front face, no depth clamp, depth bias or conservative rasterization, first provoking vertex, one sample. Libraries are kept until the device is torn down; on Astro's Playroom 1948 of them added 0.44 GB of peak memory on RADV.
- [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic
- [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc/<pid>/limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy
- On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it.