SPIRV-Tools Optimizer
SkillDev toolsSPIRV-Tools optimizer pass development, IR manipulation, and PassTest registration.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the SPIRV-Tools Optimizer skill
What this skill tells your AI
The instructions your AI receives, as published by luisagroup/luisacompute in .agents/skills/spv-opt/SKILL.md and read by ahel’s review.
Writing optimizer passes for SPIRV-Tools (src/ext/SPIRV-Tools).
Pass Skeleton
Derive from spvtools::opt::Pass (or MemPass for mem2reg-style passes), implement name() and Process().
// source/opt/my_pass.h
#ifndef SOURCE_OPT_MY_PASS_H_
#define SOURCE_OPT_MY_PASS_H_
#include "source/opt/pass.h"
namespace spvtools::opt {
class MyPass : public Pass {
public:
const char* name() const override { return "my-pass"; }
Status Process() override;
IRContext::Analysis GetPreservedAnalyses() override {
return IRContext::kAnalysisDefUse | IRContext::kAnalysisCFG;
}
};
} // namespace spvtools::opt
#endif
// source/opt/my_pass.cpp
#include "source/opt/my_pass.h"
namespace spvtools::opt {
Pass::Status MyPass::Process() {
bool modified = false;
// Iterate functions
for (auto& func : *get_module()) {
for (auto& block : func) {
for (auto& inst : block) {
// transform inst...
modified = true;
}
}
}
return modified ? Status::SuccessWithChange : Status::SuccessWithoutChange;
}
} // namespace spvtools::opt
Rules:
name()must match the--my-passCLI flag used inRegisterPassFromFlag(no leading hyphens).Process()must returnStatus::Failureonly on real errors.- If you modify the module, return
Status::SuccessWithChange; the pass manager invalidates analyses not listed inGetPreservedAnalyses(). - A single pass instance may only run once; internal state does not reset.
MemPass base class
Many load/store elimination passes derive from MemPass instead of Pass:
#include "source/opt/mem_pass.h"
class MyMemPass : public MemPass {
// Inherits helpers: GetPtr(), IsTargetVar(), CollectTargetVars(),
// HasOnlyNamesAndDecorates(), KillAllInsts(), Type2Undef(), etc.
};
Key APIs
Module / IRContext
Module* m = get_module(); // or context()->module()
IRContext* ctx = context();
m->ForEachInst([](Instruction* inst){ /* all insts */ }, true);
ctx->get_def_use_mgr(); // analysis::DefUseManager
ctx->get_type_mgr(); // analysis::TypeManager
ctx->get_constant_mgr(); // analysis::ConstantManager
ctx->get_decoration_mgr(); // analysis::DecorationManager
ctx->cfg(); // CFG
ctx->GetValueNumberTable(); // ValueNumberTable
ctx->GetStructuredCFGAnalysis(); // StructuredCFGAnalysis
ctx->InvalidateAnalyses(IRContext::kAnalysisDefUse | IRContext::kAnalysisCFG);
ctx->IsConsistent(); // debug invariant check
Instruction
spv::Op opcode = inst->opcode();
uint32_t rid = inst->result_id(); // 0 if none
uint32_t tid = inst->type_id(); // 0 if none
// Operands: inst->begin() .. inst->end()
for (auto& op : *inst) {
if (spvIsIdType(op.type)) { uint32_t id = op.words[0]; }
}
uint32_t val = inst->GetSingleWordInOperand(idx);
inst->NumInOperands();
inst->SetResultId(new_id);
inst->SetResultType(new_type_id);
inst->SetInOperand(idx, {new_val});
inst->ToBinary(&words);
// Predicates
inst->IsBranch();
inst->IsBlockTerminator();
inst->IsDecoration();
inst->IsConstant();
inst->IsLoad();
inst->IsNop();
inst->ToNop(); // turns instruction into OpNop
BasicBlock
for (auto& block : func) { // func is a Function&
uint32_t label = block.id();
Instruction* label_inst = block.GetLabelInst();
Instruction* merge = block.GetMergeInst(); // OpSelectionMerge / OpLoopMerge
Instruction* loop_merge = block.GetLoopMergeInst();
bool has_phi = block.HasPhiInstructions();
// Iterate instructions (label included if you use ForEachInst)
for (auto& inst : block) { }
block.ForEachInst([](Instruction* i){ }, true); // true = include debug lines
// Terminator helpers
block.ForEachSuccessorLabels([](uint32_t id){ });
bool is_loop_header = block.IsLoopHeader();
uint32_t merge_id = block.MergeBlockIdIfAny();
uint32_t continue_id = block.ContinueBlockIdIfAny();
Instruction* term = block.terminator();
}
Function
for (auto& func : *get_module()) {
uint32_t func_id = func->DefInst().result_id();
bool is_declaration = func->IsDeclaration();
func->ForEachParam([](Instruction* param){ });
// iterate blocks (func is a Function& from the module loop)
for (auto& block : func) { }
}
Function does not have an IsEntryPoint() method. Check entry points via the module:
bool IsEntryPoint(Function* func, Module* module) {
for (auto& entry : module->entry_points()) {
// OpEntryPoint: operand 0 = execution model, operand 1 = function id
if (entry.GetSingleWordInOperand(1) == func->result_id()) return true;
}
return false;
}
Building Instructions
#include "source/opt/ir_builder.h"
// Insert before an instruction
InstructionBuilder b(context(), insertion_point,
IRContext::kAnalysisInstrToBlockMapping | IRContext::kAnalysisDefUse);
// Or append to end of a block
InstructionBuilder b(context(), parent_block,
IRContext::kAnalysisInstrToBlockMapping | IRContext::kAnalysisDefUse);
Instruction* add = b.AddBinaryOp(type_id, spv::Op::OpIAdd, lhs, rhs);
Instruction* extract = b.AddCompositeExtract(elem_type_id, composite_id, {idx0, idx1});
Instruction* construct = b.AddCompositeConstruct(type_id, {id0, id1});
Instruction* load = b.AddLoad(type_id, ptr_id);
Instruction* store = b.AddStore(ptr_id, value_id);
Instruction* branch = b.AddBranch(target_id);
Instruction* cbranch = b.AddConditionalBranch(cond_id, true_id, false_id);
Instruction* cbranch_with_merge = b.AddConditionalBranch(cond_id, true_id, false_id, merge_id);
Instruction* phi = b.AddPhi(type_id, {val0, block0, val1, block1});
Instruction* unary = b.AddUnaryOp(type_id, spv::Op::OpConvertFToS, operand);
Instruction* nullary = b.AddNullaryOp(type_id, spv::Op::OpGroupAll);
Instruction* select = b.AddSelect(type_id, cond_id, true_id, false_id);
Instruction* access = b.AddAccessChain(ptr_type_id, base_ptr_id, {idx_id0, idx_id1});
Instruction* var = b.AddVariable(ptr_type_id, static_cast<uint32_t>(spv::StorageClass::Function));
InstructionBuilder can only preserve kAnalysisDefUse and kAnalysisInstrToBlockMapping; other analyses must be invalidated/rebuilt explicitly.
Replacing / Killing
// Replace all uses of old_id with new_id
ctx->ReplaceAllUsesWith(old_id, new_id);
// Replace uses only when predicate returns true
ctx->ReplaceAllUsesWithPredicate(old_id, new_id, [](Instruction* user) {
return user->opcode() == spv::Op::OpStore;
});
// Kill an instruction (removes from block, updates def-use)
ctx->KillInst(inst);
// Kill an id and all its uses
ctx->KillDef(id);
// Get the defining instruction for an id
Instruction* def = ctx->get_def_use_mgr()->GetDef(id);
// Get users of an id
ctx->get_def_use_mgr()->ForEachUser(id, [](Instruction* user){ });
uint32_t n = ctx->get_def_use_mgr()->NumUsers(id);
Safe Iteration & Phi Nodes
When deleting instructions while iterating, collect first and kill after:
std::vector<Instruction*> to_kill;
get_module()->ForEachInst([&](Instruction* inst) {
if (inst->IsNop()) to_kill.push_back(inst);
}, false);
for (auto* inst : to_kill) context()->KillInst(inst);
Phi operands arrive as (value_id, parent_block_id) pairs:
if (inst->opcode() == spv::Op::OpPhi) {
for (uint32_t i = 0; i + 1 < inst->NumInOperands(); i += 2) {
uint32_t value = inst->GetSingleWordInOperand(i);
uint32_t parent = inst->GetSingleWordInOperand(i + 1);
}
}
Testing
Tests live in test/opt/. Use PassTest<::testing::Test> fixture.
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "test/opt/pass_fixture.h"
#include "test/opt/pass_utils.h"
#include "source/opt/my_pass.h"
using MyPassTest = PassTest<::testing::Test>;
TEST_F(MyPassTest, Basic) {
const std::string before = R"(
OpCapability Shader
OpMemoryModel Logical GLSL450
%void = OpTypeVoid
%main = OpFunction %void None %void
%entry = OpLabel
OpReturn
OpFunctionEnd
)";
const std::string after = before;
SinglePassRunAndCheck<MyPass>(before, after, /*skip_nop=*/false, /*do_validation=*/false);
}
Fixture helpers:
SinglePassRunAndCheck<PassT>(before, after, skip_nop, do_validation, args...)— exact match.SinglePassRunAndCheck<PassT>(before, after, skip_nop, args...)— overload without validation.SinglePassRunAndMatch<PassT>(original, do_validation, args...)— runs pass, disassembles, then checks with EffceeCHECK:patterns embedded inoriginal. Always skips OpNop. Returnsstd::tuple<std::string, Pass::Status>.SinglePassRunAndFail<PassT>(original, args...)— expectsStatus::Failure, checks error messages with EffceeCHECK:patterns embedded inoriginal.SinglePassRunToBinary<PassT>(assembly, skip_nop, args...)— returnsstd::tuple<std::vector<uint32_t>, Pass::Status>.SetAssembleOptions(SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS)— keep ids from assembly.SetDisassembleOptions(SPV_BINARY_TO_TEXT_OPTION_NO_HEADER)— omit SPIR-V header in output.SetTargetEnv(spv_target_env)— change target environment (defaultSPV_ENV_UNIVERSAL_1_3).
For match tests, embed CHECK: lines as comments in the assembly string:
const std::string assembly = R"(
; CHECK: OpReturn
OpCapability Shader
OpMemoryModel Logical GLSL450
%void = OpTypeVoid
%main = OpFunction %void None %void
%entry = OpLabel
OpReturn
OpFunctionEnd
)";
SinglePassRunAndMatch<MyPass>(assembly, false);
Multi-pass tests
TEST_F(MyPassTest, Pipeline) {
const std::string before = R"(... )";
const std::string after = R"(... )";
AddPass<MyFirstPass>();
AddPass<MySecondPass>();
RunAndCheck(before, after);
}
Manual context tests
#include "source/opt/build_module.h"
TEST(MyPass, Manual) {
std::unique_ptr<IRContext> ctx =
BuildModule(SPV_ENV_UNIVERSAL_1_3, nullptr, assembly,
SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS);
ASSERT_NE(ctx, nullptr);
MyPass pass;
auto status = pass.Run(ctx.get());
EXPECT_EQ(status, Pass::Status::SuccessWithChange);
EXPECT_TRUE(ctx->IsConsistent());
}
Registering a Pass
- Add header to
source/opt/passes.h(or include directly). - Add
CreateMyPassPass()factory declaration toinclude/spirv-tools/optimizer.hpp:
Optimizer::PassToken CreateMyPassPass();
- Implement factory in
source/opt/optimizer.cpp:
Optimizer::PassToken CreateMyPassPass() {
return MakeUnique<Optimizer::PassToken::Impl>(MakeUnique<opt::MyPass>());
}
- Add CLI flag mapping in
source/opt/optimizer.cppinsideOptimizer::RegisterPassFromFlag:
} else if (pass_name == "my-pass") {
RegisterPass(CreateMyPassPass());
- Add source files to
source/opt/CMakeLists.txt:
my_pass.h
...
my_pass.cpp
- Add a test target/file under
test/opt/(e.g.my_pass_test.cpp) and list it intest/opt/CMakeLists.txt.
Look at nearby passes in RegisterPassFromFlag for the exact pattern. Passes with arguments parse pass_args before calling RegisterPass(...).
Analyses & Invalidation
Available IRContext::Analysis bits:
kAnalysisNone,kAnalysisDefUse,kAnalysisInstrToBlockMapping,kAnalysisDecorationskAnalysisCombinatorskAnalysisCFG,kAnalysisDominatorAnalysis,kAnalysisLoopAnalysiskAnalysisNameMap,kAnalysisScalarEvolution,kAnalysisRegisterPressurekAnalysisValueNumberTable,kAnalysisStructuredCFG,kAnalysisBuiltinVarIdkAnalysisIdToFuncMapping,kAnalysisConstants,kAnalysisTypeskAnalysisDebugInfo,kAnalysisLiveness,kAnalysisIdToGraphMapping
After Process() returns SuccessWithChange, the pass manager automatically calls:
ctx->InvalidateAnalysesExceptFor(GetPreservedAnalyses());
If you mutate IDs outside normal helpers (e.g. CompactIdsPass), manually invalidate kAnalysisDebugInfo and any others that become stale mid-pass.
If you need an analysis inside Process() and are not preserving it, it is usually fine to request it via ctx->get_def_use_mgr() etc.; the manager will build it on demand. Just make sure GetPreservedAnalyses() reflects what survives your transformations.
Pass Manager / Recipes
spvtools::Optimizer opt(SPV_ENV_UNIVERSAL_1_3);
opt.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&, const char* msg) {
std::cerr << msg << std::endl;
});
opt.RegisterPass(spvtools::CreateCompactIdsPass())
.RegisterPass(spvtools::CreateAggressiveDCEPass());
opt.Run(binary.data(), binary.size(), &optimized);
Built-in recipes:
RegisterPerformancePasses()/RegisterSizePasses()/RegisterLegalizationPasses()- All three also have overloads taking a
bool preserve_interfaceargument.
File Map
| File | Purpose |
|---|---|
source/opt/pass.h / pass.cpp | Base Pass class |
source/opt/mem_pass.h / mem_pass.cpp | MemPass base for mem2reg-style passes |
source/opt/empty_pass.h / null_pass.h | No-op passes for testing |
source/opt/ir_context.h / ir_context.cpp | IRContext, analysis management |
source/opt/module.h | Module, header, section lists |
source/opt/function.h | Function |
source/opt/basic_block.h | BasicBlock |
source/opt/instruction.h | Instruction, Operand, DebugScope |
source/opt/ir_builder.h | InstructionBuilder |
source/opt/def_use_manager.h | DefUseManager |
source/opt/type_manager.h | TypeManager |
source/opt/constants.h | ConstantManager |
source/opt/cfg.h | CFG |
source/opt/fold.h | Folder (constant folding) |
source/opt/passes.h | Unified include for all pass headers |
test/opt/pass_fixture.h | PassTest fixture |
include/spirv-tools/optimizer.hpp | Public C++ API |
source/opt/optimizer.cpp | Pass factories & CLI flag dispatch |
examples/cpp-interface/main.cpp | Example of assemble / validate / optimize / disassemble |
Signals
- GitHub stars
- 1k
- Forks
- 108
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
spv-opt- Source
- github.com/luisagroup/luisacompute