Files
AnyPS5/core/shader/recompiler/Recompiler.cpp
T
NikoandClaude Opus 5.5 16ea32c97d feat(agc): blend into color targets with a reversed component order
8_8_8_8 and 16_16_16_16 targets with COMP_SWAP 2 or 3 keep the exported
alpha in their first channel and red or blue in their fourth, so a plain
Vulkan blend would run the alpha equation on a color channel. Such draws
threw "blending into a color target with a reversed component order is
not implemented" (Gran Turismo 7, #4782).

The AMD SI and CIK register references describe CB_TARGET_MASK in the
order of components after blending and before COMP_SWAP is applied, and
RADV programs CB_BLENDn_CONTROL without regard to the swap while it
renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 through SWAP_STD_REV
and SWAP_ALT_REV and passes the CTS blend tests on Navi10. The color
block is therefore taken to blend in export order: the alpha equation
and source-alpha factors use the exported alpha and the destination
alpha is the first channel.

TranslateReversedBlend keeps the factor pairs Vulkan applies per channel
in the pipeline. Other source factors are computed by the pixel shader
from its MRT0 export (clamped for UNORM targets, as BLEND_CLAMP says)
and handed to the blend as a dual-source second color, which the
fragment output post-pass adds at location 0 index 1; when both
positions need it, the export is premultiplied by its source factor.
The code is a pipeline specialization, serialized in request version
26, and stays out of the prepared shader key.

sRGB, SNORM and integer targets, constant and second-source factors,
destination alpha in the color equation, destination factors the two
equations do not share, different color and alpha operations, MIN or
MAX with factors other than ONE, other color exports when the shader
computes factors, and devices without dualSrcBlend still throw, now
with the blend control, mapping and format in the message.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 12:51:27 +02:00

1500 lines
81 KiB
C++

