| /* | |
| * Copyright 2025 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "src/gpu/graphite/vk/VulkanSpirvTransforms.h" | |
| #include "include/private/base/SkAssert.h" | |
| #include "src/sksl/codegen/SkSLCodeGenTypes.h" | |
| #include "src/sksl/spirv.h" | |
| #ifdef SK_DEBUG | |
| #include "src/gpu/graphite/Log.h" | |
| #include "src/sksl/SkSLCompiler.h" | |
| #include "src/sksl/codegen/SkSLSPIRVValidator.h" | |
| #endif | |
| #include <cstdint> | |
| #include <vector> | |
| namespace skgpu::graphite { | |
| namespace { | |
| /* | |
| * The SPIR-V transformer is implemented as one main class, SpirvTransformer, and a number of | |
| * logically separate transformations (currently one), such as SpirvMultisampleTransformer. | |
| * | |
| * SpirvTransformer is a simple loop over the SPIR-V instructions. The instructions of interest are | |
| * parsed (as much as the transformations need), and passed to individual transformers. | |
| * | |
| * The transformers themselves may change an instruction, by generating new one(s) and dropping the | |
| * original instruction, or may add instructions triggered when encountering a specific instruction. | |
| * | |
| * This design lets multiple transformations be done in a single pass, avoiding the need to walk | |
| * over the SPIR-V again and again, most of which is uninteresting to the transformers. | |
| */ | |
| using SpirvBlob = std::vector<uint32_t>; | |
| /* | |
| * SPIR-V's first five words are: | |
| * | |
| * Index 0: Magic number | |
| * Index 1: Version number | |
| * Index 2: Generator's magic number | |
| * Index 3: ID bound; all ids are smaller than this | |
| * Index 4: 0 (reserved) | |
| * | |
| * The rest of the instructions start at index 5 in sections (see 2.4. Logical Layout of a Module in | |
| * the SPIR-V spec): | |
| * | |
| * OpCapability instructions | |
| * OpExtInst instructions | |
| * OpExtInstImport instructions | |
| * OpMemoryModel instructions | |
| * OpEntryPoint instructions | |
| * OpExecutionMode instructions | |
| * Debug instructions | |
| * Types, constants and global variable instructions | |
| * Function declarations | |
| * | |
| * Most instructions are uninteresting to the transformer and are skipped over. | |
| */ | |
| constexpr uint32_t kIDBoundIndex = 3; | |
| constexpr uint32_t kInstructionsStartIndex = 5; | |
| SpvId GetNewId(SpirvBlob* blob) { | |
| // Return a new ID and increase the ID bound. | |
| return (*blob)[kIDBoundIndex]++; | |
| } | |
| SpvOp GetInstructionOp(const uint32_t* instruction) { | |
| return static_cast<SpvOp>(instruction[0] & SpvOpCodeMask); | |
| } | |
| uint32_t GetInstructionWordCount(const uint32_t* instruction) { | |
| return instruction[0] >> SpvWordCountShift; | |
| } | |
| uint32_t MakeInstructionOp(SpvOp opCode, uint32_t wordCount) { | |
| return opCode | wordCount << SpvWordCountShift; | |
| } | |
| void CopyInstruction(const uint32_t* instruction, size_t wordCount, SpirvBlob* spirvOut) { | |
| spirvOut->insert(spirvOut->end(), instruction, instruction + wordCount); | |
| } | |
| enum class TransformationState { | |
| Transformed, | |
| Unchanged, | |
| }; | |
| /* | |
| * A transformer that at a high level changes subpassLoad(input) to subpassLoad(inputMS, sample), | |
| * such that shaders with input attachment are usable with multisampling. It unconditionally adds a | |
| * built-in SampleId variable and the corresponding SampleRateShading capability with the assumption | |
| * that the SPIR-V generator does not already produce them. | |
| */ | |
| class SpirvMultisampleTransformer { | |
| public: | |
| SpirvMultisampleTransformer(uint32_t maxId) : fIsInputAttachmentImage(maxId + 1, false) {} | |
| void transformOpCapability(SpvCapability capability, SpirvBlob* spirvOut); | |
| TransformationState transformOpEntryPoint(const uint32_t* instruction, SpirvBlob* spirvOut); | |
| void transformOpDecorate(SpirvBlob* spirvOut); | |
| TransformationState transformOpTypePointer(const uint32_t* instruction, SpirvBlob* spirvOut); | |
| TransformationState transformOpTypeImage(SpvId resultId, | |
| SpvId sampledTypeId, | |
| SpirvBlob* spirvOut); | |
| void transformOpLoad(SpvId resultTypeId, SpvId resultId, SpvId pointerId, SpirvBlob* spirvOut); | |
| TransformationState transformOpImageRead(SpvId resultTypeId, | |
| SpvId resultId, | |
| SpvId imageId, | |
| SpvId coordinateId, | |
| SpirvBlob* spirvOut); | |
| private: | |
| // Map OpLoad result to whether it was from the input attachment variable. | |
| std::vector<bool> fIsInputAttachmentImage; | |
| bool fHasAddedDecorations = false; | |
| SpvId fSampleIdVarId = 0; | |
| }; | |
| void SpirvMultisampleTransformer::transformOpCapability(SpvCapability capability, | |
| SpirvBlob* spirvOut) { | |
| // Add the SampleRateShading capability, once when the InputAttachment capability is encountered | |
| if (capability == SpvCapabilityInputAttachment) { | |
| // Generate: | |
| // OpCapability SampleRateShading | |
| spirvOut->push_back(MakeInstructionOp(SpvOpCapability, 2)); | |
| spirvOut->push_back(SpvCapabilitySampleRateShading); | |
| } | |
| } | |
| TransformationState SpirvMultisampleTransformer::transformOpEntryPoint(const uint32_t* instruction, | |
| SpirvBlob* spirvOut) { | |
| // Keep the entry point instruction as is, just append the SampleId variable to it. | |
| fSampleIdVarId = GetNewId(spirvOut); | |
| const uint32_t originalWordCount = GetInstructionWordCount(instruction); | |
| // The length of the instruction is increased by one | |
| spirvOut->push_back(MakeInstructionOp(SpvOpEntryPoint, originalWordCount + 1)); | |
| // The rest of the instruction is copied verbatim | |
| CopyInstruction(instruction + 1, originalWordCount - 1, spirvOut); | |
| // The last word is the new variable | |
| spirvOut->push_back(fSampleIdVarId); | |
| return TransformationState::Transformed; | |
| } | |
| void SpirvMultisampleTransformer::transformOpDecorate(SpirvBlob* spirvOut) { | |
| // Add decorations for the new SampleId variable as soon as the decorations section is visited. | |
| // Note that there is always at least one decoration, because the shaders have _some_ input or | |
| // output. | |
| if (fHasAddedDecorations) { | |
| return; | |
| } | |
| // Generate: | |
| // OpDecorate %SampleIdVar RelaxedPrecision | |
| // OpDecorate %SampleIdVar Flat | |
| // OpDecorate %SampleIdVar BuiltIn SampleId | |
| spirvOut->push_back(MakeInstructionOp(SpvOpDecorate, 3)); | |
| spirvOut->push_back(fSampleIdVarId); | |
| spirvOut->push_back(SpvDecorationRelaxedPrecision); | |
| spirvOut->push_back(MakeInstructionOp(SpvOpDecorate, 3)); | |
| spirvOut->push_back(fSampleIdVarId); | |
| spirvOut->push_back(SpvDecorationFlat); | |
| spirvOut->push_back(MakeInstructionOp(SpvOpDecorate, 4)); | |
| spirvOut->push_back(fSampleIdVarId); | |
| spirvOut->push_back(SpvDecorationBuiltIn); | |
| spirvOut->push_back(SpvBuiltInSampleId); | |
| fHasAddedDecorations = true; | |
| } | |
| TransformationState SpirvMultisampleTransformer::transformOpTypePointer(const uint32_t* instruction, | |
| SpirvBlob* spirvOut) { | |
| // When the int pointer type declaration is encountered, declare the SampleId variable right | |
| // away. | |
| const SpvId resultId = instruction[1]; | |
| if (resultId != SkSL::spirv::kIdTypePointerInputInt) { | |
| return TransformationState::Unchanged; | |
| } | |
| // Keep the int pointer declaration. | |
| CopyInstruction(instruction, GetInstructionWordCount(instruction), spirvOut); | |
| // Generate: | |
| // %SampleIdVar = OpVariable %kIdTypePointerInputInt Input | |
| spirvOut->push_back(MakeInstructionOp(SpvOpVariable, 4)); | |
| spirvOut->push_back(SkSL::spirv::kIdTypePointerInputInt); | |
| spirvOut->push_back(fSampleIdVarId); | |
| spirvOut->push_back(SpvStorageClassInput); | |
| return TransformationState::Transformed; | |
| } | |
| TransformationState SpirvMultisampleTransformer::transformOpTypeImage(SpvId resultId, | |
| SpvId sampledTypeId, | |
| SpirvBlob* spirvOut) { | |
| // Change the OpTypeImage instruction for the input attachment to be multisampled. The result id | |
| // is untouched, so the OpTypePointer and OpVariable instructions remain as-is! | |
| if (resultId != SkSL::spirv::kIdTypeImageSubpassData) { | |
| return TransformationState::Unchanged; | |
| } | |
| // The instruction must have been: | |
| // | |
| // %resultId = OpTypeImage %sampledTypeId SubpassData 0 0 0 2 Unknown | |
| // | |
| // It is transformed to: | |
| // | |
| // %resultId = OpTypeImage %sampledTypeId SubpassData 0 0 1 2 Unknown | |
| spirvOut->push_back(MakeInstructionOp(SpvOpTypeImage, 9)); | |
| spirvOut->push_back(resultId); | |
| spirvOut->push_back(sampledTypeId); | |
| spirvOut->push_back(SpvDimSubpassData); | |
| spirvOut->push_back(0); | |
| spirvOut->push_back(0); | |
| spirvOut->push_back(1); | |
| spirvOut->push_back(2); | |
| spirvOut->push_back(SpvImageFormatUnknown); | |
| return TransformationState::Transformed; | |
| } | |
| void SpirvMultisampleTransformer::transformOpLoad(SpvId resultTypeId, | |
| SpvId resultId, | |
| SpvId pointerId, | |
| SpirvBlob* spirvOut) { | |
| // If the input attachment variable is loaded, remember the result id. This is needed to know | |
| // which OpImageRead instructions to transform. | |
| if (pointerId == SkSL::spirv::kIdVariableImageSubpassData && | |
| resultId < fIsInputAttachmentImage.size()) { | |
| fIsInputAttachmentImage[resultId] = true; | |
| } | |
| } | |
| TransformationState SpirvMultisampleTransformer::transformOpImageRead(SpvId resultTypeId, | |
| SpvId resultId, | |
| SpvId imageId, | |
| SpvId coordinateId, | |
| SpirvBlob* spirvOut) { | |
| // If reading from the input attachment variable, load from the SampleId variable and add a | |
| // `Sample` operand to the image read operation. The result id is kept as is, so the rest of the | |
| // SPIR-V is unaffected. | |
| if (imageId >= fIsInputAttachmentImage.size() || !fIsInputAttachmentImage[imageId]) { | |
| return TransformationState::Unchanged; | |
| } | |
| // The instruction is: | |
| // | |
| // %resultId = OpImageRead %resultTypeId %imageId %coordinateId | |
| // | |
| // It is transformed into: | |
| // | |
| // %sample = OpLoad %kIdTypeInt %SampleIdVar | |
| // %resultId = OpImageRead %resultTypeId %imageId %coordinateId Sample %sample | |
| const SpvId sample = GetNewId(spirvOut); | |
| spirvOut->push_back(MakeInstructionOp(SpvOpLoad, 4)); | |
| spirvOut->push_back(SkSL::spirv::kIdTypeInt); | |
| spirvOut->push_back(sample); | |
| spirvOut->push_back(fSampleIdVarId); | |
| spirvOut->push_back(MakeInstructionOp(SpvOpImageRead, 7)); | |
| spirvOut->push_back(resultTypeId); | |
| spirvOut->push_back(resultId); | |
| spirvOut->push_back(imageId); | |
| spirvOut->push_back(coordinateId); | |
| spirvOut->push_back(SpvImageOperandsSampleMask); | |
| spirvOut->push_back(sample); | |
| return TransformationState::Transformed; | |
| } | |
| // A SPIR-V transformer. It walks the instructions and modifies them as necessary, for example to | |
| // modify input attachment load when multisampled. | |
| class SpirvTransformer { | |
| public: | |
| SpirvTransformer(const SpirvBlob& spirvIn, | |
| const SPIRVTransformOptions& options, | |
| SpirvBlob* spirvOut) | |
| : fSpirvIn(spirvIn) | |
| , fSpirvOut(spirvOut) | |
| , fOptions(options) | |
| , fMultisampleTransformer(spirvIn[kIDBoundIndex]) { | |
| // Preallocate memory for the result, with some room for additions by transformations. | |
| fSpirvOut->reserve(fSpirvIn.size() + 64); | |
| } | |
| void transform(); | |
| private: | |
| void onTransformBegin(); | |
| const uint32_t* getCurrentInstruction(SpvOp* opCodeOut, uint32_t* wordCountOut) const; | |
| // Transform instructions: | |
| void transformInstruction(); | |
| // SPIR-V to transform: | |
| const SpirvBlob& fSpirvIn; | |
| // Transformed SPIR-V: | |
| SpirvBlob* fSpirvOut; | |
| // Traversal state: | |
| size_t fCurrentWord = 0; | |
| bool fIsInFunctionSection = false; | |
| // Transformation state: | |
| SPIRVTransformOptions fOptions; | |
| SpirvMultisampleTransformer fMultisampleTransformer; | |
| // Instructions that potentially need transformation. They return Transformed if the instruction | |
| // is transformed. If Unchanged is returned, the instruction should be copied as-is. | |
| TransformationState transformOpCapability(const uint32_t* instruction); | |
| TransformationState transformOpEntryPoint(const uint32_t* instruction); | |
| TransformationState transformOpDecorate(const uint32_t* instruction); | |
| TransformationState transformOpTypePointer(const uint32_t* instruction); | |
| TransformationState transformOpTypeImage(const uint32_t* instruction); | |
| TransformationState transformOpLoad(const uint32_t* instruction); | |
| TransformationState transformOpImageRead(const uint32_t* instruction); | |
| }; | |
| void SpirvTransformer::onTransformBegin() { | |
| // SPIR-V is expected to be valid. | |
| SkASSERT(fSpirvIn.size() >= kInstructionsStartIndex); | |
| // Make sure the transformer is not reused. | |
| SkASSERT(fCurrentWord == 0); | |
| SkASSERT(fIsInFunctionSection == false); | |
| // Make sure the output SPIR-V storage is not reused. | |
| SkASSERT(fSpirvOut->empty()); | |
| // Copy the SPIR-V header to the output right away. This is needed for GetNewId() to work. | |
| fSpirvOut->assign(fSpirvIn.begin(), fSpirvIn.begin() + kInstructionsStartIndex); | |
| fCurrentWord = kInstructionsStartIndex; | |
| } | |
| const uint32_t* SpirvTransformer::getCurrentInstruction(SpvOp* opCodeOut, | |
| uint32_t* wordCountOut) const { | |
| SkASSERT(fCurrentWord < fSpirvIn.size()); | |
| const uint32_t* instruction = &fSpirvIn[fCurrentWord]; | |
| *opCodeOut = GetInstructionOp(instruction); | |
| *wordCountOut = GetInstructionWordCount(instruction); | |
| // Basic validity check, instruction cound must not exceed SPIR-V size. | |
| SkASSERT(fCurrentWord + *wordCountOut <= fSpirvIn.size()); | |
| return instruction; | |
| } | |
| void SpirvTransformer::transform() { | |
| this->onTransformBegin(); | |
| while (fCurrentWord < fSpirvIn.size()) { | |
| this->transformInstruction(); | |
| } | |
| } | |
| void SpirvTransformer::transformInstruction() { | |
| uint32_t wordCount; | |
| SpvOp opCode; | |
| const uint32_t* instruction = this->getCurrentInstruction(&opCode, &wordCount); | |
| if (opCode == SpvOpFunction) { | |
| // SPIR-V is structured in sections. Function declarations come last. Only a few | |
| // instructions inside functions need to be inspected. | |
| fIsInFunctionSection = true; | |
| } | |
| // Only look at interesting instructions. | |
| TransformationState transformationState = TransformationState::Unchanged; | |
| if (fIsInFunctionSection) { | |
| // Look at in-function opcodes. | |
| switch (opCode) { | |
| case SpvOpLoad: | |
| transformationState = this->transformOpLoad(instruction); | |
| break; | |
| case SpvOpImageRead: | |
| transformationState = this->transformOpImageRead(instruction); | |
| break; | |
| default: | |
| break; | |
| } | |
| } else { | |
| // Look at global declaration opcodes. | |
| switch (opCode) { | |
| case SpvOpCapability: | |
| transformationState = this->transformOpCapability(instruction); | |
| break; | |
| case SpvOpEntryPoint: | |
| transformationState = this->transformOpEntryPoint(instruction); | |
| break; | |
| case SpvOpDecorate: | |
| transformationState = this->transformOpDecorate(instruction); | |
| break; | |
| case SpvOpTypePointer: | |
| transformationState = this->transformOpTypePointer(instruction); | |
| break; | |
| case SpvOpTypeImage: | |
| transformationState = this->transformOpTypeImage(instruction); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| // If the instruction was not transformed, copy it to output as is. | |
| if (transformationState == TransformationState::Unchanged) { | |
| CopyInstruction(instruction, wordCount, fSpirvOut); | |
| } | |
| // Advance to next instruction. | |
| fCurrentWord += wordCount; | |
| } | |
| TransformationState SpirvTransformer::transformOpCapability(const uint32_t* instruction) { | |
| const SpvCapability capability = static_cast<SpvCapability>(instruction[1]); | |
| if (fOptions.fMultisampleInputLoad) { | |
| fMultisampleTransformer.transformOpCapability(capability, fSpirvOut); | |
| } | |
| return TransformationState::Unchanged; | |
| } | |
| TransformationState SpirvTransformer::transformOpEntryPoint(const uint32_t* instruction) { | |
| // Note: currently there is only one transformer, so control can be given to that. If more | |
| // transformers are added in the future, this function should instead take the list of interface | |
| // ids out, pass them to each transformer for a chance to modify it, then rewrite the entry | |
| // point once with the result. | |
| if (fOptions.fMultisampleInputLoad) { | |
| return fMultisampleTransformer.transformOpEntryPoint(instruction, fSpirvOut); | |
| } | |
| return TransformationState::Unchanged; | |
| } | |
| TransformationState SpirvTransformer::transformOpDecorate(const uint32_t* instruction) { | |
| if (fOptions.fMultisampleInputLoad) { | |
| fMultisampleTransformer.transformOpDecorate(fSpirvOut); | |
| } | |
| return TransformationState::Unchanged; | |
| } | |
| TransformationState SpirvTransformer::transformOpTypePointer(const uint32_t* instruction) { | |
| if (fOptions.fMultisampleInputLoad) { | |
| if (fMultisampleTransformer.transformOpTypePointer(instruction, fSpirvOut) == | |
| TransformationState::Transformed) { | |
| return TransformationState::Transformed; | |
| } | |
| } | |
| return TransformationState::Unchanged; | |
| } | |
| TransformationState SpirvTransformer::transformOpTypeImage(const uint32_t* instruction) { | |
| const SpvId resultId = instruction[1]; | |
| const SpvId sampledTypeId = instruction[2]; | |
| if (fOptions.fMultisampleInputLoad) { | |
| if (fMultisampleTransformer.transformOpTypeImage(resultId, sampledTypeId, fSpirvOut) == | |
| TransformationState::Transformed) { | |
| return TransformationState::Transformed; | |
| } | |
| } | |
| return TransformationState::Unchanged; | |
| } | |
| TransformationState SpirvTransformer::transformOpLoad(const uint32_t* instruction) { | |
| const SpvId resultTypeId = instruction[1]; | |
| const SpvId resultId = instruction[2]; | |
| const SpvId pointerId = instruction[3]; | |
| if (fOptions.fMultisampleInputLoad) { | |
| fMultisampleTransformer.transformOpLoad(resultTypeId, resultId, pointerId, fSpirvOut); | |
| } | |
| return TransformationState::Unchanged; | |
| } | |
| TransformationState SpirvTransformer::transformOpImageRead(const uint32_t* instruction) { | |
| const SpvId resultTypeId = instruction[1]; | |
| const SpvId resultId = instruction[2]; | |
| const SpvId imageId = instruction[3]; | |
| const SpvId coordinateId = instruction[4]; | |
| if (fOptions.fMultisampleInputLoad) { | |
| if (fMultisampleTransformer.transformOpImageRead( | |
| resultTypeId, resultId, imageId, coordinateId, fSpirvOut) == | |
| TransformationState::Transformed) { | |
| return TransformationState::Transformed; | |
| } | |
| } | |
| return TransformationState::Unchanged; | |
| } | |
| } // anonymous namespace | |
| SkSL::NativeShader TransformSPIRV(const SkSL::NativeShader& spirv, | |
| const SPIRVTransformOptions& options) { | |
| SkSL::NativeShader result; | |
| SpirvTransformer transformer(spirv.fBinary, options, &result.fBinary); | |
| transformer.transform(); | |
| #ifdef SK_DEBUG | |
| // Validate the SPIR-V after performing any transformations. This is rather costly, so only | |
| // do this on debug builds. | |
| static SkSL::Compiler compiler; | |
| if (!SkSL::ValidateSPIRV(compiler.errorReporter(), {spirv.fBinary})) { | |
| SKGPU_LOG_E("SPIR-V transformations yielded invalid SPIR-V."); | |
| } | |
| #endif | |
| return result; | |
| } | |
| } // namespace skgpu::graphite |