Files
AnyPS5/core/shader/recompiler/Recompiler.hpp
T
AlxcBTWandClaude Opus 5.5 18d9874a06 feat(shader): give reads the masked-select pass keeps the whole pixel ancillary word
A packed ancillary word that still reaches an export, or any other read
the masked-select pass cannot drop, no longer stops the recompile. After
that pass every remaining use gets the composed word: the primitive class
at bits 0-1, SampleId at bits 8-11 and Layer at bits 16-28, with every
other bit zero. Those lanes do export the ancillary VGPR on the console,
so the word has to be right, not just present.

The primitive class (0 point, 1 line, 2 triangle, 3 rectangle) comes from
the draw. RasterPrimitiveOf maps rect lists to 3, line lists on the vertex
path to 1 and everything else the driver accepts to 2, because its
geometry and tessellation paths only output triangles. It reaches the
shader as pipeline specialization constant 8233, the way dual-source
blending does: a request field that is folded into the specialized
module and is part of the result memo, the draw key and the serialized
request (version 17).

Only shaders that read the class pay for it. ShaderInfo records the read
(disk cache format 28), and only those shaders put the class in their
module key, so a shader that reads just the sample or layer field keeps
one module for every primitive class. The draw sets the class only when
the pixel shader loads the ancillary VGPR, so other draws keep the draw
cache entries they have today.

The VRS rates (bits 2-5) and the eye id (bit 7) are 0 because the driver
supports neither VRS nor multiview. SampleId is 0 because the driver
rejects MSAA and per-sample shading, and Layer is 0 while it ignores
vertex layer exports. TechnicalDebt.md records the layout sources and
these assumptions.

Tests:
- pixel_ancillary builds an export to target 0 whose two channels select
  between a packed value and a phi carrying the word. It checks the word
  for each primitive class, sample and layer, checks that the three
  shapes the masked-select pass keeps now compile with the composed word,
  and that a program whose select the pass drops reads no class.
- agc_driver_pixel_ancillary_word exports the word from a draw and reads
  back 2, 3 and 1 for triangle, rectangle and line. A shader that reads
  only the sample field shares one module across the classes, and the
  whole-word shader gets one per class. It also checks RasterPrimitiveOf.
- agc_shader_memory covers the version 17 round trip, and
  agc_shader_disk_cache the new flag.

Both ancillary tests fail with the old refusal when the composition is
disabled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:06:21 +13:00

556 lines
18 KiB
C++

