blob: f09bc835ce57ae00fe8c329c8c04d978e16afd40 [file] [log] [blame]
/*
* 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