#include "Recompiler.hpp"
#include "StencilReplay.hpp"
#include <algorithm>
#include <cstring>
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstdio>
#include "CacheKey.hpp"
#include "CompiledVariant.hpp"
#include "VertexInputSpecialization.hpp"
#include "FragmentOutputSpecialization.hpp"
#include "SpirvBackend/SpirvSpecialization.hpp"
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
#include "ShaderDiskCache.hpp"
#include <list>
#include <functional>
#include <map>
#include <set>
#include <mutex>
#include <new>
#include <shared_mutex>
#include <unordered_map>
#include "ControlFlow/include/ControlFlow/GraphBuilder.hpp"
#include "ControlFlow/include/ControlFlow/Structurizer.hpp"
#include "RdnaDecoder/include/RdnaDecoder/RdnaInstructionDecoder.hpp"
#include "IntermediateRepresentation/include/IntermediateRepresentation/IrProgram.hpp"
#include "Optimization/include/Optimization/BindingAllocator.hpp"
#include "Optimization/include/Optimization/ConstantFolder.hpp"
#include "Optimization/include/Optimization/DeadCodeEliminator.hpp"
#include "Optimization/include/Optimization/DenormalFlushEliminator.hpp"
#include "Optimization/include/Optimization/DescriptorBindingBuilder.hpp"
#include "Optimization/include/Optimization/HostInterpolationChecker.hpp"
#include "Optimization/include/Optimization/MaskedSelectEliminator.hpp"
#include "Optimization/include/Optimization/ReadLaneEliminator.hpp"
#include "Optimization/include/Optimization/RedundantValueEliminator.hpp"
#include "Optimization/include/Optimization/RequestMemoryView.hpp"
#include "Optimization/include/Optimization/ResourceMaterializer.hpp"
#include "Optimization/ResourceProgram.hpp"
#include "Optimization/include/Optimization/ResourceTracker.hpp"
#include "Optimization/include/Optimization/ShaderInfoCollector.hpp"
#include "Optimization/include/Optimization/SrtWalker.hpp"
#include "Optimization/include/Optimization/SsaBuilder.hpp"
#include "Optimization/include/Optimization/TessellationLowering.hpp"
#include "SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp"
#if ANYPS5_ENABLE_SPIRV_TOOLS
#include "SpirvBackend/SpirvOptimizer.hpp"
#endif
#include "SpirvBackend/SpirvMemory/SpirvInputOutput.hpp"
#include "Translation/include/Translation/InstructionTranslator.hpp"
#include "Translation/include/Translation/ShaderInputInfoBuilder.hpp"
#include <exception>
#include <stdexcept>
#include <string>
#include <ControlFlow/RequestSerializer.hpp>
namespace ShaderRecompiler {
namespace {
ShaderStageKind toShaderStageKind(ShaderStage stage) {
switch (stage) {
case ShaderStage::Compute:
return ShaderStageKind::Compute;
case ShaderStage::Vertex:
return ShaderStageKind::Vertex;
case ShaderStage::TessellationControl:
return ShaderStageKind::TessellationControl;
case ShaderStage::TessellationEvaluation:
return ShaderStageKind::TessellationEvaluation;
case ShaderStage::Fragment:
return ShaderStageKind::Pixel;
case ShaderStage::Local:
return ShaderStageKind::Local;
case ShaderStage::Mesh:
return ShaderStageKind::Mesh;
case ShaderStage::Geometry:
break;
}
throw std::runtime_error("ShaderRecompiler::Recompile: unsupported shader stage");
}
}
namespace {
// The host subgroup width wave64 programs are laid out for. Debug aid: APS5_SINGLE_LANE=<hex code
// addresses, comma separated, or "all"> keeps the listed programs at one guest lane per invocation.
std::uint32_t HostSubgroupSize(const RecompileRequest& request) {
const bool vertexStage = request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Local;
if (vertexStage && !request.target.vertexSubgroups) return 1u;
static const std::string list = [] { const char* text = std::getenv("APS5_SINGLE_LANE"); return text ? std::string(text) : std::string(); }();
if (!list.empty()) {
if (list == "all") return 64u;
char address[32];
std::snprintf(address, sizeof(address), "%llx", static_cast<unsigned long long>(request.shader.codeAddress));
if (list.find(address) != std::string::npos) return 64u;
}
return request.target.subgroupSize;
}
RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::span<const std::uint32_t> code) {
if (stage != ShaderStageKind::Local) return RdnaInstructionDecoder{}.Decode(code);
auto program = DecodeRdnaFrontProgram(code);
static constexpr std::array<std::uint32_t, 1> EndProgram{0xbf810000u};
program.instructions.back() = DecodeRdnaInstruction(program.instructions.back().programCounter, EndProgram, 0u);
return program;
}
ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) {
const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr;
std::optional<TessellationConfiguration> tessellationCopy;
if (request.graphics && request.graphics->tessellation) {
tessellationCopy = *request.graphics->tessellation;
tessellationCopy->instancePatches = TessellationInstancePatches(request);
const auto& capabilities = request.target.supportedCapabilities;
tessellationCopy->drawParameters = std::find(capabilities.begin(), capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityDrawParameters)) != capabilities.end();
}
const auto* tessellation = tessellationCopy ? &*tessellationCopy : nullptr;
return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh, tessellation);
}
std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) {
if (!request.context.compute.has_value()) return 0u;
const auto bytes = static_cast<std::uint64_t>(request.context.compute->ldsSizeDwords) * 4u;
return bytes + 4u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast<std::uint32_t>(bytes) : 0u;
}
std::atomic<std::uint64_t> frontEndBuilds{0};
}
struct PreparedControlFlow {
PreparedControlFlow(const RecompileRequest& request, const SwappcInfo& swappcInfo)
: stage(request.shader.stage), fallbacks(swappcInfo.linkedCallFallbacks.begin(), swappcInfo.linkedCallFallbacks.end()), swappc(WithFallbacks(swappcInfo, fallbacks)), code(request.shader.code.begin(), request.shader.code.end()),
decoded(DecodeStageProgram(toShaderStageKind(request.shader.stage), code)), cfg(GraphBuilder{}.Build(decoded, &swappc)) {
Structurizer{}.Structurize(cfg);
}
PreparedControlFlow(const PreparedControlFlow&) = delete;
PreparedControlFlow& operator=(const PreparedControlFlow&) = delete;
bool Matches(const RecompileRequest& request, const SwappcInfo& swappcInfo) const {
return stage == request.shader.stage && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed &&
swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount &&
std::ranges::equal(fallbacks, swappcInfo.linkedCallFallbacks) && std::ranges::equal(code, request.shader.code);
}
static SwappcInfo WithFallbacks(SwappcInfo info, const std::vector<std::uint32_t>& fallbacks) {
info.linkedCallFallbacks = fallbacks;
return info;
}
ShaderStage stage;
std::vector<std::uint32_t> fallbacks;
SwappcInfo swappc;
std::vector<std::uint32_t> code;
RdnaProgram decoded;
ControlFlowGraph cfg;
};
std::shared_ptr<const PreparedControlFlow> ShaderPreparationContext::AcquireFrontend(const RecompileRequest& request) {
const auto inputInfo = RequestInputInfo(request);
const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast<std::uint32_t>(request.context.userData.size()), 0u, request.context.linkedCallFallbacks};
static const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr;
if (reuse && frontend != nullptr && frontend->Matches(request, swappc)) {
return frontend;
}
auto prepared = std::make_shared<const PreparedControlFlow>(request, swappc);
if (reuse) {
frontend = prepared;
}
return prepared;
}
std::uint64_t FrontEndBuilds() {
return frontEndBuilds.load(std::memory_order_relaxed);
}
std::uint32_t ShaderPreparationContext::ResourcePrograms() const {
return resourcePrograms;
}
IrProgram PrepareResourceProgram(const RecompileRequest& request) {
return PrepareResourceProgram(request, nullptr);
}
IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation) {
frontEndBuilds.fetch_add(1, std::memory_order_relaxed);
ShaderPreparationContext local;
auto& context = preparation != nullptr ? *preparation : local;
++context.resourcePrograms;
context.interfaceSource.reset();
context.interfaceProgram.reset();
const auto frontend = context.AcquireFrontend(request);
const auto stageKind = toShaderStageKind(request.shader.stage);
const auto inputInfo = RequestInputInfo(request);
const auto& decoded = frontend->decoded;
const auto& cfg = frontend->cfg;
TranslateOptions translateOptions {};
translateOptions.stage = stageKind;
translateOptions.shaderHash = request.shader.codeAddress;
translateOptions.waveSize = request.context.waveSize;
translateOptions.userDataBaseRegister = request.context.userDataBaseRegister;
translateOptions.userDataCount = static_cast<std::uint32_t>(request.context.userData.size());
translateOptions.scratchDwords = request.context.compute.has_value() ? request.context.compute->scratchDwords : 0u;
translateOptions.sharedMemoryBytes = DeviceMemoryLdsBytes(request);
translateOptions.fragmentShaderBarycentricEnabled = request.target.fragmentShaderBarycentricEnabled;
const auto supports = [&](spv::Capability capability) { return std::ranges::find(request.target.supportedCapabilities, static_cast<std::uint32_t>(capability)) != request.target.supportedCapabilities.end(); };
translateOptions.nativeFloat16 = supports(spv::CapabilityFloat16) && supports(spv::CapabilityRoundingModeRTE) && supports(spv::CapabilityDenormPreserve);
translateOptions.floatMode = request.context.floatMode;
translateOptions.inputInfo = inputInfo;
constexpr InstructionTranslator translator;
EmbeddedFetchPlan embeddedFetch;
if ((stageKind == ShaderStageKind::Vertex || stageKind == ShaderStageKind::Local) && inputInfo.vertex != nullptr && inputInfo.vertex->fetchEmbedded) {
embeddedFetch = EmbeddedVertexFetchAnalyzer{}.Analyze(decoded, inputInfo.vertex->fetchAttribReg, inputInfo.vertex->fetchBufferReg, request.context.userDataBaseRegister, static_cast<std::uint32_t>(request.context.userData.size()), request.context.waveSize);
translateOptions.embeddedFetch = &embeddedFetch;
}
auto program = translator.Translate(decoded, cfg, translateOptions);
// Debug aid: APS5_DUMP_IR=<hex code address> (or "all") prints the program after each front-end pass.
const auto dumpIr = [&](const char* pass) {
static const std::string list = [] { const char* text = std::getenv("APS5_DUMP_IR"); return text ? std::string(text) : std::string(); }();
if (list.empty()) return;
char address[32];
std::snprintf(address, sizeof(address), "%llx", static_cast<unsigned long long>(request.shader.codeAddress));
if (list != "all" && list.find(address) == std::string::npos) return;
std::fprintf(stderr, "==== IR 0x%s after %s\n%s\n", address, pass, ProgramToString(program).c_str());
};
constexpr SsaBuilder ssaBuilder;
constexpr ConstantFolder constantFolder;
constexpr DeadCodeEliminator deadCodeEliminator;
constexpr ReadLaneEliminator readLaneEliminator;
constexpr MaskedSelectEliminator maskedSelectEliminator;
const auto simplify = [&] {
dumpIr("translate");
ssaBuilder.Rewrite(program);
dumpIr("ssa");
constantFolder.Fold(program);
ResolveControlFlowIdentities(program);
deadCodeEliminator.RemoveIdentities(program);
deadCodeEliminator.Eliminate(program);
dumpIr("fold");
const auto readLaneStats = readLaneEliminator.Eliminate(program, translateOptions.waveSize);
if (readLaneStats.rewrittenReads != 0u) {
constantFolder.Fold(program);
ResolveControlFlowIdentities(program);
deadCodeEliminator.RemoveIdentities(program);
deadCodeEliminator.Eliminate(program);
}
if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) {
const bool liveAncillary = std::ranges::any_of(program.Blocks(), [](const auto& block) {
return std::ranges::any_of(block->Instructions(), [](const IrValue* value) {
return value->Opcode() == IrOpcode::GetBuiltin && value->HasUses() && value->Argument(0)->Resolve()->ImmediateU32() == static_cast<std::uint32_t>(StageInputKind::PackedAncillary);
});
});
if (liveAncillary) {
constantFolder.Fold(program);
ResolveControlFlowIdentities(program);
deadCodeEliminator.RemoveIdentities(program);
}
deadCodeEliminator.Eliminate(program);
}
if (constantFolder.ComposePackedAncillary(program)) {
deadCodeEliminator.Eliminate(program);
}
};
simplify();
if (stageKind == ShaderStageKind::Pixel && !translateOptions.fragmentShaderBarycentricEnabled && !HostInterpolationChecker{}.Lower(program, *inputInfo.pixel)) {
translateOptions.fragmentShaderBarycentricEnabled = true;
program = translator.Translate(decoded, cfg, translateOptions);
program.Metadata().barycentricEmulation = true;
simplify();
}
constexpr DenormalFlushEliminator denormalFlushEliminator;
if (denormalFlushEliminator.Eliminate(program).removedFlushes != 0u) {
deadCodeEliminator.Eliminate(program);
}
if (inputInfo.vertex != nullptr) {
constexpr TessellationLowering tessellationLowering;
tessellationLowering.Lower(program, inputInfo.vertex->tess);
}
constexpr SrtWalker srtWalker;
srtWalker.BuildPlan(program);
deadCodeEliminator.Eliminate(program);
dumpIr("srt");
constexpr ResourceTracker resourceTracker;
resourceTracker.Track(program);
deadCodeEliminator.Eliminate(program);
dumpIr("resources");
program.Resources().srgbDecodeFormats = request.target.srgbDecodeFormats;
return program;
}
// A materialized result of one variant over one snapshot (Recompile(request, capture)): the
// shared immutable object every later capture that reproduces the snapshot receives, so Populate
// and the per-request copy run once per distinct snapshot.
struct ResultMemoEntry {
std::uint64_t variantId;
std::uint64_t hash;
std::shared_ptr<const RecompileResult> result;
};
struct EmissionFailure {
std::uint64_t codeAddress;
BindingLayout layout;
std::exception_ptr failure;
};
struct SourceEntry : std::enable_shared_from_this<SourceEntry> {
std::mutex mutex;
// The code the entry was built for: the key carries only a hash of it, so a candidate entry is
// accepted only when its code matches word for word. Owned here because the request's span
// points into a registration the driver may replace while the entry lives on.
std::vector<std::uint32_t> code;
std::vector<std::uint32_t> linkedCallFallbacks;
std::shared_ptr<const IrResourcePlan> plan;
// A plan build that threw (an unsupported resource chain or control flow) is remembered and
// rethrown: the front end ran every pass before failing, ~13 ms per dispatch of a shader the
// title issues every frame (0x1048947300 at the intro video). APS5_NO_FAILURE_MEMO=1 rebuilds.
std::exception_ptr planFailure;
std::unique_ptr<IrProgram> program;
std::vector<std::shared_ptr<const CompiledVariant>> variants;
std::vector<EmissionFailure> emissionFailures;
// The result memo, most recently used first, at most ResultMemoEntries (under mutex).
std::list<ResultMemoEntry> memo;
std::unordered_map<std::uint64_t, std::list<ResultMemoEntry>::iterator> memoIndex;
};
namespace {
class PrimedSharedMutex : public std::shared_mutex {
public:
PrimedSharedMutex() {
lock();
unlock();
}
};
struct ResourceProgram {
explicit ResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr, bool reclaim = true) : program(std::make_unique<IrProgram>(PrepareResourceProgram(request, preparation))) {
const auto clearRegisterState = [](const auto& storage) {
for (const auto& block : storage) {
block->ssaScalarValues.fill(nullptr);
block->ssaVectorValues.fill(nullptr);
block->ssaThreadBitScalarValues.fill(nullptr);
block->ssaScalarMaskTags.fill(nullptr);
}
};
clearRegisterState(program->Blocks());
clearRegisterState(program->Resources().blockStorage);
if (reclaim) program->ReclaimUnusedValues();
plan = std::make_shared<const IrResourcePlan>(ResourceMaterializer{}.ExtractPlan(*program));
}
std::unique_ptr<IrProgram> program;
std::shared_ptr<const IrResourcePlan> plan;
};
std::shared_ptr<const IrResourcePlan> makeResourcePlan(const RecompileRequest& request) {
return ResourceProgram(request, nullptr, false).plan;
}
struct SourceKeyHash {
std::size_t operator()(const std::vector<std::uint64_t>& key) const {
std::size_t hash = 0;
for (const auto value : key) {
hash ^= static_cast<std::size_t>(value) + static_cast<std::size_t>(0x9e3779b97f4a7c15ull) + (hash << 6u) + (hash >> 2u);
if constexpr (sizeof(std::size_t) < sizeof(value)) hash ^= static_cast<std::size_t>(value >> 32u);
}
return hash;
}
};
bool FailureMemo() {
static const bool memo = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr;
return memo;
}
bool diskPlans() {
static const bool enabled = [] {
const char* dumpIr = std::getenv("APS5_DUMP_IR");
return std::getenv("APS5_NO_PERF_DISK_PLANS") == nullptr && (dumpIr == nullptr || *dumpIr == '\0');
}();
return enabled && ShaderDiskCache::Enabled() && !DebugProbeActive();
}
std::shared_ptr<SourceEntry> getSource(const RecompileRequest& request, ShaderPreparationContext* preparation = nullptr, bool reclaim = true) {
static PrimedSharedMutex mutex;
// Entries whose code hashes alike share a bucket; the code comparison picks the right one.
static std::unordered_map<std::vector<std::uint64_t>, std::vector<std::shared_ptr<SourceEntry>>, SourceKeyHash> sources;
struct SourceKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, SourceKeyStorage>();
RecompileCacheKey::Build(request, key);
key.push_back(HostSubgroupSize(request));
const auto find = [&]() -> std::shared_ptr<SourceEntry> {
const auto found = sources.find(key);
if (found == sources.end()) return nullptr;
for (const auto& entry : found->second) {
if (std::equal(entry->code.begin(), entry->code.end(), request.shader.code.begin(), request.shader.code.end())) return entry;
}
return nullptr;
};
std::shared_ptr<SourceEntry> source;
{
std::shared_lock lock(mutex);
source = find();
}
if (source == nullptr) {
std::unique_lock lock(mutex);
source = find();
if (source == nullptr) {
source = std::make_shared<SourceEntry>();
source->code.assign(request.shader.code.begin(), request.shader.code.end());
source->linkedCallFallbacks.assign(request.context.linkedCallFallbacks.begin(), request.context.linkedCallFallbacks.end());
auto& bucket = sources[key];
if (!bucket.empty()) {
// A second entry under one key is a code hash collision (or the unhashed key with
// identical code, which cannot happen); each is reported under the profile switch.
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
static std::uint64_t collisions = 0;
++collisions;
if (profile) std::fprintf(stderr, "[recompile] source key collision %llu: %zu entries share a key (%zu code words)\n", static_cast<unsigned long long>(collisions), bucket.size() + 1, request.shader.code.size());
}
bucket.push_back(source);
}
}
{
std::lock_guard lock(source->mutex);
if (source->plan == nullptr) {
if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure);
const bool disk = diskPlans();
std::vector<std::byte> planKey;
if (disk) {
ShaderDiskCache::BuildPlanKey(request, HostSubgroupSize(request), planKey);
IrResourcePlan loaded;
if (ShaderDiskCache::LoadPlan(planKey, loaded)) {
loaded.shaderHash = request.shader.codeAddress;
source->plan = std::make_shared<const IrResourcePlan>(std::move(loaded));
}
}
if (source->plan == nullptr) {
try {
ResourceProgram resource(request, preparation, reclaim);
source->plan = std::move(resource.plan);
source->program = std::move(resource.program);
} catch (...) {
if (FailureMemo()) source->planFailure = std::current_exception();
throw;
}
if (disk) ShaderDiskCache::StorePlan(std::move(planKey), source->plan);
}
}
}
return source;
}
std::array<std::uint32_t, 3> partialThreads(const RecompileRequest& request) {
return request.context.compute ? request.context.compute->partialThreads : std::array<std::uint32_t, 3>{};
}
std::uint64_t nextVariantId() {
static std::atomic<std::uint64_t> variants{0};
return variants.fetch_add(1, std::memory_order_relaxed) + 1;
}
template<typename TStep>
auto recordEmissionFailure(std::exception_ptr* emissionFailure, TStep&& step) -> decltype(step()) {
try {
return step();
} catch (const std::bad_alloc&) {
throw;
} catch (...) {
if (emissionFailure != nullptr) *emissionFailure = std::current_exception();
throw;
}
}
void prepareInterface(const RecompileRequest& request, IrProgram& program) {
const auto inputInfo = RequestInputInfo(request);
constexpr DeadCodeEliminator deadCodeEliminator;
constexpr ResourceMaterializer resourceMaterializer;
const bool nativeSampleOffsets = request.target.nonConstantImageOffsets && std::find(request.target.supportedCapabilities.begin(), request.target.supportedCapabilities.end(), spv::CapabilityImageGatherExtended) != request.target.supportedCapabilities.end();
resourceMaterializer.ApplyStaticInterface(program, nativeSampleOffsets);
deadCodeEliminator.RemoveIdentities(program);
constexpr RedundantValueEliminator redundantValueEliminator;
static_cast<void>(redundantValueEliminator.Eliminate(program));
deadCodeEliminator.Eliminate(program);
constexpr ShaderInfoCollector shaderInfoCollector;
shaderInfoCollector.Collect(program, inputInfo);
}
CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) {
const auto inputInfo = RequestInputInfo(request);
constexpr BindingAllocator bindingAllocator;
bool registration = request.target.registrationModules && (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Fragment);
program.Metadata().bindingLayoutComplete = false;
const bool stencilReplay = request.shader.stage == ShaderStage::Fragment && std::ranges::any_of(program.Info().outputs, [](const StageOutput& output) { return output.kind == StageOutputKind::StencilRef; }) && std::ranges::find(request.target.supportedCapabilities, static_cast<std::uint32_t>(spv::CapabilityStencilExportEXT)) == request.target.supportedCapabilities.end();
auto bindings = bindingAllocator.Allocate(program, request.layout, registration, stencilReplay);
SpirvTargetOptions targetOptions {};
targetOptions.vulkanVersion = request.target.vulkanVersion;
targetOptions.spirvVersion = request.target.spirvVersion;
targetOptions.subgroupSize = HostSubgroupSize(request) == 1u ? 1u : request.target.subgroupSize;
targetOptions.bdaAbiVersion = request.target.bdaAbiVersion;
targetOptions.supportedCapabilities = request.target.supportedCapabilities;
targetOptions.supportedExtensions = request.target.supportedExtensions;
targetOptions.nonConstantImageOffsets = request.target.nonConstantImageOffsets;
targetOptions.narrowSubgroupClock = request.target.narrowSubgroupClock;
targetOptions.float64DenormPreserve = request.target.float64DenormPreserve;
targetOptions.registrationModules = registration;
targetOptions.fragmentDescriptorSet = request.target.registrationModules;
targetOptions.float16DenormPreserve = request.target.float16DenormPreserve;
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, stencilReplay);
targetOptions.registrationModules = false;
result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions);
}
result.registrationModule = registration;
result.stencilReplaySafe = (request.shader.stage == ShaderStage::Vertex || stencilReplay) && StencilReplaySafe(program);
if (stencilReplay) {
result.stencilReferences = StencilReferenceValues(program);
result.stencilReferenceDword = bindings.layout.stencilReferenceDword;
}
result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u;
result.memoryOffsetDword = bindings.layout.memoryOffsetDword;
result.shaderDataDwords = bindings.layout.ShaderDataDwords();
result.imageMetadataDword = bindings.layout.ImageMetadataDword();
result.runtimeImageCount = bindings.layout.runtimeImageCount;
result.hostSubgroupSize = HostSubgroupSize(request);
result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr;
result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr;
result.vertexOffsetShared = program.Info().vertexOffsetShared;
result.instanceOffsetShared = program.Info().instanceOffsetShared;
result.vertexOffsetConflict = program.Info().vertexOffsetConflict;
result.instanceOffsetConflict = program.Info().instanceOffsetConflict;
for (const auto& output : program.Info().outputs) {
if (output.kind == StageOutputKind::Parameter) result.parameterExports.push_back(output.location);
}
if (request.shader.stage == ShaderStage::Fragment) {
result.fragmentParameters = DescribeFragmentParameters(program, inputInfo);
const auto& inputs = program.Info().inputs;
const auto reads = [&](StageInputKind kind) { return std::any_of(inputs.begin(), inputs.end(), [&](const auto& input) { return input.kind == kind; }); };
if (program.Metadata().barycentricEmulation) result.barycentricEmulation = {true, reads(StageInputKind::BaryCoordSmooth), reads(StageInputKind::BaryCoordNoPerspective)};
}
if (request.shader.stage == ShaderStage::Vertex || request.shader.stage == ShaderStage::Local) {
if (inputInfo.vertex == nullptr) throw std::runtime_error("vertex input metadata is missing");
for (const auto& input : program.Info().inputs) {
if (input.kind != StageInputKind::Parameter) continue;
if (input.location >= static_cast<std::uint32_t>(inputInfo.vertex->resourcesNum)) throw std::runtime_error("vertex attribute location exceeds resource count");
result.vertexInputs.push_back({input.location, input.componentCount, inputInfo.vertex->resourcesDst[input.location].fetchIndex});
if (inputInfo.vertex->fetchEmbedded) {
for (const auto& block : program.Blocks()) {
for (const auto* instruction : block->Instructions()) {
if (instruction->Opcode() == IrOpcode::GetAttribute && instruction->Argument(0)->Resolve()->ImmediateU32() == input.location) result.vertexInputs.back().outputMask |= 1u << instruction->Argument(1)->Resolve()->ImmediateU32();
}
}
}
}
}
const bool embedded = !result.vertexInputs.empty() && inputInfo.vertex != nullptr && inputInfo.vertex->fetchEmbedded;
if (embedded) PrepareVertexInputSpecialization(result);
#if ANYPS5_ENABLE_SPIRV_TOOLS
result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets, !embedded);
if (embedded) {
std::array<std::uint32_t, ShaderVertexStageInfo::MaxResources> classes{};
for (std::uint32_t kind = 1u; kind < 3u; ++kind) {
classes.fill(kind);
const auto specialized = SpecializeVertexInputTypes(result, classes);
static_cast<void>(ValidateAndOptimizeSpirv(specialized, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets, false));
}
}
#endif
return {request.layout, {}, std::move(static_cast<CompiledBindingLayout&>(bindings)), std::move(result)};
}
CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, std::exception_ptr* emissionFailure = nullptr) {
return recordEmissionFailure(emissionFailure, [&] {
prepareInterface(request, program);
auto variant = emitVariant(request, program);
variant.info = std::move(program).TakeCompiledInfo();
return variant;
});
}
struct SpecializedModule {
std::uint64_t specializationId = 0;
SharedSpirv spirv;
std::vector<std::uint32_t> bindings;
bool pushData = false;
};
std::shared_ptr<const SpecializedModule> buildSpecializedModule(const CompiledShaderArtifact& artifact, std::span<const std::uint32_t> classes, std::span<const PipelineSpecializationConstant> constants, const SpirvTarget& target) {
auto source = SpecializeVertexInputTypes(artifact, classes);
std::map<std::uint32_t, std::uint32_t> supplied;
for (const auto& constant : constants) {
if (!supplied.emplace(constant.id, constant.value).second) throw std::runtime_error("duplicate prepared specialization ID");
}
std::map<std::uint32_t, std::uint32_t> values;
std::set<std::uint32_t> specializedIds;
std::set<std::uint32_t> pushTypes;
std::optional<std::uint32_t> replaySlotBytes;
const auto& words = source.Words();
if (words.size() < 5u || words[0] != spv::MagicNumber) throw std::runtime_error("invalid prepared specialization module");
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
const auto op = static_cast<spv::Op>(words[cursor] & 0xffffu);
if (count == 0u || count > words.size() - cursor) throw std::runtime_error("truncated prepared specialization instruction");
if (!artifact.stencilReferences.empty() && op == spv::OpTypePointer && count == 4u && words[cursor + 2u] == spv::StorageClassPushConstant) pushTypes.insert(words[cursor + 3u]);
if (op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId) {
const auto found = supplied.find(words[cursor + 3u]);
if (found == supplied.end()) throw std::runtime_error("missing prepared specialization value");
auto value = found->second;
if (!artifact.stencilReferences.empty()) {
if (found->first == PipelineSpecialization::StencilReferenceOffset) replaySlotBytes = value / PushData::DwordCount * NativePushSlotSize;
if (found->first == PipelineSpecialization::StencilReferenceOffset || found->first == PipelineSpecialization::PushDataOffset) value %= PushData::DwordCount;
}
if (!values.emplace(words[cursor + 1u], value).second) throw std::runtime_error("duplicate prepared specialization value");
specializedIds.insert(found->first);
}
cursor += count;
}
std::vector<std::uint32_t> materialized(words.begin(), words.begin() + 5);
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
const auto op = static_cast<spv::Op>(words[cursor] & 0xffffu);
if (op == spv::OpSpecConstant) {
if (count != 4u || !values.contains(words[cursor + 2u])) throw std::runtime_error("invalid prepared specialization constant");
materialized.insert(materialized.end(), {(4u << 16u) | spv::OpConstant, words[cursor + 1u], words[cursor + 2u], values.at(words[cursor + 2u])});
} else if (op == spv::OpMemberDecorate && count == 5u && words[cursor + 3u] == spv::DecorationOffset && pushTypes.contains(words[cursor + 1u])) {
if (!replaySlotBytes || words[cursor + 2u] != 0u || *replaySlotBytes >= NativePushConstantSize) throw std::runtime_error("invalid stencil replay push constant slot");
materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count - 1u);
materialized.push_back(*replaySlotBytes);
} else if (!(op == spv::OpDecorate && count == 4u && words[cursor + 2u] == spv::DecorationSpecId)) {
materialized.insert(materialized.end(), words.begin() + cursor, words.begin() + cursor + count);
}
cursor += count;
}
materialized = SpecializeFragmentOutputs(SpecializeSpirv(materialized), constants, specializedIds);
#if ANYPS5_ENABLE_SPIRV_TOOLS
materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, true, true);
#endif
std::map<std::uint32_t, std::uint32_t> descriptorVariables;
std::set<std::uint32_t> usedDescriptors;
for (std::size_t cursor = 5; cursor < materialized.size();) {
const auto count = materialized[cursor] >> 16u;
const auto op = static_cast<spv::Op>(materialized[cursor] & 0xffffu);
if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationBinding) descriptorVariables.emplace(materialized[cursor + 1u], materialized[cursor + 3u]);
if ((op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain || op == spv::OpPtrAccessChain || op == spv::OpInBoundsPtrAccessChain || op == spv::OpLoad || op == spv::OpCopyObject) && count >= 4u) usedDescriptors.insert(materialized[cursor + 3u]);
cursor += count;
}
const auto unused = [&](std::uint32_t id) { return descriptorVariables.contains(id) && !usedDescriptors.contains(id); };
std::vector<std::uint32_t> compact(materialized.begin(), materialized.begin() + 5);
for (std::size_t cursor = 5; cursor < materialized.size();) {
const auto count = materialized[cursor] >> 16u;
const auto op = static_cast<spv::Op>(materialized[cursor] & 0xffffu);
if (op == spv::OpEntryPoint) {
const auto start = compact.size();
std::size_t interfaceIndex = 3u;
for (; interfaceIndex < count; ++interfaceIndex) {
const auto word = materialized[cursor + interfaceIndex];
if ((word & 0xffu) == 0u || (word & 0xff00u) == 0u || (word & 0xff0000u) == 0u || (word & 0xff000000u) == 0u) { ++interfaceIndex; break; }
}
compact.insert(compact.end(), materialized.begin() + cursor, materialized.begin() + cursor + interfaceIndex);
for (auto index = interfaceIndex; index < count; ++index) if (!unused(materialized[cursor + index])) compact.push_back(materialized[cursor + index]);
compact[start] = (static_cast<std::uint32_t>(compact.size() - start) << 16u) | spv::OpEntryPoint;
} else if (!((op == spv::OpVariable && unused(materialized[cursor + 2u])) || ((op == spv::OpDecorate || op == spv::OpName) && unused(materialized[cursor + 1u])))) {
compact.insert(compact.end(), materialized.begin() + cursor, materialized.begin() + cursor + count);
}
cursor += count;
}
materialized = std::move(compact);
#if ANYPS5_ENABLE_SPIRV_TOOLS
materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false);
#endif
auto module = std::make_shared<SpecializedModule>();
std::map<std::uint32_t, std::uint32_t> bindingNumbers;
for (std::size_t cursor = 5; cursor < materialized.size();) {
const auto count = materialized[cursor] >> 16u;
const auto op = static_cast<spv::Op>(materialized[cursor] & 0xffffu);
if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationBinding) bindingNumbers.emplace(materialized[cursor + 1u], materialized[cursor + 3u]);
if (op == spv::OpVariable && count >= 4u) {
if (const auto found = bindingNumbers.find(materialized[cursor + 2u]); found != bindingNumbers.end()) module->bindings.push_back(found->second);
module->pushData |= materialized[cursor + 3u] == spv::StorageClassPushConstant;
}
cursor += count;
}
module->spirv = std::move(materialized);
module->specializationId = constants.empty() ? 0u : nextVariantId();
return module;
}
struct SpecializedModuleEntry {
std::once_flag ready;
std::exception_ptr failure;
std::shared_ptr<const SpecializedModule> module;
};
struct PreparedModuleEntry {
std::once_flag ready;
std::exception_ptr failure;
std::shared_ptr<const SpecializedModule> module;
DescriptorBindingPlan bindings;
};
struct PreparedBindingPlan {
std::once_flag ready;
std::exception_ptr failure;
DescriptorBindingPlan bindings;
PrimedSharedMutex mutex;
std::map<std::vector<std::uint32_t>, std::shared_ptr<PreparedModuleEntry>> modules;
};
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 PrimedSharedMutex 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();
key.push_back(variant.artifact.variantId);
for (std::size_t index = 0; index < variant.info.info.buffers.size(); ++index) {
const auto& descriptor = snapshot.buffers.at(index);
key.push_back(descriptor.dwordCount);
key.push_back(descriptor.dwords[1] & 0xffff0000u);
key.push_back(descriptor.dwords[3]);
const bool present = descriptor.dwords[2] != 0u && (descriptor.dwords[0] != 0u || (descriptor.dwords[1] & 0xffffu) != 0u);
key.push_back(present ? 4u | (descriptor.dwords[0] & 3u) : 0u);
}
for (std::size_t index = 0; index < variant.info.info.images.size(); ++index) {
const auto& descriptor = snapshot.images.at(index);
key.push_back(descriptor.dwordCount);
key.push_back(descriptor.dwords[0] != 0u || (descriptor.dwords[1] & 0xffu) != 0u);
key.push_back(descriptor.dwords[1] & ~0xffu);
for (std::size_t word = 2; word < 8; ++word) key.push_back(descriptor.dwords[word]);
}
for (std::size_t index = 0; index < variant.info.info.samplers.size(); ++index) {
const auto& descriptor = snapshot.samplers.at(index);
key.push_back(descriptor.dwordCount);
for (std::size_t word = 0; word < 4; ++word) key.push_back(descriptor.dwords[word]);
}
key.push_back(exports.size());
for (const auto mapping : exports) key.push_back(mapping);
static PrimedSharedMutex mutex;
static std::map<std::vector<std::uint64_t>, std::shared_ptr<PreparedBindingPlan>> plans;
std::shared_ptr<PreparedBindingPlan> plan;
{
std::shared_lock lock(mutex);
if (const auto found = plans.find(key); found != plans.end()) plan = found->second;
}
if (plan == nullptr) {
std::unique_lock lock(mutex);
const auto found = plans.find(key);
plan = found != plans.end() ? found->second : plans.emplace(key, std::make_shared<PreparedBindingPlan>()).first->second;
}
std::call_once(plan->ready, [&] {
try {
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();
}
});
if (plan->failure) std::rethrow_exception(plan->failure);
return plan;
}
std::shared_ptr<const SpecializedModule> specializeModule(const CompiledShaderArtifact& artifact, std::span<const std::uint32_t> classes, std::span<const PipelineSpecializationConstant> constants, const SpirvTarget& target) {
struct ModuleKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, ModuleKeyStorage>();
key.clear();
key.push_back(artifact.variantId);
for (const auto& constant : constants) key.push_back((static_cast<std::uint64_t>(constant.id) << 32u) | constant.value);
static PrimedSharedMutex mutex;
static std::map<std::vector<std::uint64_t>, std::shared_ptr<SpecializedModuleEntry>> modules;
std::shared_ptr<SpecializedModuleEntry> 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<SpecializedModuleEntry>()).first->second;
}
std::call_once(entry->ready, [&] {
try {
entry->module = buildSpecializedModule(artifact, classes, constants, target);
} catch (...) {
entry->failure = std::current_exception();
}
});
if (entry->failure) std::rethrow_exception(entry->failure);
return entry->module;
}
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) = {
.memoryOffsetDword = artifact.memoryOffsetDword,
.shaderDataDwords = artifact.shaderDataDwords,
.imageMetadataDword = artifact.imageMetadataDword,
.runtimeImageCount = artifact.runtimeImageCount,
.runtimeImageResources = artifact.runtimeImageResources,
.registrationModule = artifact.registrationModule,
.bdaAbiVersion = artifact.bdaAbiVersion,
.runtimeAbiVersion = artifact.runtimeAbiVersion,
.vertexInputs = artifact.vertexInputs,
.vertexOffsetSgpr = artifact.vertexOffsetSgpr,
.instanceOffsetSgpr = artifact.instanceOffsetSgpr,
.vertexOffsetShared = artifact.vertexOffsetShared,
.instanceOffsetShared = artifact.instanceOffsetShared,
.vertexOffsetConflict = artifact.vertexOffsetConflict,
.instanceOffsetConflict = artifact.instanceOffsetConflict,
.hostSubgroupSize = artifact.hostSubgroupSize,
.parameterExports = artifact.parameterExports,
.fragmentParameters = artifact.fragmentParameters,
.barycentricEmulation = artifact.barycentricEmulation,
.stencilReferences = artifact.stencilReferences,
.stencilReplaySafe = artifact.stencilReplaySafe,
.stencilReferenceDword = artifact.stencilReferenceDword,
.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>{}, request.target.registrationModules);
const DescriptorBindingPlan* bindingPlan = &plan->bindings;
std::shared_ptr<PreparedModuleEntry> entry;
struct LocalModuleKeyStorage {};
auto& moduleKey = HostThreadLocal<std::vector<std::uint32_t>, LocalModuleKeyStorage>();
moduleKey.clear();
if (variant.bindings.layout.UsesPushData()) moduleKey.push_back(request.layout.pushConstantOffsetBytes / 4u);
if (!artifact.stencilReferences.empty()) moduleKey.push_back(request.layout.pushConstantOffsetBytes / 4u + artifact.stencilReferenceDword);
if (request.context.pixel) {
for (const auto packing : request.context.pixel->targetExportPacking) moduleKey.push_back(static_cast<std::uint32_t>(packing));
moduleKey.push_back(request.context.pixel->dualSourceBlend ? 1u : 0u);
moduleKey.push_back(request.context.pixel->reversedBlend);
if (variant.info.info.rasterPrimitive) moduleKey.push_back(static_cast<std::uint32_t>(request.context.pixel->rasterPrimitive));
if (variant.info.info.layerExported) moduleKey.push_back(request.context.pixel->layerExported ? 1u : 0u);
}
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;
const auto& resource = vertex.resources[input.location];
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 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;
std::uint32_t selectors = kind << 12u;
auto& formatComponents = result.vertexAttributes.back().formatComponents;
formatComponents = 1u;
for (std::uint32_t component = 0; component < 4u; ++component) {
const auto selector = (input.outputMask & (1u << component)) != 0u ? (resource.fields[3] >> (component * 3u)) & 7u : 0u;
if (selector == 2u || selector == 3u) throw std::runtime_error("reserved prepared vertex component selector");
if (selector >= 4u) formatComponents = std::max(formatComponents, selector - 3u);
selectors |= selector << (component * 3u);
}
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) {
{
std::shared_lock lock(plan->mutex);
if (const auto found = plan->modules.find(moduleKey); found != plan->modules.end()) entry = found->second;
}
if (entry == nullptr) {
std::unique_lock lock(plan->mutex);
const auto found = plan->modules.find(moduleKey);
entry = found != plan->modules.end() ? found->second : plan->modules.emplace(moduleKey, std::make_shared<PreparedModuleEntry>()).first->second;
}
const auto prepare = [&] {
auto constants = plan->bindings.specialization;
std::size_t index = 0;
if (variant.bindings.layout.UsesPushData()) constants.push_back({PipelineSpecialization::PushDataOffset, moduleKey[index++]});
if (!artifact.stencilReferences.empty()) constants.push_back({PipelineSpecialization::StencilReferenceOffset, moduleKey[index++]});
if (request.context.pixel) {
for (std::uint32_t target = 0; target < request.context.pixel->targetExportPacking.size(); ++target) constants.push_back({PipelineSpecialization::ExportPackingBase + target, moduleKey[index++]});
constants.push_back({PipelineSpecialization::DualSourceBlend, moduleKey[index++]});
constants.push_back({PipelineSpecialization::ReversedBlend, moduleKey[index++]});
if (variant.info.info.rasterPrimitive) constants.push_back({PipelineSpecialization::RasterPrimitive, moduleKey[index++]});
if (variant.info.info.layerExported) constants.push_back({PipelineSpecialization::LayerExported, moduleKey[index++]});
}
if (!artifact.vertexInputPatches.empty()) {
for (const auto& input : result.vertexInputs) {
const auto selectors = moduleKey[index++];
const auto first = PipelineSpecialization::VertexBase + input.location * PipelineSpecialization::VertexWords;
for (std::uint32_t component = 0; component < 4u; ++component) constants.push_back({first + component, (selectors >> (component * 3u)) & 7u});
const auto kind = selectors >> 12u;
constants.push_back({first + 4u, kind == 0u ? 0x3f800000u : 1u});
constants.push_back({first + 5u, kind});
}
}
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);
entry->module = module;
};
std::call_once(entry->ready, [&] {
try {
prepare();
} catch (...) {
entry->failure = std::current_exception();
}
});
if (entry->failure) std::rethrow_exception(entry->failure);
const auto& module = entry->module;
result.specializationId = module->specializationId;
result.spirv = module->spirv;
bindingPlan = &entry->bindings;
} else {
result.spirv = artifact.spirv;
}
const auto tables = ResourceMaterializer::ResolveImageTables(variant.info.info, snapshot, artifact.variantId);
BindingAllocationResult bindings;
DescriptorBindingBuilder{}.Populate(bindings, variant.bindings, *bindingPlan, variant.info.userDataBase, snapshot, partialThreads(request), &tables);
result.workgroupMemoryDwords = WorkgroupMemoryStrideDwords(variant.info.info);
result.bindings = std::move(bindings.bindings);
result.pushConstants = std::move(bindings.pushConstants);
if (!artifact.stencilReferences.empty()) {
if (artifact.stencilReferenceDword * sizeof(std::uint32_t) != result.pushConstants.size() || result.pushConstants.size() + sizeof(std::uint32_t) > request.layout.pushConstantSizeBytes) throw std::runtime_error("stencil replay push constant lies outside the stage range");
result.pushConstants.resize(result.pushConstants.size() + sizeof(std::uint32_t));
}
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;
if (!tables.map.empty()) {
result.imageTablePoison.reserve(tables.poison.size());
for (const auto& poison : tables.poison) result.imageTablePoison.push_back({poison.words, poison.dwordCount, poison.resource, static_cast<std::uint32_t>(poison.reason)});
result.imageTableRanges = snapshot.tables.ranges;
result.imageTableReadRanges.reserve(snapshot.tables.tables.size());
for (const auto& table : snapshot.tables.tables) result.imageTableReadRanges.push_back(table.readRanges);
result.imageTableShader = snapshot.tables.shader;
result.imageTableFaults = tables.faults;
}
return result;
}
bool sameLayout(const BindingLayout& left, const BindingLayout& right) {
return left.descriptorSet == right.descriptorSet && left.firstBinding == right.firstBinding && left.pushConstantOffsetBytes == right.pushConstantOffsetBytes && left.pushConstantSizeBytes == right.pushConstantSizeBytes;
}
}
struct PreparedInterface {
static CompiledVariant Compile(SourceEntry& source, const RecompileRequest& request, ShaderPreparationContext& preparation, std::exception_ptr* emissionFailure) {
if (preparation.interfaceSource.get() != &source || preparation.interfaceProgram->Resources().shaderHash != request.shader.codeAddress) {
preparation.interfaceSource.reset();
preparation.interfaceProgram.reset();
auto program = source.program != nullptr ? std::move(source.program) : std::make_unique<IrProgram>(PrepareResourceProgram(request, &preparation));
recordEmissionFailure(emissionFailure, [&] { prepareInterface(request, *program); });
preparation.interfaceSource = source.shared_from_this();
preparation.interfaceProgram = std::move(program);
}
auto& program = *preparation.interfaceProgram;
return recordEmissionFailure(emissionFailure, [&] {
auto variant = emitVariant(request, program);
variant.info = program.CompiledInfo();
return variant;
});
}
};
namespace {
std::shared_ptr<const CompiledVariant> findOrCompileVariant(SourceEntry& source, const RecompileRequest& request, bool& cacheHit, ShaderPreparationContext* preparation = nullptr) {
for (const auto& candidate : source.variants) {
if (sameLayout(candidate->layout, request.layout)) {
cacheHit = true;
return candidate;
}
}
for (const auto& failure : source.emissionFailures) {
if (failure.codeAddress == request.shader.codeAddress && sameLayout(failure.layout, request.layout)) std::rethrow_exception(failure.failure);
}
cacheHit = false;
const bool disk = ShaderDiskCache::Enabled() && !DebugProbeActive();
std::vector<std::byte> diskKey;
std::shared_ptr<const CompiledVariant> variant;
if (disk) {
ShaderDiskCache::BuildKey(request, HostSubgroupSize(request), diskKey);
CompiledVariant loaded;
if (ShaderDiskCache::Load(diskKey, loaded)) {
loaded.layout = request.layout;
loaded.artifact.variantId = nextVariantId();
variant = std::make_shared<const CompiledVariant>(std::move(loaded));
}
}
if (variant == nullptr) {
static const bool layoutPair = std::getenv("APS5_NO_PERF_LAYOUT_PAIR") == nullptr;
std::exception_ptr emissionFailure;
try {
if (layoutPair && preparation != nullptr) {
variant = std::make_shared<const CompiledVariant>(PreparedInterface::Compile(source, request, *preparation, &emissionFailure));
} else {
auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request, preparation);
source.program.reset();
variant = std::make_shared<const CompiledVariant>(compileVariant(request, std::move(program), &emissionFailure));
}
} catch (...) {
if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, emissionFailure});
throw;
}
if (disk) {
if (auto persisted = ShaderDiskCache::Persist(diskKey, *variant)) variant = std::move(persisted);
else ShaderDiskCache::Store(std::move(diskKey), variant);
}
}
source.program.reset();
source.variants.push_back(variant);
return variant;
}
RecompileResult materializeVariant(SourceEntry& source, const RecompileRequest& request, const ResourceSnapshot& snapshot) {
std::shared_ptr<const CompiledVariant> variant;
bool cacheHit = false;
{
std::lock_guard lock(source.mutex);
variant = findOrCompileVariant(source, request, cacheHit);
}
auto result = materializeResult(*variant, request, snapshot);
result.cacheHit = cacheHit;
return result;
}
RecompileResult RecompileImpl(const RecompileRequest& request) {
static_cast<void>(RequestInputInfo(request));
RequestMemoryView memory(request.context.memory);
const auto runtime = memory.MakeRuntime(request.context.userData, request.shader.codeAddress);
ResourceSnapshot snapshot;
constexpr ResourceMaterializer materializer;
if (!request.useCache) {
auto program = PrepareResourceProgram(request);
const auto plan = materializer.ExtractPlan(program);
materializer.Materialize(plan, runtime, snapshot);
const auto variant = compileVariant(request, std::move(program));
return materializeResult(variant, request, snapshot);
}
const auto source = getSource(request);
materializer.Materialize(*source->plan, runtime, snapshot);
return materializeVariant(*source, request, snapshot);
}
// APS5_NO_RESULT_MEMO=1: every Recompile(request, capture) materializes its own result as before.
bool ResultMemo() {
static const bool resultMemo = std::getenv("APS5_NO_RESULT_MEMO") == nullptr;
return resultMemo;
}
constexpr std::size_t ResultMemoEntries = 256;
struct ResultMemoCounters {
std::atomic<std::uint64_t> hits{0}, misses{0}, evictions{0}, populateNanoseconds{0};
std::atomic<std::int64_t> lastReport{0};
};
ResultMemoCounters& resultMemoCounters() {
static ResultMemoCounters counters;
return counters;
}
void reportResultMemo() {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
if (!profile) return;
auto& counters = resultMemoCounters();
const auto now = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
auto last = counters.lastReport.load(std::memory_order_relaxed);
if (last == 0) {
counters.lastReport.compare_exchange_strong(last, now, std::memory_order_relaxed);
return;
}
if (now - last < 10'000'000'000ll || !counters.lastReport.compare_exchange_strong(last, now, std::memory_order_relaxed)) return;
const auto hits = counters.hits.exchange(0, std::memory_order_relaxed);
const auto misses = counters.misses.exchange(0, std::memory_order_relaxed);
const auto evictions = counters.evictions.exchange(0, std::memory_order_relaxed);
const auto populate = counters.populateNanoseconds.exchange(0, std::memory_order_relaxed);
std::fprintf(stderr, "[recompile] result memo (10 s): %llu hits, %llu misses (%.1f%% hits), Populate %.1f us per miss / %.1f ms in total, %llu evictions\n", static_cast<unsigned long long>(hits), static_cast<unsigned long long>(misses), hits + misses != 0 ? 100.0 * static_cast<double>(hits) / static_cast<double>(hits + misses) : 0.0, misses != 0 ? static_cast<double>(populate) / 1000.0 / static_cast<double>(misses) : 0.0, static_cast<double>(populate) / 1e6, static_cast<unsigned long long>(evictions));
}
// Everything materializeResult reads besides the variant: the snapshot (the descriptor words, the
// flattened SRT, the user data, the uniform fill) and, for the vertex family, the V# table the
// attributes are resolved from.
std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapshot& snapshot) {
std::uint64_t hash = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) {
hash ^= value;
hash *= 0x100000001b3ull;
};
const auto mixWords = [&](std::span<const std::uint32_t> words) {
mix(words.size());
for (const auto word : words) mix(word);
};
const auto mixDescriptors = [&](const std::vector<DescriptorValue>& values) {
mix(values.size());
for (const auto& value : values) {
mix(value.dwordCount);
for (std::uint32_t i = 0; i < value.dwordCount && i < value.dwords.size(); ++i) mix(value.dwords[i]);
}
};
mixDescriptors(snapshot.buffers);
mixDescriptors(snapshot.images);
mixDescriptors(snapshot.samplers);
mixWords(snapshot.flattenedSrt);
mixWords(snapshot.userData);
mix(static_cast<std::uint64_t>(snapshot.uniformFill.kind));
mix(snapshot.uniformFill.resource);
for (const auto stride : snapshot.uniformFill.groupStride) mix(stride);
mix(snapshot.uniformFill.words);
mix(snapshot.uniformFill.value);
const auto& tables = snapshot.tables;
mix(tables.shader);
mix(tables.tables.size());
for (const auto& table : tables.tables) {
mix(table.base);
mix(table.size);
mix(table.records);
mix(table.keys);
mix(table.fault);
mix(table.outside ? 1u : 0u);
mixWords(table.codes);
mix(table.readRanges.size());
for (const auto& [base, size] : table.readRanges) {
mix(base);
mix(size);
}
}
mixDescriptors(tables.words);
mix(tables.ranges.size());
for (const auto& [base, size] : tables.ranges) {
mix(base);
mix(size);
}
for (const auto threads : partialThreads(request)) mix(threads);
mix(request.layout.pushConstantOffsetBytes);
if (request.context.pixel) {
for (const auto mapping : request.context.pixel->targetExportMapping) mix(mapping);
for (const auto packing : request.context.pixel->targetExportPacking) mix(static_cast<std::uint64_t>(packing));
mix(request.context.pixel->dualSourceBlend);
mix(request.context.pixel->reversedBlend);
mix(static_cast<std::uint64_t>(request.context.pixel->rasterPrimitive));
mix(request.context.pixel->layerExported);
}
if (request.context.vertex) {
const auto& vertex = *request.context.vertex;
const auto count = std::min<std::uint32_t>(vertex.resourcesNum, ShaderVertexStageInfo::MaxResources);
mix(count);
for (std::uint32_t i = 0; i < count; ++i) {
for (const auto field : vertex.resources[i].fields) mix(field);
mix(vertex.resourcesDst[i].fetchIndex);
}
} else {
mix(1ull << 32u);
}
return hash;
}
// The memo'd result of `source`'s variant for the snapshot (design13 R5): a hit returns the shared
// object, a miss materializes outside the source mutex and inserts (a concurrent miss's object is
// as good). `memoHit` reports the hit.
std::shared_ptr<const RecompileResult> materializePreparedMemoized(SourceEntry& source, const std::shared_ptr<const CompiledVariant>& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool cacheHit, bool* memoHit) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto hash = snapshotHash(request, snapshot);
std::uint64_t index = 0;
auto& counters = resultMemoCounters();
{
std::lock_guard lock(source.mutex);
index = (variant->artifact.variantId * 0x9e3779b97f4a7c15ull) ^ hash;
const auto found = source.memoIndex.find(index);
if (found != source.memoIndex.end() && found->second->variantId == variant->artifact.variantId && found->second->hash == hash) {
source.memo.splice(source.memo.begin(), source.memo, found->second);
counters.hits.fetch_add(1, std::memory_order_relaxed);
if (memoHit != nullptr) *memoHit = true;
reportResultMemo();
return found->second->result;
}
}
const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
auto result = std::make_shared<RecompileResult>(materializeResult(*variant, request, snapshot));
result->cacheHit = cacheHit;
if (profile) counters.populateNanoseconds.fetch_add(static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - started).count()), std::memory_order_relaxed);
counters.misses.fetch_add(1, std::memory_order_relaxed);
std::shared_ptr<const RecompileResult> shared = std::move(result);
{
std::lock_guard lock(source.mutex);
const auto found = source.memoIndex.find(index);
if (found != source.memoIndex.end()) {
if (found->second->variantId == variant->artifact.variantId && found->second->hash == hash) {
source.memo.splice(source.memo.begin(), source.memo, found->second);
shared = found->second->result;
} else {
source.memo.erase(found->second);
source.memoIndex.erase(found);
}
}
if (source.memoIndex.find(index) == source.memoIndex.end()) {
source.memo.push_front({variant->artifact.variantId, hash, shared});
source.memoIndex.emplace(index, source.memo.begin());
while (source.memo.size() > ResultMemoEntries) {
const auto& last = source.memo.back();
source.memoIndex.erase((last.variantId * 0x9e3779b97f4a7c15ull) ^ last.hash);
source.memo.pop_back();
counters.evictions.fetch_add(1, std::memory_order_relaxed);
}
}
}
reportResultMemo();
return shared;
}
std::shared_ptr<const RecompileResult> materializeMemoized(SourceEntry& source, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool* memoHit) {
std::shared_ptr<const CompiledVariant> variant;
bool cacheHit = false;
{
std::lock_guard lock(source.mutex);
variant = findOrCompileVariant(source, request, cacheHit);
}
return materializePreparedMemoized(source, variant, request, snapshot, cacheHit, memoHit);
}
// The capture already resolved the source entry (stage input validation included) and materialized
// the request over exactly the words the driver captured, so neither is repeated here.
std::shared_ptr<const RecompileResult> RecompileImpl(const RecompileRequest& request, const ResourceCapture& capture, bool* memoHit) {
if (!request.useCache || capture.source == nullptr) {
auto program = PrepareResourceProgram(request);
const auto variant = compileVariant(request, std::move(program));
return std::make_shared<const RecompileResult>(materializeResult(variant, request, capture.snapshot));
}
if (!ResultMemo()) return std::make_shared<const RecompileResult>(materializeVariant(*capture.source, request, capture.snapshot));
return materializeMemoized(*capture.source, request, capture.snapshot, memoHit);
}
template <typename Impl>
auto recompileReporting(const RecompileRequest& request, Impl&& impl) -> decltype(impl()) {
try {
return impl();
} catch (const std::exception& e) {
constexpr auto requestSerializer = RequestSerializer{};
const auto inputInfo = "\nRecompileRequest:\n" + requestSerializer.Serialize(request);
throw std::runtime_error(std::string("ShaderRecompiler::Recompile: ") + e.what() + inputInfo);
} catch (...) {
throw std::runtime_error("ShaderRecompiler::Recompile: unknown exception");
}
}
}
std::shared_ptr<const IrResourcePlan> GetResourcePlan(const RecompileRequest& request) {
static_cast<void>(RequestInputInfo(request));
if (request.useCache) return getSource(request)->plan;
return makeResourcePlan(request);
}
namespace {
// The capture's materialization; with pure flat slots in the plan the walk's read addresses are
// traced into the capture (ResourceCapture::readTrace).
void materializeCapture(ResourceCapture& capture, const SrtRuntime& runtime) {
const auto& plan = *capture.plan;
if (std::none_of(plan.pureFlatSlots.begin(), plan.pureFlatSlots.end(), [](std::uint8_t pure) { return pure != 0u; })) {
ResourceMaterializer{}.Materialize(plan, runtime, capture.snapshot);
return;
}
SrtRuntime traced = runtime;
traced.readTrace = &capture.readTrace;
ResourceMaterializer{}.Materialize(plan, traced, capture.snapshot);
auto& other = capture.readTrace.otherReads;
std::sort(other.begin(), other.end());
other.erase(std::unique(other.begin(), other.end()), other.end());
}
}
std::shared_ptr<const ResourceCapture> CaptureResources(const RecompileRequest& request, const SrtRuntime& runtime) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
// Validates the stage inputs once per request, as GetResourcePlan and Recompile(request) do.
static_cast<void>(RequestInputInfo(request));
auto capture = std::make_shared<ResourceCapture>();
if (request.useCache) {
capture->source = getSource(request);
capture->plan = capture->source->plan;
} else {
capture->plan = makeResourcePlan(request);
}
if (profile) capture->sourceNanoseconds = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - started).count());
materializeCapture(*capture, runtime);
return capture;
}
std::shared_ptr<const SourceHandle> ResolveSource(const RecompileRequest& request) {
if (!request.useCache) return nullptr;
static_cast<void>(RequestInputInfo(request));
return std::make_shared<const SourceHandle>(SourceHandle{getSource(request)});
}
std::shared_ptr<const SourceHandle> PrepareShader(const RecompileRequest& request) {
return PrepareShader(request, nullptr);
}
std::shared_ptr<const SourceHandle> PrepareShader(const RecompileRequest& request, ShaderPreparationContext* preparation) {
return recompileReporting(request, [&]() -> std::shared_ptr<const SourceHandle> {
static_cast<void>(RequestInputInfo(request));
auto handle = std::make_shared<SourceHandle>();
handle->source = getSource(request, preparation, false);
RecompileCacheKey::BuildInterface(request, handle->staticKey);
handle->staticKey.push_back(HostSubgroupSize(request));
bool cacheHit = false;
std::lock_guard lock(handle->source->mutex);
handle->artifact = findOrCompileVariant(*handle->source, request, cacheHit, preparation);
return handle;
});
}
bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle) {
struct PreparedKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, PreparedKeyStorage>();
BuildPreparedShaderKey(request, key);
return MatchesPreparedShader(request, handle, key);
}
void BuildPreparedShaderKey(const RecompileRequest& request, std::vector<std::uint64_t>& key) {
RecompileCacheKey::BuildInterface(request, key);
key.push_back(HostSubgroupSize(request));
}
bool MatchesPreparedShader(const RecompileRequest& request, const SourceHandle& handle, std::span<const std::uint64_t> key) {
if (handle.source == nullptr || handle.artifact == nullptr) return false;
const auto& layout = request.layout;
const auto& prepared = handle.artifact->bindings.layout;
if (layout.descriptorSet != handle.artifact->layout.descriptorSet || layout.firstBinding != handle.artifact->layout.firstBinding) return false;
if (layout.pushConstantOffsetBytes % 4u != 0u || layout.pushConstantSizeBytes % 4u != 0u || layout.pushConstantOffsetBytes > NativePushConstantSize || layout.pushConstantSizeBytes > NativePushConstantSize - layout.pushConstantOffsetBytes) return false;
const auto words = prepared.ShaderDataDwords();
const bool usesPush = prepared.runtimeImageCount == 0u && words != 0u && words <= layout.pushConstantSizeBytes / 4u;
if (prepared.UsesPushData() != usesPush) return false;
if (!handle.artifact->artifact.stencilReferences.empty() && (prepared.stencilReferenceDword >= layout.pushConstantSizeBytes / 4u || prepared.stencilReferenceDword != (usesPush ? words : 0u))) return false;
if (!std::ranges::equal(key, handle.staticKey) || handle.source->code.size() != request.shader.code.size()) return false;
return handle.source->code.data() == request.shader.code.data() || std::ranges::equal(handle.source->code, request.shader.code);
}
std::span<const std::uint32_t> GetPreparedCode(const SourceHandle& handle) {
if (handle.source == nullptr || handle.artifact == nullptr) throw std::runtime_error("ShaderRecompiler: prepared artifact is missing");
return handle.source->code;
}
PreparedShaderInvocation::PreparedShaderInvocation(const RecompileRequest& request, const std::shared_ptr<const SourceHandle>& handle) : request(request), handle(handle) {
this->request.context.linkedCallFallbacks = handle->source->linkedCallFallbacks;
}
std::optional<PreparedShaderInvocation> PreparedShaderInvocation::TryCreate(const RecompileRequest& request, const std::shared_ptr<const SourceHandle>& handle) {
struct PreparedKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, PreparedKeyStorage>();
BuildPreparedShaderKey(request, key);
return TryCreate(request, handle, key);
}
std::optional<PreparedShaderInvocation> PreparedShaderInvocation::TryCreate(const RecompileRequest& request, const std::shared_ptr<const SourceHandle>& handle, std::span<const std::uint64_t> key) {
if (handle == nullptr || !MatchesPreparedShader(request, *handle, key)) return std::nullopt;
if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast<void>(RequestInputInfo(request));
return PreparedShaderInvocation(request, handle);
}
std::shared_ptr<const ResourceCapture> PreparedShaderInvocation::Capture(const SrtRuntime& runtime) const {
auto capture = std::make_shared<ResourceCapture>();
capture->source = handle->source;
capture->plan = handle->source->plan;
materializeCapture(*capture, runtime);
return capture;
}
std::shared_ptr<const RecompileResult> PreparedShaderInvocation::Materialize(const ResourceCapture& capture) const {
if (capture.source != handle->source || capture.plan != handle->source->plan) throw std::runtime_error("ShaderRecompiler: resource capture belongs to another prepared shader");
if (request.useCache && ResultMemo()) return materializePreparedMemoized(*handle->source, handle->artifact, request, capture.snapshot, true, nullptr);
auto result = std::make_shared<RecompileResult>(materializeResult(*handle->artifact, request, capture.snapshot));
result->cacheHit = true;
return result;
}
const CompiledShaderArtifact& GetPreparedArtifact(const SourceHandle& handle) {
if (handle.artifact == nullptr) throw std::runtime_error("ShaderRecompiler: prepared artifact is missing");
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");
if (request.useCache && ResultMemo()) return materializePreparedMemoized(*handle.source, handle.artifact, request, capture.snapshot, true, nullptr);
auto result = std::make_shared<RecompileResult>(materializeResult(*handle.artifact, request, capture.snapshot));
result->cacheHit = true;
return result;
}
std::shared_ptr<const ResourceCapture> CaptureResources(const RecompileRequest& request, const SrtRuntime& runtime, const SourceHandle& handle) {
if (handle.source == nullptr) throw std::runtime_error("ShaderRecompiler: source handle is missing");
if (handle.artifact != nullptr && !MatchesPreparedShader(request, handle)) throw std::runtime_error("ShaderRecompiler: prepared artifact does not match the static ABI");
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
// The whole vertex family (Vertex, Local, TC, TE, Mesh) validates V# fields the memo key does not cover.
if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast<void>(RequestInputInfo(request));
auto capture = std::make_shared<ResourceCapture>();
capture->source = handle.source;
capture->plan = handle.source->plan;
if (profile) capture->sourceNanoseconds = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - started).count());
materializeCapture(*capture, runtime);
return capture;
}
RecompileResult Recompile(const RecompileRequest& request) {
return recompileReporting(request, [&] { return RecompileImpl(request); });
}
std::shared_ptr<const RecompileResult> Recompile(const RecompileRequest& request, const ResourceCapture& capture, bool* memoHit) {
if (memoHit != nullptr) *memoHit = false;
return recompileReporting(request, [&] { return RecompileImpl(request, capture, memoHit); });
}
}