#ifndef CORE_SHADER_RECOMPILIER_INCLUDE_SHADER_RECOMPILIER_RECOMPILER_HPP
#define CORE_SHADER_RECOMPILIER_INCLUDE_SHADER_RECOMPILIER_RECOMPILER_HPP
#include "RuntimeAbi.hpp"
#include "PipelineSpecialization.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <initializer_list>
#include <memory>
#include <optional>
#include <span>
#include <utility>
#include <string_view>
#include <vector>
namespace ShaderRecompiler {
enum class ShaderStage {
Compute,
Vertex,
TessellationControl,
TessellationEvaluation,
Geometry,
Fragment,
Local,
Mesh
};
struct MemoryRegion {
std::uint64_t guestAddress;
std::span<const std::byte> bytes;
};
struct ShaderBinary {
ShaderStage stage;
std::uint64_t codeAddress;
std::span<const std::uint32_t> code;
std::uint64_t headerAddress;
std::span<const std::byte> header;
};
struct ShaderComputeStageInfo {
std::array<std::uint32_t, 3> numThreads;
std::uint32_t ldsSizeDwords;
std::array<bool, 3> groupIdEnable;
bool tgSizeEnable;
std::uint32_t threadIdComponentCount;
std::array<std::uint32_t, 3> partialThreads;
std::uint32_t scratchDwords = 0;
[[nodiscard]] bool PartialGroups() const {
return partialThreads != std::array<std::uint32_t, 3>{};
}
};
enum class PixelInput : std::uint32_t {
PerspectiveSample,
PerspectiveCenter,
PerspectiveCentroid,
PerspectivePullModel,
LinearSample,
LinearCenter,
LinearCentroid,
LineStipple,
PositionX,
PositionY,
PositionZ,
PositionW,
FrontFace,
Ancillary,
SampleCoverage,
PositionFixedPoint,
Count
};
constexpr std::uint32_t PixelInputBit(PixelInput input) {
return 1u << static_cast<std::uint32_t>(input);
}
constexpr std::uint32_t PixelInputVgprCount(PixelInput input) {
switch (input) {
case PixelInput::PerspectiveSample:
case PixelInput::PerspectiveCenter:
case PixelInput::PerspectiveCentroid:
case PixelInput::LinearSample:
case PixelInput::LinearCenter:
case PixelInput::LinearCentroid:
return 2u;
case PixelInput::PerspectivePullModel:
return 3u;
default:
return 1u;
}
}
constexpr std::uint32_t PixelInputVgpr(std::uint32_t inputAddr, PixelInput input) {
std::uint32_t vgpr = 0;
for (std::uint32_t i = 0; i < static_cast<std::uint32_t>(input); ++i) {
if ((inputAddr & (1u << i)) != 0u) vgpr += PixelInputVgprCount(static_cast<PixelInput>(i));
}
return vgpr;
}
enum class ConservativeZExport : std::uint8_t {
AnyZ,
LessThanZ,
GreaterThanZ
};
enum class ColorExportPacking : std::uint8_t {
None,
Unorm10_11_11
};
enum class RasterPrimitive : std::uint8_t {
Point,
Line,
Triangle,
Rectangle
};
struct ShaderPixelStageInfo {
std::uint32_t interpolatorCount;
std::array<std::uint32_t, 32> interpolatorSettings;
bool wave32;
std::uint32_t inputAddr;
bool hasPerspectiveCenterVgpr;
bool perspectiveCentroid;
bool posX;
bool posY;
bool posZ;
bool posW;
bool frontFace;
bool ancillary;
bool sampleShading;
bool noPerspective;
bool linearCentroid;
bool pixelKillEnable;
bool depthExportEnable;
bool sampleMaskExportEnable;
bool earlyZ;
bool executeOnNoop;
ConservativeZExport conservativeZExport;
bool orderedPixelShader;
std::array<std::uint8_t, 8> targetOutputMode;
std::array<std::uint8_t, 8> targetExportMapping;
std::array<ColorExportPacking, 8> targetExportPacking;
bool dualSourceBlend;
bool alphaToCoverage;
bool stencilRefExportEnable;
RasterPrimitive rasterPrimitive = RasterPrimitive::Triangle;
};
struct ShaderVertexBufferResource {
std::array<std::uint32_t, 4> fields;
};
struct ShaderVertexResourceDestination {
std::int32_t registerStart;
std::int32_t registersNum;
std::int32_t attrId;
std::uint32_t fetchIndex;
};
struct ShaderVertexStageInfo {
static constexpr std::uint32_t MaxResources = 32;
std::array<ShaderVertexBufferResource, MaxResources> resources;
std::array<ShaderVertexResourceDestination, MaxResources> resourcesDst;
std::uint32_t resourcesNum;
std::uint32_t fetchAttribReg;
std::uint32_t fetchBufferReg;
bool fetchEmbedded;
};
struct ShaderFloatMode {
std::uint32_t floatMode = 0;
bool dx10Clamp = false;
bool ieeeMode = false;
bool fp16Overflow = false;
bool operator==(const ShaderFloatMode&) const = default;
};
inline constexpr std::uint32_t InterpolationQuiet = 1u;
inline constexpr std::uint32_t InterpolationFlush32 = 2u;
inline constexpr std::uint32_t InterpolationFlush16 = 4u;
inline constexpr std::uint32_t InterpolationRound16Shift = 3u;
struct GuestContext {
std::uint32_t waveSize;
std::uint32_t userDataBaseRegister;
std::span<const std::uint32_t> userData;
std::optional<ShaderComputeStageInfo> compute;
std::optional<ShaderPixelStageInfo> pixel;
std::optional<ShaderVertexStageInfo> vertex;
std::span<const MemoryRegion> memory;
std::optional<ShaderFloatMode> floatMode;
};
struct MeshTargetLimits {
std::array<std::uint32_t, 3> maxWorkgroupSize;
std::uint32_t maxWorkgroupInvocations;
std::uint32_t maxSharedMemoryBytes;
std::uint32_t maxOutputVertices;
std::uint32_t maxOutputPrimitives;
std::uint32_t maxOutputComponents;
std::uint32_t maxOutputMemoryBytes;
std::uint32_t outputPerVertexGranularity;
std::uint32_t outputPerPrimitiveGranularity;
};
struct TessellationTargetLimits {
std::uint32_t maxPatchSize;
std::uint32_t maxControlPerVertexInputComponents;
std::uint32_t maxControlPerVertexOutputComponents;
std::uint32_t maxControlPerPatchOutputComponents;
std::uint32_t maxControlTotalOutputComponents;
std::uint32_t maxEvaluationInputComponents;
std::uint32_t maxEvaluationOutputComponents;
};
struct SpirvTarget {
std::uint32_t vulkanVersion;
std::uint32_t spirvVersion;
std::uint32_t subgroupSize;
std::uint32_t bdaAbiVersion;
std::span<const std::uint32_t> supportedCapabilities;
std::span<const std::string_view> supportedExtensions;
bool fragmentShaderBarycentricEnabled;
std::array<std::uint32_t, 3> maxWorkgroupSize;
std::uint32_t maxWorkgroupInvocations;
std::uint32_t maxWorkgroupSharedMemoryBytes;
std::optional<MeshTargetLimits> mesh;
std::optional<TessellationTargetLimits> tessellation;
bool nonConstantImageOffsets = false;
std::uint32_t srgbDecodeFormats = 0;
bool narrowSubgroupClock = false;
bool fixedPushSlots = false;
bool float64DenormPreserve = false;
bool registrationModules = false;
std::uint32_t registrationWordLimit = 200000u;
bool vertexSubgroups = true;
};
struct BindingLayout {
std::uint32_t descriptorSet;
std::uint32_t firstBinding;
std::uint32_t pushConstantOffsetBytes;
std::uint32_t pushConstantSizeBytes;
};
enum class ProgramRole {
Main,
GeometryBack,
Local,
Hull,
Domain,
Fragment
};
struct LinkedProgram {
ProgramRole role;
ShaderBinary binary;
std::uint32_t userDataBaseRegister;
std::uint32_t firstUserSgpr;
std::span<const std::uint32_t> userData;
};
struct MeshConfiguration {
std::uint32_t inputPrimitive;
std::uint32_t primitivesPerGroup;
std::uint32_t verticesPerGroup;
std::uint32_t maxVertices;
std::uint32_t maxPrimitives;
std::uint32_t threadsPerGroup;
std::uint32_t ldsSizeDwords;
std::uint32_t provokingVertex;
std::uint32_t esgsItemSize = 0;
};
struct TessellationConfiguration {
std::uint32_t inputControlPoints;
std::uint32_t outputControlPoints;
std::uint32_t domain;
std::uint32_t partitioning;
std::uint32_t outputTopology;
};
inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104;
inline constexpr std::uint32_t MeshDrawPushBytes = 24;
inline constexpr std::uint32_t MeshArgumentAddressDword = 4;
inline constexpr std::uint32_t MeshArgumentIndexCountDword = 3;
inline constexpr std::uint32_t MeshArgumentFirstIndexDword = 4;
inline constexpr std::uint32_t MeshArgumentBytes = 20;
inline constexpr std::uint32_t MeshIndexRestartTable = 0x100;
inline constexpr std::uint32_t MeshRestartLengthDword = MeshArgumentBytes / 4;
inline constexpr std::uint32_t MeshRestartTableDword = MeshRestartLengthDword + 1;
inline constexpr std::uint32_t MeshIndexBufferUserWord = 4;
inline constexpr std::uint32_t WorkgroupMemoryDescriptorSet = 1;
struct GraphicsDrawParameters {
std::uint64_t indexAddress;
std::uint32_t indexCount;
std::uint32_t indexElementBytes;
std::uint32_t instanceCount;
};
struct GraphicsCompileContext {
std::uint32_t firstUserSgpr;
std::span<const LinkedProgram> linkedPrograms;
std::optional<MeshConfiguration> mesh;
std::optional<TessellationConfiguration> tessellation;
GraphicsDrawParameters draw;
};
struct RecompileRequest {
ShaderBinary shader;
GuestContext context;
SpirvTarget target;
BindingLayout layout;
std::optional<GraphicsCompileContext> graphics;
bool useCache = true;
};
enum class DescriptorKind {
UniformBuffer,
StorageBuffer,
UniformTexelBuffer,
StorageTexelBuffer,
SampledImage,
StorageImage,
Sampler
};
enum class DescriptorImageShape {
Image1D,
Image2D,
Image2DArray,
ImageCube,
Image3D,
Image1DArray
};
enum class DescriptorRole {
GuestBuffers,
GuestImages,
GuestSamplers,
Gds,
BdaPagetable,
FaultBuffer,
FlattenedSrt,
ShaderData
};
struct DescriptorBinding {
DescriptorKind kind;
DescriptorRole role;
std::uint32_t descriptorSet;
std::uint32_t binding;
std::uint32_t count;
std::vector<std::uint32_t> guestDescriptor;
bool readOnly = false;
std::optional<DescriptorImageShape> imageShape;
std::vector<bool> samplerDepthCompare;
// Guest image elements the shader stores to (or updates atomically); the others are only read.
std::vector<bool> imageWritten;
std::vector<bool> imageDepthCompare;
std::vector<bool> imageAtomic;
std::vector<bool> imageAtomic64;
// Guest buffer elements the shader updates atomically (one entry per element of a GuestBuffers
// binding, empty otherwise). An atomic on a host-imported range is a serialized PCIe round trip
// (~0.4-0.5 us each on NVIDIA), so a driver may keep these elements in device-local memory.
std::vector<bool> bufferAtomic;
// Guest buffer elements the shader may store to through this V# (any store or atomic in the
// program, whatever its offset), one entry per element of a GuestBuffers binding, empty
// otherwise. A false entry is proved: every access of that element is a load. A driver may then
// skip the write-back and the pending-write note for the element; an element beyond the vector
// (a producer that does not fill it) must be treated as written.
std::vector<bool> bufferWritten;
std::vector<bool> samplerUnnormalized;
std::vector<bool> samplerIntegerBorder;
std::vector<bool> imageUnnormalized;
std::vector<bool> imageUnnormalizedBaseLevel;
std::vector<std::uint32_t> imageSamplers;
std::vector<bool> bufferRead;
};
struct VertexAttribute {
std::uint32_t location;
std::uint32_t components;
ShaderVertexBufferResource resource;
std::uint32_t fetchIndex;
std::uint32_t formatComponents = 0;
};
struct FragmentParameter {
std::uint32_t location;
std::uint32_t sourceLocation;
bool flat;
bool perVertex;
bool custom = false;
bool flatP10 = false;
};
struct BarycentricEmulation {
bool active = false;
bool smooth = false;
bool linear = false;
};
struct BarycentricEmulationLayout {
static constexpr std::uint32_t NoLocation = 0xffffffffu;
std::uint32_t smoothLocation = NoLocation;
std::uint32_t linearLocation = NoLocation;
std::vector<std::pair<std::uint32_t, std::uint32_t>> perVertexLocations;
};
[[nodiscard]] BarycentricEmulationLayout LayoutBarycentricEmulation(std::span<const FragmentParameter> parameters, const BarycentricEmulation& emulation);
// Compiled SPIR-V shared between a cached variant and every result materialized from it: results
// are copied per dispatch and draw, so the words are reference counted and only duplicated when a
// holder writes to them (tests and tools patch modules in place). Reads look like a vector.
// The non-const data(), operator[], begin() and end() count as writes: on a shared holder (every
// cached result) they clone the module, so a consumer that only reads takes the result by const
// reference, or the per-dispatch copy this class removes comes back without a compiler hint.
class SharedSpirv {
public:
SharedSpirv() = default;
SharedSpirv(std::vector<std::uint32_t> words) : words(std::make_shared<std::vector<std::uint32_t>>(std::move(words))) {}
SharedSpirv& operator=(std::vector<std::uint32_t> other) {
words = std::make_shared<std::vector<std::uint32_t>>(std::move(other));
return *this;
}
[[nodiscard]] const std::vector<std::uint32_t>& Words() const { return words ? *words : Empty(); }
operator const std::vector<std::uint32_t>&() const { return Words(); }
[[nodiscard]] std::size_t size() const { return Words().size(); }
[[nodiscard]] bool empty() const { return Words().empty(); }
[[nodiscard]] const std::uint32_t* data() const { return Words().data(); }
[[nodiscard]] std::uint32_t* data() { return Mutable().data(); }
[[nodiscard]] const std::uint32_t& operator[](std::size_t index) const { return Words()[index]; }
[[nodiscard]] std::uint32_t& operator[](std::size_t index) { return Mutable()[index]; }
[[nodiscard]] std::vector<std::uint32_t>::const_iterator begin() const { return Words().begin(); }
[[nodiscard]] std::vector<std::uint32_t>::const_iterator end() const { return Words().end(); }
[[nodiscard]] std::vector<std::uint32_t>::iterator begin() { return Mutable().begin(); }
[[nodiscard]] std::vector<std::uint32_t>::iterator end() { return Mutable().end(); }
void resize(std::size_t count) { Mutable().resize(count); }
std::vector<std::uint32_t>::iterator insert(std::vector<std::uint32_t>::const_iterator where, std::initializer_list<std::uint32_t> values) { return Mutable().insert(where, values); }
friend bool operator==(const SharedSpirv& left, const SharedSpirv& right) { return left.words == right.words || left.Words() == right.Words(); }
private:
static const std::vector<std::uint32_t>& Empty() {
static const std::vector<std::uint32_t> empty;
return empty;
}
// Copy on write: a holder whose words are shared gets its own copy before the first write.
std::vector<std::uint32_t>& Mutable() {
if (words == nullptr || words.use_count() != 1) words = std::make_shared<std::vector<std::uint32_t>>(Words());
return *words;
}
std::shared_ptr<std::vector<std::uint32_t>> words;
};
struct VertexInput {
std::uint32_t location;
std::uint32_t components;
std::uint32_t fetchIndex;
std::uint32_t outputMask = 0;
bool operator==(const VertexInput& other) const = default;
};
struct VertexInputPatch {
std::uint32_t location;
std::uint32_t word;
std::array<std::uint32_t, 3> values;
bool operator==(const VertexInputPatch& other) const = default;
};
struct CompiledShaderArtifact {
std::vector<VertexInputPatch> vertexInputPatches;
SharedSpirv spirv;
std::uint32_t memoryOffsetDword = 0;
std::uint32_t shaderDataDwords = 0;
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;
std::int32_t vertexOffsetSgpr = -1;
std::int32_t instanceOffsetSgpr = -1;
bool vertexOffsetShared = false;
bool instanceOffsetShared = false;
bool vertexOffsetConflict = false;
bool instanceOffsetConflict = false;
std::uint32_t hostSubgroupSize = 0;
std::vector<std::uint32_t> parameterExports;
std::vector<FragmentParameter> fragmentParameters;
BarycentricEmulation barycentricEmulation;
std::uint64_t variantId = 0;
};
struct ShaderInvocation {
std::vector<PipelineSpecializationConstant> specialization;
std::uint64_t specializationId = 0;
std::vector<DescriptorBinding> bindings;
std::vector<std::byte> pushConstants;
std::vector<VertexAttribute> vertexAttributes;
std::uint32_t poisonedSrtReads = 0;
};
struct RecompileResult : CompiledShaderArtifact, ShaderInvocation {
bool cacheHit = false;
std::uint32_t workgroupMemoryDwords = 0;
[[nodiscard]] std::uint64_t PipelineVariantId() const { return specializationId != 0 ? specializationId : variantId; }
};
[[nodiscard]] RecompileResult Recompile(const RecompileRequest& request);
struct ResourceCapture;
[[nodiscard]] std::shared_ptr<const RecompileResult> Recompile(const RecompileRequest& request, const ResourceCapture& capture, bool* memoHit = nullptr);
// Debug aid (see DebugProbe in Translation/TranslationContext.hpp): the APS5_PROBE register probe is
// only applied while a driver has it active, so it can be limited to one dispatch; the recompile
// cache keys on it.
void SetDebugProbeActive(bool active);
[[nodiscard]] bool DebugProbeActive();
[[nodiscard]] bool RayTracingStrict();
[[nodiscard]] bool RayTracingMiss();
struct RectListShaders {
RecompileResult control;
RecompileResult evaluation;
};
[[nodiscard]] RectListShaders BuildRectListShaders(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target);
struct GeometryStageLimits {
std::uint32_t maxGeometryInputComponents;
std::uint32_t maxGeometryOutputComponents;
std::uint32_t maxGeometryOutputVertices;
std::uint32_t maxGeometryTotalOutputComponents;
std::uint32_t maxFragmentInputComponents;
};
[[nodiscard]] RecompileResult BuildBarycentricGeometryShader(const RecompileResult& vertex, const RecompileResult& fragment, const SpirvTarget& target, const std::optional<GeometryStageLimits>& limits);
}
#endif