spv-opt
Skill1.1k starsChanged 35 days agoApache-2.0View on GitHub
SPIRV-Tools optimizer pass development, IR manipulation, PassTest registration, and LuisaCompute spvtools::Optimizer integration (presets, LUISA_SPIRV_OPT_* env knobs).
Trending
Quiet since July.thin dataabout the whole repository
Who's talking about LuisaGroup/LuisaCompute
- Stack Overflow“c++ error: ‘struct std::atomic_flag’ has no member named ‘test’”Stack Overflow · Dec 26, 2022 · 1 votes
- Tencent/ncnn“optimize multi-head attention (increased shared-memory tile size, add fused kernel)”GitHub · Jul 10, 2026
- bazelbuild/bazel-central-registry“wanted: [LuisaGroup/LuisaCompute]”GitHub · Nov 8, 2025
- MuGdxy/SymEigen“Try to add support for luisa-compute”GitHub · Sep 15, 2025
- LuisaGroup/LuisaRender“Remove `util/u64.h` and use `Var<ulong>` instead”GitHub · Jun 2, 2024
The biggest 5 of 5 mentions linking to the repository that our crawl found. Each line is their own headline.
In this repository22 other files it gives its agents.
- .github/copilot-instructions.mdCopilot instructions
- astSkill
- backend-architectureSkill
- cmakeSkill
- cpp-styleSkill
- debugSkill
- gitSkill
- glslangSkill
- hlslSkill
- ir-pipelineSkill
- lc_apiSkill
- lc_native_shaderSkill
- lc_optimizeSkill
- llvm-spirvSkill
- lspSkill
- project_structureSkill
- renderdocSkill
- spirv_codegenSkill
- testSkill
- xir-passesSkill
- xmakeSkill
- yyjsonSkill
The file
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 1 is distinctive, and 11 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 55
If you modify the module, return Status::SuccessWithChange.
- Distinctive: line 224
When deleting instructions while iterating, collect first and kill after:
- Very specific: line 332
Add header to source/opt/passes.h (or include directly).
- Very specific: line 333
Add CreateMyPassPass() factory declaration to include/spirv-tools/optimizer.hpp:
- Very specific: line 345
Add CLI flag mapping in source/opt/optimizer.cpp inside Optimizer::RegisterPassFromFlag:
- Very specific: line 352
Add source files to source/opt/CMakeLists.txt:
- Very specific: line 360
Add a test target/file under test/opt/ (e.g.
- Very specific: line 362
Look at nearby passes in RegisterPassFromFlag for the exact pattern.
- Very specific: line 375
After Process() returns SuccessWithChange, Pass::Run() itself (not PassManager) automatically calls:
- Very specific: line 380
If you mutate IDs outside normal helpers (e.g.
- Very specific: line 382
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..
- Very specific: line 398
All three also have overloads taking a bool preserve_interface argument.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
SPIRV-Tools Optimizer
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::optRules:
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;Pass::Run()invalidates analyses not listed inGetPreservedAnalyses()(source/opt/pass.cpp). - 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 checkInstruction
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->ToBinaryWithoutAttachedDebugInsts(&words);
// Predicates
inst->IsBranch();
inst->IsBlockTerminator();
inst->IsDecoration();
inst->IsConstant();
inst->IsLoad();
inst->IsNop();
inst->ToNop(); // turns instruction into OpNopBasicBlock
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.ForEachSuccessorLabel([](uint32_t* id){ }); // NOTE: callback takes uint32_t*
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()) { // func is a Function&
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
%fn = OpTypeFunction %void
%main = OpFunction %void None %fn
%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
%fn = OpTypeFunction %void
%main = OpFunction %void None %fn
%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, Pass::Run() itself (not PassManager) 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.
Repo Integration (LuisaCompute)
LuisaCompute consumes SPIRV-Tools as a vendored library and registers no custom Pass subclasses of its own — the "Registering a Pass" workflow above only applies when editing src/ext/SPIRV-Tools itself. The integration point is lc::spirv::optimize_spirv() in src/backends/common/spirv/spirv_codegen/optimizer.cpp (called from entry.cpp):
- Builds a
spvtools::OptimizerforSPV_ENV_VULKAN_1_2and registers only built-inspvtools::Create*Pass()factories, then runs the pipeline in a fixed-point loop until the binary stops changing. - Presets (
SpirvOptimizerOptions, default level 2 →compute):none;lightweight= adce, block-merge, simplification, dead-branch-elim;compute=register_compute_passes()(DXC-like pipeline;loop-unrollis registered only when the input already carries an explicitUnrollOpLoopMergecontrol bit);full=RegisterPerformancePasses()+ private-to-local + copy-propagate-arrays + spread-volatile-semantics + compact-ids.trim-capabilitiesis appended only for modules within the audited capability allowlist (can_safely_trim_capabilities). - Env knobs read in the same file:
LUISA_SPIRV_OPT_LEVEL(int),LUISA_SPIRV_OPT_PASSES(preset name),LUISA_SPIRV_OPT_PASS_FLAGS(comma/space-separated--pass[=arg]flags appended after the preset viaOptimizer::RegisterPassesFromFlags; an invalid flag fails closed and keeps the input binary),LUISA_SPIRV_OPT_MAX_ITERATIONS(fixed-point cap, 1–10, default 5),LUISA_SPIRV_OPT_SROA_LIMIT(scalar-replacement limit, default 100, 0 = unlimited),LUISA_SPIRV_OPT_MAX_ID_BOUND,LUISA_SPIRV_OPT_PRESERVE_BINDINGS(1/true/0/false). - To exercise any upstream pass (or the whole registry) on a built runtime without code changes, set e.g.
LUISA_SPIRV_OPT_PASS_FLAGS=--strength-reduction.
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 |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

