本文へ移動
cccskills
無料GitHub で公開

spirv_codegen

Native Vulkan XIR-to-SPIR-V codegen, legalization, validation, bindings, control flow, and target features.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md38.3 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Native XIR to SPIR-V

The native code generator lives in src/backends/common/spirv/spirv_codegen/ and lowers XIR to SPIR-V 1.5 with glslang's spv::Builder. Vulkan enables it with LUISA_COMPUTE_ENABLE_VK_XIR_SPIRV (CMake) or lc_vk_backend_use_xir_spirv (xmake).

This skill describes the native path, not the separate AST -> LLVM -> SPIR-V implementation in spirv_llvm/.

Source map

FilesResponsibility
entry.h/.cppPublic compile entries, target-feature state, result assembly, validation and optimization
utils.h/.cppAST -> XIR and the backend legalization pipeline
dialect.h/.cppFail-closed XIR handoff validation
pointer_legalization.h/.cppSPIR-V callable ABI specialization policy
argument_usage.h/.cppFixed-point function-argument usage shared by legalization and emission
bindless_usage.h/.cppExact global-heap and per-array metadata requirements by resource opcode
texture_sampling.h/.cppCanonical sampler selector, image dimensionality, and target-contract planning
structural_closure.h/.cppCanonical backend-emitted block closure and malformed-role diagnostics
call_graph_validation.h/.cppKernel-reachable, callee-before-caller function graph
control_flow_plan.h/.cppImmutable logical-to-physical structured-CFG plan
instruction_layout.h/.cppSPIR-V instruction word-count limits, including OpSwitch and OpPhi
buffer_layout.h/.cppVulkan typed-SSBO layout compatibility and word-storage fallback planning
aggregate_index.h/.cppTyped GEP/extract/insert index planning and struct-index canonicalization
optimizer.h/.cppSPIRV-Tools validation and optimization presets
target_feature_mask.hPersisted required-feature bit contract
target_features.hLogical-device feature snapshot and pure lowering decisions
runtime_target_plan.h/.cppPre-binding descriptor, ray-query, subgroup, and bindless runtime contract
kernel_argument_role.hStable per-argument native acceleration-structure role bits
src/backends/vk/shader_artifact_codec.h/.cppCanonical Vulkan shader-artifact writer/parser, integrity checks, SPIR-V validation and feature reconciliation
bind.cppDescriptor properties, argument buffer, bindless heaps, constant UBO
type.cppLogical and storage-layout type conversion
emit.cppModule/function/block emission, prologues, native Phi emission
condition_inst.cppEmission from the frozen control-flow plan
instruction.cppNon-control instructions, resources, atomics, ray query, builtins

CMake glob-registers spirv_codegen/*.cpp in luisa-compute-spirv; xmake does the same for lc-spirv.

Compilation contract

There are two supported entries:

SpirvResult compile_spirv(Function kernel, const ShaderOption &,
                          SpirvTargetFeatures);
SpirvResult compile_spirv_xir(Function kernel, const xir::Module *,
                              const ShaderOption &,
                              SpirvTargetFeatures);

compile_spirv translates and legalizes AST input. compile_spirv_xir is for an already legalized module and is used by exact-XIR backend tests. The AST Function remains the external descriptor/argument ABI in both cases.

The compile sequence is:

  1. Validate the AST/XIR kernel ABI and the native SPIR-V XIR dialect. Generic XIR validity remains whole-module; backend-specific work uses the canonical kernel-reachable call graph and each reachable function's structural closure.
  2. Freeze the reachable functions, used types/constants, exact bindless usage, atomic-buffer representation, atomic target contract, sampler contract, and runtime-only target-feature plan before descriptor or instruction emission.
  3. Compute fixed-point argument usage and exact per-argument acceleration and bindless-metadata roles. Merge only the public synchronization usage that is deliberately conservative; descriptor roles remain optimized-XIR exact.
  4. Generate descriptor properties, the internal argument block, global heaps, and per-argument resource globals from those immutable plans.
  5. Emit reachable functions through immutable control-flow plans.
  6. Dump the module and validate it for SPV_ENV_VULKAN_1_2.
  7. Run the selected SPIRV-Tools optimizer preset and commit its output only if optimization and validation both succeed.
  8. Validate the selected final binary, reconcile capability-owned target bits, then return the exact properties, argument usage/roles, and feature mask that serialization and runtime binding consume.

The pre-optimization module must be valid. The optimizer is never a repair step for malformed SPIR-V.

The same rule applies inside emission: generic XIR, the native dialect, and the frozen AST/XIR ABI are hard invariants. Do not coerce invalid arithmetic operands or synthesize missing kernel arguments. Assert if a verified boolean select or planned argument buffer does not lower as expected. _ensure_type is an internal same-shape numeric conversion helper, not a general cast operation: boolean conversion belongs to XIR STATIC_CAST, and unsupported class or shape pairs must fail rather than fall through to OpBitcast.

Emit ordinary XIR arithmetic as ordinary SPIR-V instructions. Do not turn constant operands into OpSpecConstantOp: the runtime exposes no matching specialization-constant ABI. Keep optional integer constant folding and strength reduction in the XIR/SPIRV-Tools optimization layers instead of embedding partial, always-on peepholes in instruction selection.

SpirvCodegenEntry owns its spv::Builder normally. Its destructor first clears maps keyed by XIR objects, then the builder is destroyed. Do not add a release() leak workaround.

AST to XIR legalization

The mandatory final pipeline in utils.cpp establishes backend semantics even when optional optimization is disabled:

  1. Lower ray-query loops to ordinary loop form.
  2. Promote safe read-only reference arguments.
  3. Optionally optimize still-structured XIR.
  4. Run destructure_cfg.
  5. Run SPIR-V pointer/resource-call specialization.
  6. Optionally run ordinary inlining and scalar/SSA cleanup.
  7. Temporarily run reg2mem because restructure_cfg currently requires Phi-free raw CFG.
  8. Run restructure_cfg.
  9. Clear payload only from blocks outside ordinary all-edge reachability and immediately run mem2reg, then audit that no typed reg2mem spill provenance remains; retain every block identity.
  10. Fix self-referential values and validate the dialect.

The temporary reg2mem is a boundary adapter for restructuring, not the codegen representation. Do not add a final blanket Phi elimination. Native OpPhi emission owns reconstructed SSA and avoids imposing avoidable memory traffic on the generated module or on spirv-opt.

Every temporary slot created by generic reg2mem carries Reg2MemSpillMD with PHI or CROSS_BLOCK provenance. Preserve this typed metadata through instruction cloning, SROA splitting, and XIR text/bitcode round-trips; names and comments are diagnostic only. Ordinary final legalization recovers SSA immediately after restructuring. Pre-autodiff legalization is the intentional exception: autodiff requires Phi-free IR, so typed spills remain in memory form across autodiff and SROA. The final post-restructure audit then examines every metadata owner and fails if any marker remains or is misplaced. The exact-XIR dialect independently rejects a tagged alloca in its active structural closure. Untagged user local allocas remain a supported XIR/SPIR-V construct and must not be rejected by this boundary.

Generic multi-block inlining must run after destructure_cfg. A target-specific exception belongs in pointer_legalization.cpp, which uses the generic atomic inline_call_sites_pass_run_on_module primitive only after preflighting the whole selected batch.

Dialect boundary

validate_spirv_xir_codegen_dialect is the single fail-closed handoff. When adding an XIR opcode or type:

  • classify it explicitly as supported, semantic no-op, or unsupported;
  • validate operand count, types, storage class and backend semantic limits;
  • add a focused diagnostic rather than relying on an emitter assertion;
  • update the complete opcode-matrix test;
  • add an exact codegen test when the construct is accepted.

Keep two scopes deliberately separate. Generic xir_verify_module validity is a whole-module contract, including unused definitions and orphan blocks. The native dialect, target/resource planners, and emitter consume only the canonical kernel-reachable function graph and each function's structural closure. Never weaken generic verification to reachable-only, and never let an unused callable or true orphan manufacture descriptors, target features, or emitted SPIR-V.

Important type boundaries include:

  • OpTypeArray length is strictly positive; XIR may still represent a zero-length host type, so the SPIR-V dialect rejects it;
  • storage ArrayStride is strictly positive; buffers with zero-sized elements and nested storage arrays with zero-sized elements are rejected;
  • SPV_EXT_float8 permits FP8 values only in its listed transport, storage, conversion, composite and selection instructions. General FP8 arithmetic and comparisons remain invalid. Do not classify OpTranspose as transport: it belongs to the matrix-instruction category, which the extension does not admit. FP8-to-bool casts must widen each scalar lane to float32 before the unordered comparison so NaN remains truthy;
  • texture dimensions are 2 or 3 and scalar elements are float32/int32/uint32;
  • texture writes consume exactly the verifier-mandated four-component texel vector. The emitter must not pad, smear, or otherwise repair a scalar or short-vector payload that crossed the dialect boundary;
  • opaque ray-query values have deliberately restricted lifetime and argument rules;
  • kernel reference arguments are not part of the Vulkan descriptor ABI.

Structural closure and inactive payload

All backend analyses and emission use plan_spirv_codegen_structural_closure. It contains the ordinary CFG reachable from the function body plus every raw structured role block recursively owned by that CFG. Function-owned blocks outside this set are true orphans and do not participate in exact native emission, descriptor analysis, uniformity, or planner ownership.

A raw role block can belong to the structural closure without being ordinarily reachable. Exact XIR emission accepts such a disconnected role payload only when it has a self-contained, flat value/lifetime contract: no Phi, nested structured owner, Break/Continue, opaque ray-query state, cross-block instruction value, forward same-block use, or branch re-entry. Its terminator must be Return or Unreachable. The emitter and resource/callable analyses must still inspect accepted payload because direct exact-XIR codegen has not run a dead-payload pass.

glslang's mandatory postProcess(false) subsequently rewrites an ordinary-unreachable merge or continue target to its canonical unreachable form, so instructions from that payload do not appear in the dumped SPIR-V. Boundary tests must prove structural closure, callable discovery, and resource planning before this canonicalization, then expect the dead payload opcodes to be absent from the final validated binary.

After restructure_cfg, every executable transfer is explicit. clear_spirv_codegen_inactive_block_payloads may therefore replace payload in both true orphans and ordinary-unreachable role blocks with Unreachable before mem2reg. These categories remain distinct: a true orphan is outside the exact-emission closure, while a disconnected role keeps its structural block identity even when this mandatory legalization proves its payload dead. Do not make optional DCE determine closure membership or merge ownership.

Callable pointer and resource legalization

SPIR-V policy must not leak into the generic XIR inliner. The backend computes fixed-point argument usage, then specializes only call sites whose retained callable ABI cannot be represented safely.

Current rules:

  • a reference formal receiving a function-local AllocaInst or compatible reference argument may remain a Function pointer;
  • a shared alloca is Workgroup storage and must be specialized;
  • indirect-dispatch buffer arguments are always specialized;
  • used buffer and bindless resource formals are specialized into the call site;
  • a writable acceleration structure is specialized;
  • a texture used for both read and write is specialized;
  • a genuinely unused resource formal need not force specialization.

Legalization is fixed-point because inlining one layer can expose a pointer at another layer. It must preflight recursion, call shape, structured boundaries, and all selected sites before mutating any function. Ordinary switches that do not block a selected inline remain native switches.

Do not solve callable ABI failures by enabling VariablePointers globally. Descriptor-backed buffer/bindless arguments are specialized, and only safe opaque/resource modes remain as callable parameters.

Structured control flow and Phi

ControlFlowPlan freezes the final physical graph before instruction emission. It owns:

  • reverse-post-order block schedule;
  • construct headers, merge targets and continue targets;
  • synthetic loop headers, continues and edge trampolines;
  • source-sensitive merge routing;
  • physical loop-boundary validation;
  • logical Phi incoming paths through synthetic forwarding blocks.

Emission preallocates every physical block. It then predeclares one OpPhi in the result block and, where necessary, auxiliary OpPhi nodes in forwarding blocks. Incoming values are resolved at the logical predecessor tail before its terminator. Finalization checks that planned and actual physical predecessor sets are identical.

Rules:

  • the physical SPIR-V function entry has no predecessor and no Phi;
  • Loop.prepare has exactly one non-null BasicBlock role operand and has exactly one of two terminator forms: Branch(Loop.body) or ConditionalBranch(bool, Loop.body, Loop.merge); both are native loop headers and lower directly to OpLoopMerge followed by the matching branch;
  • an OpPhi must be the first non-line instruction in its block;
  • every physical predecessor appears exactly once;
  • a loop header has one entry and one backedge, and the backedge passes through the declared continue target;
  • one SPIR-V merge block cannot be owned by multiple constructs;
  • forwarding a logical incoming through a synthetic edge requires a Phi in that forwarding block, not reuse of a non-dominating value;
  • use plan_spirv_phi_instruction and plan_spirv_switch_instruction before allocating variable-length instructions; SPIR-V word count is 16-bit.

Never rediscover or redirect edges during emission. Add facts to ControlFlowPlan, validate them there, then consume the frozen plan.

Types and layout literals

_convert_type handles logical SSA types. _convert_laid_out_type recursively decorates buffer payloads with Offset, ArrayStride, ColMajor, and MatrixStride.

SPIR-V layout and binding literals are unsigned 32-bit words. glslang's single-literal convenience overload takes int; do not route a wide ABI value through it. Use the vector<unsigned> decoration overload for variable offsets, strides, descriptor sets, and bindings. For makeArrayType, a small nonzero third argument may mark the type explicitly laid out, followed by the real unsigned ArrayStride decoration.

Logical bool has no StorageBuffer representation. A second mismatch comes from 64-bit three- and four-component vectors: Vulkan gives them 32-byte standard storage alignment, while Luisa host vector alignment is capped at 16 bytes. plan_spirv_typed_buffer_layout recursively checks matrix/array strides, structure member offsets, structure stride, and the outer runtime-array stride. Any incompatible non-atomic Buffer<T> uses the byte-exact uint32 word ABI. Atomic analysis consumes the same layout decision and selects one representation per Buffer<T> before type conversion; a 64-bit integer atomic that requires typed storage conflicts with a layout that requires word storage and must fail at the handoff. Never enable scalar-block layout implicitly: the runtime does not request that Vulkan feature as part of this ABI.

Direct-buffer StorageBufferMetadata carries a runtime descriptor bias, but the Vulkan argument preprocessor proves that a typed buffer view's offset and size are exact multiples of its logical element stride. Preserve the resulting gcd(element_size, 4) alignment in word-storage reads and writes. Dropping it to one byte needlessly expands ordinary aligned stores into masked atomic-CAS loops. Raw byte-buffer operations have no such proof and remain alignment one.

XIR atomics specify atomicity but expose no memory-order operand. Emit SPIR-V atomics with Relaxed memory semantics while retaining the pointer-derived Device or Workgroup scope. This matches CUDA/HIP Monotonic and fallback __ATOMIC_RELAXED; block synchronization and runtime resource barriers own visibility ordering. Do not attach AcquireRelease or broad memory-class bits to every RMW: that silently strengthens the cross-backend contract and can serialize unrelated atomics. Compare-exchange success and failure semantics, including software float CAS loops, are both relaxed.

Large array constants may use the portable constant UBO planner. Only layouts with an exact host-to-std140 serializer are eligible. Planning is checked for alignment, multiplication, cumulative range, and the portable 16 KiB limit.

Aggregate indices

All GEP, dynamic extract/insert, and atomic address walks use plan_spirv_aggregate_indices.

  • array/vector/matrix/buffer indices retain their legal integer value IDs;
  • structure indices must be constant and are canonicalized to unsigned 32-bit OpConstant IDs;
  • planning validates the whole walk before emission;
  • usage analysis must track the canonical emitted index, not keep an otherwise dead narrow or 64-bit source constant alive.

Bindings and Vulkan ABI

hlsl::Property is a persisted ABI shared by codegen, serialization, Vulkan layout creation, and dispatch binding. The backend uses:

  • set 0: dense local descriptors;
  • set 1: sixteen immutable samplers;
  • sets 2+: enabled buffer, 2D texture, and 3D texture heaps in that order;
  • ConstantValue: a descriptor-free push-constant pseudo-property.

All consumers must ignore ConstantValue during descriptor lookup regardless of property order. Writer and reader both validate the canonical property shape. Runtime planning separately checks ordinary descriptor limits, update-after-bind aggregate limits, acceleration-structure limits, set count, and vkGetDescriptorSetLayoutSupport.

Keep internal emission roles explicit beside each public property. Debug names such as _Global, _bdarr_*, tex2d_heap, and tex3d_heap may describe the generated module, but must not select argument-buffer IDs, bindless table types, texture dimensions, or heap IDs. Unknown public property kinds fail closed before a NoResult ID can enter the interface list.

Native direct-buffer views use an internal argument buffer. Non-resource values are host-layout packed first; StorageBufferMetadata records follow at their natural alignment. HLSL debug validation words use a mutually exclusive trailer. The common checked argument-block planner is the source of truth for both sizing and emission.

Storage-buffer alias decorations describe backing memory, not merely one descriptor access path. Luisa permits the same buffer or overlapping views to be supplied through multiple arguments, including imported native resources and bindless arrays. Emit Aliased on every user-bindable storage-buffer declaration that may participate. A read-only declaration may carry NonWritable only when the module has no writable user-resource path that could alias its backing memory; include direct buffers, bindless buffer stores, writable accel-instance storage, writable textures/external memory, and the custom indirect-dispatch buffer in that proof. Do not infer immutability from the declaration's local Usage::READ alone.

Volatile direct-buffer accesses require three matching SPIR-V facts: a Volatile memory operand on each load/store, the backend's matching device fence, and Coherent on that exact buffer declaration. Propagate coherence as an exact fixed-point argument role through callables; do not mark every buffer with the same element type coherent, because that needlessly disables caching for unrelated resources. Coherent is not a substitute for an uncertain alias contract: omit NonWritable when a read declaration may alias writable user memory, but do not make it coherent. The backend-owned _Global argument block and bindless metadata blocks cannot alias user resources; keep them NonWritable and do not decorate them Aliased or Coherent.

Ordinary XIR LoadInst and StoreInst are exact-typed memory operations: the address is an lvalue of the loaded/stored type, and a stored value is an rvalue. Enforce that contract at the dialect handoff. Do not smear scalars or insert bitcasts in the SPIR-V emitter to make a mismatched store validate; those conversions manufacture semantics for invalid XIR and can hide an upstream pass defect.

Bindless buffer planning has two independent facts. A real bindless read/write needs the global unbounded buffer heap and the matching array's local metadata descriptor; a size-only query needs only that local metadata descriptor. SpirvResult::useBufferBindless therefore means the global heap only. Fixed- point argument analysis emits SPIRVBindlessBufferMetadata only beside each bindless argument that actually needs it; do not turn this back into one module-wide optional descriptor per bindless argument.

The native XIR dialect currently accepts only ordinary MULTIPLE bindless layout operations whose uniformity can be proven from XIR. Typed and explicit uniform-index AST operations remain honest HLSL fallback reasons: XIR resource instructions do not yet preserve the typed slot layout or the caller's uniform-index promise. Do not erase those route guards or map typed operations onto ordinary resource ops. Native support requires first-class orthogonal layout/index-mode flags through AST↔XIR, cloning, verification, text/bitcode, callable argument analysis, persisted Vulkan argument roles, runtime layout checking, and SPIR-V slot resolution before the fallback can be relaxed.

For a divergent descriptor lookup, apply NonUniformEXT to the actual descriptor-array index and preserve it through the resulting access-chain pointer, descriptor load, and consuming image/sampler value as required. Do not decorate prefix structure/array indices. Those are commonly interned constants such as zero; decorating one contaminates every unrelated use of the same module-global SPIR-V ID and may unnecessarily pessimize driver analysis.

Acceleration structures likewise have two independent native roles: traversal uses SPIRVAccel, while instance-property access uses the separate SPIRVAccelInstance/SPIRVAccelInstanceRW buffer. SpirvResult::argument_roles is parallel to argument_usages and persists the exact role mask in SavedArgument::resource_aux. A zero role mask is valid for an unused native accel. The all-ones sentinel means an older/non-native artifact whose role is unspecified; native serialization and dispatch must never infer optional accel descriptors greedily from neighboring properties.

Ray-query traversal

OpRayQueryProceedKHR advances traversal; a true result means traversal is still incomplete. Direct closest-hit tracing must therefore emit a structured loop that calls OpRayQueryProceedKHR until it returns false before reading committed intersection fields. ForceOpaqueKHR removes candidate-intersection handling, but it does not make one call sufficient. Direct any-hit tracing uses TerminateOnFirstHitKHR, so its single proceed call remains intentional.

Keep both sides covered: a structural SPIR-V test should distinguish the closest-hit loop from the any-hit single call, and a Vulkan runtime test should place a farther primitive before an overlapping nearer primitive so a premature committed read cannot accidentally pass.

Persisted shaders include section sizes and hashes for properties, saved arguments, SPIR-V, printers, and constant data. require_recompile must parse and validate the complete artifact; a matching prefix is not sufficient.

shader_artifact_codec is the single production owner of the compute and raster artifact format. Fresh serialization, require_recompile, and live shader loading must all use its encoder/decoder rather than maintaining parallel header or section parsers. The decoder verifies the semantic header, bounded total size, every section digest, the persisted interface and printer records, the Vulkan 1.2 SPIR-V module, and the stage-specific main entry point before returning decoded data. Printer records use a bounded, non-fatal parser for the narrower ShaderPrintFormatter type and brace dialect; artifact text must never be passed directly to the fatal Type::from parser. The codec applies final-capability reconciliation only to XIR_SPIRV; HLSL and LLVM artifacts retain their independently produced feature contracts. Tests should round-trip the production codec through an in-memory BinaryIO and recompute hashes in malformed-SPIR-V and printer fixtures so they cannot pass by exercising only framing helpers.

Target features

Every optional capability has two sides:

  1. codegen records the exact logical-device feature needed by the emitted artifact and rejects it when that feature is unavailable;
  2. the resulting required-feature mask is persisted and checked against the enabled logical-device mask on load.

Operations call _require_target_feature(bit, enabled) at the semantic emission site to record a provisional requirement. Availability validation is deferred until after optimization only for one-to-one capability-owned bits: dead-code elimination may remove the last feature-bearing instruction, and the final SPIRV-Tools trim-capabilities pass may then remove its declaration. Runtime/layout and lowering-owned bits still fail immediately. Codegen reconciles capability-backed bits from the final validated binary before checking the complete logical-device mask and persisting the artifact. This is also how narrow arithmetic remains distinct from narrow storage: Vulkan's 8/16-bit storage capabilities permit restricted loads, stores, and width-only conversions without Int8, Int16, or Float16, while constants and arithmetic retain the shader capability.

Only one-to-one OpCapability requirements are final-binary-owned. Runtime layout and semantic features that cannot be reconstructed from capability declarations—descriptor binding flags, sampler anisotropy, storage-class- specific atomics, subgroup extended types, and ray-query descriptor ABI—stay emission-owned. Never clear them merely because a similarly named capability is absent. Conversely, do not strip unsupported capabilities with a raw binary rewrite. The vendored SPIRV-Tools trim pass has an explicit, incomplete input contract, so codegen registers it only when every declared capability is in the locally audited allowlist. Storage-only UniformAndStorageBuffer16BitAccess without Int16 or Float16 is excluded because this pass revision would remove that live capability. Outside the audited domain, retain provisional requirements conservatively.

Final reconciliation does not make every dead unsupported source operation compilable. Feature-dependent planners still run before SPIR-V exists: buffer float atomics may choose a word/CAS fallback, shared float atomics may reject when no legal fallback exists, sampler anisotropy is validated against the runtime sampler heap contract, and narrow constant UBO lowering is selected only when the matching Uniform-storage feature is enabled. Tests at the final artifact boundary must start from an already legal emitted module and must not imply otherwise.

Vulkan device creation must preserve the same independence. In particular, shaderFloat16, shaderInt8, storageBuffer8BitAccess, uniformAndStorageBuffer8BitAccess, storageBuffer16BitAccess, and uniformAndStorageBuffer16BitAccess are six separate feature bits. Never enable or advertise them as an all-or-nothing width bundle.

Unknown persisted bits fail closed. An imported VkDevice does not reveal which optional features were enabled, so the backend must not infer enabled features from physical-device support. Bindless update-after-bind is disabled for imported devices without an explicit feature contract.

Cache and AOT loading follows a strict integrity order. The fixed-width header has a canonical semantic digest built by appending each field in little-endian order; never hash raw struct padding. Verify that digest before trusting the persisted dialect or feature mask. Then bound all serialized section sizes, read the complete payload, verify every section digest, and validate SPIR-V for the Vulkan environment before interpreting final capabilities. For the serialized XIR_SPIRV dialect only, reconcile all capability-owned bits from the validated module (or the union of stage modules) and require an exact match with the persisted mask. Preserve emission-owned bits from the integrity- protected header, then check the reconciled requirements against the logical device. Do not apply native capability-accounting assumptions to HLSL or LLVM artifacts. Builds with the LLVM SPIR-V path still link the common validator and validate loaded modules before pipeline creation.

Cache consumers also state their required shader identity explicitly. Compute cache reads require shader MD5, type MD5, and the selected codegen dialect on both preflight and actual deserialization; native JIT uses XIR_SPIRV, while HLSL compilation states its own dialect. Raster AOT loading requires the expected type MD5 and HLSL_SPIRV dialect during codec decoding, before any Vulkan pipeline is constructed. Generic AOT loading leaves a dimension unconstrained only through an explicit load-requirements field. This prevents a matching filename or shader hash from crossing a codegen-dialect or argument-type boundary.

When adding a feature, update:

  • SpirvTargetFeatures and target_feature_mask.h;
  • Vulkan physical support query and logical feature enabling;
  • emitted extension/capability;
  • serialized required-feature mask and pipeline-environment hash;
  • exact supported, missing, and cache-load tests.

Constants and exact bits

Floating constants use their exact XIR bit representation. The local glslang extension makeFpConstantFromBits interns the declared-width payload directly; do not round-trip through host double. Tests cover signed zero, NaNs and payload preservation.

Use glslang's width-matched integer constant constructor. A 64-bit OpConstant requires two literal words, even when its value is only zero or one; makeIntConstant(type, value, ...) emits one word, so use makeInt64Constant for every 64-bit integer type. Keep a SPIRV-Tools validation fixture for helper-generated constants such as integer/bool cast zero and one values.

Variable-length literals such as switch cases retain the selector width and signedness in XIR/text/binary interchange. SPIR-V OpSwitch emits the exact one- or two-word case encoding required by the selector width.

Validation and optimization

validate_spirv uses SPIRV-Tools with SPV_ENV_VULKAN_1_2. Validation runs before and after optimization. Every optimizing preset conditionally ends with the grammar-aware trim-capabilities pass when the input module satisfies the locally audited capability contract. Optimizer output is validated before it is committed; optimizer failure or invalid output retains the already validated input binary. Final feature reconciliation always uses the binary that will actually be persisted.

LUISA_SPIRV_OPT_LEVEL selects the optimizer preset. Use level 0 to isolate emission, but never treat that as a fix. Optional loop unrolling applies only when the emitted loop control requests it.

Useful diagnostics:

LUISA_DUMP_SOURCE=1             # dump XIR stages and Vulkan SPIR-V disassembly
LUISA_DUMP_SPV=1                # dump pre-optimization binary
LUISA_SPIRV_OPT_LEVEL=0         # isolate native emission
LUISA_VULKAN_REQUIRE_NATIVE_XIR_SPIRV=1 # reject any user-shader HLSL fallback

Use LUISA_VULKAN_REQUIRE_NATIVE_XIR_SPIRV=1 for runtime or graphics validation that specifically claims to exercise XIR -> SPIR-V. The guard does not reject internal Vulkan builtins, which deliberately remain HLSL-generated. It is parsed by every Vulkan build: a build without native XIR codegen fails before compiling a user Function, a native build reports every unsupported feature that requested the compatibility path, and AOT loading under the guard accepts only an XIR_SPIRV artifact. Add LUISA_DUMP_SOURCE=1 when the test must prove fresh JIT code generation rather than execution of a valid cached native artifact; source dumping forces the native compile path before cache deserialization.

Integer bit operations

XIR CLZ, CTZ, POPCOUNT, and REVERSE accept only uint32 scalars or vectors. Keep their emitter paths exact: CLZ uses FindUMsb, CTZ uses FindILsb, and popcount/reverse lower directly to their core SPIR-V instructions. Signed selection and narrow-integer promotion in these paths are dead repair logic, not forward-compatible support. Runtime checks for popcount/reverse must compare integer results directly; converting full-width bit patterns to float loses enough precision to hide incorrect results.

Floating-point contraction

When ShaderOption::enable_fast_math is false, every emitted floating multiply/add/subtract that represents a source arithmetic operation must carry NoContraction. This includes the component instructions used to expand matrix arithmetic, the instructions used to expand floating reductions, and native dot, matrix multiply, and outer-product instructions, not only scalar/vector OpFMul, OpFAdd, and OpFSub. LENGTH_SQUARED also lowers to OpDot and needs the same decoration. Decorating only a final OpCompositeConstruct is invalid and does not protect its component operations. Keep a rounding-sensitive runtime check and exact SPIR-V decoration count for ordinary, reduction, and matrix multiply/add paths.

When fast math contracts a multiply/add pair, select the multiply exactly once and share that plan with instruction scheduling and FMA emission. An add may have two multiply operands, and its first multiply may be multi-use while its second is single-use. Deferring every single-use multiply whose user is an add can therefore suppress a value that the chosen FMA still needs as its addend, causing a use-before-definition failure. Only the selected single-use multiply may be deferred; all other products must be emitted normally.

XIR generalizes OUTER_PRODUCT to two matrix operands for autodiff. That form means A * transpose(B) and should lower to one OpTranspose followed by one OpMatrixTimesMatrix; the ordinary vector form remains OpOuterProduct.

XIR ROUND has the C/C++ round contract: halfway cases round away from zero and signed zero is preserved. Do not implement it as trunc(x + sign(x) * 0.5): the addition can round the float immediately below 0.5 to 1.0. Classify the fractional magnitude against an exactly typed 0.5, choose the adjacent integral magnitude, then copy the original sign bit. Runtime coverage must include nextafter values on both sides of positive and negative 0.5, not only exact halfway values.

Run test_vk_native_route_guard vk in both native-XIR and LLVM/HLSL-only Vulkan build trees. The native configuration verifies an explicit fallback reason is rejected; the non-native configuration verifies the build-unavailable diagnostic. The test isolates fatal diagnostics in a child process.

test_vk_spirv_codegen_path contains strict-native cases and three explicit compatibility cases: typed BUFFER_ONLY, an HLSL-writer/native-consumer ABI test, and an empty-plan test with a deliberately native-HLSL shader. Run the whole suite under Vulkan validation without the strict guard, then run every native case with LUISA_VULKAN_REQUIRE_NATIVE_XIR_SPIRV=1. Do not clear the guard inside the compatibility tests or describe their HLSL shaders as native XIR output. Keep the strict runner's explicit compatibility exclusions in sync instead of documenting a case count that changes whenever coverage grows.

The Vulkan backend's shared backend_print_code_enabled() contract reads LUISA_DUMP_SOURCE; it does not read the obsolete LUISA_COMPUTE_PRINT_CODE variable.

Testing expectations

Planner-only tests are appropriate for arithmetic limits and malformed input, but accepted codegen features also need an exact compile_spirv_xir or AST compile test so SPIRV-Tools validates the emitted module.

Important targets under src/tests/unit/ext/:

  • test_spirv_xir_dialect
  • test_spirv_pointer_legalization
  • test_spirv_control_flow_plan
  • test_spirv_instruction_layout
  • test_spirv_buffer_layout
  • test_spirv_aggregate_indices
  • test_spirv_target_feature_codegen
  • test_spirv_runtime_target_plan
  • test_spirv_raw_float_constants
  • test_spirv_optimizer
  • test_vk_device_feature_plan
  • test_vk_saved_argument_contract
  • test_vk_shader_binary_contract
  • test_argument_block_layout

For an accepted feature, test both the exact boundary and one-over rejection. For control flow, include the physical edge shape that caused the bug rather than only calling a mirror helper. For persisted/runtime ABI changes, test both writer and reader/consumer order where feasible.

Change checklist

Before handing off a native SPIR-V change:

  1. Keep generic XIR passes target-neutral.
  2. Establish a checked, immutable plan before target mutation.
  3. Validate numeric width, signedness, alignment, count, and storage class at every subsystem boundary.
  4. Update dialect classification and diagnostics.
  5. Add planner rejection tests and validator-backed accepted tests.
  6. Validate pre- and post-optimization SPIR-V.
  7. Run Vulkan validation layers for runtime layout/descriptor changes.
  8. Exercise the Vulkan XIR -> SPIR-V path, not an LLVM/HLSL fallback.
  9. Keep cached artifact and logical-device feature contracts in sync.
  10. Update this skill when an architectural invariant changes.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

ast

無料

Manual AST construction with FunctionBuilder for kernels and callables without DSL sugar.

日本語の概要は準備中です。原文の説明を表示しています。

LuisaGroup/LuisaCompute1,0532026年10月10日 更新

Backend plugin architecture, DeviceInterface, dynamic loading, and command encoding.

日本語の概要は準備中です。原文の説明を表示しています。

LuisaGroup/LuisaCompute1,0532026年10月10日 更新

cmake

無料

CMake build options, custom functions, and backend patterns for LuisaCompute.

日本語の概要は準備中です。原文の説明を表示しています。

LuisaGroup/LuisaCompute1,0532026年10月10日 更新

cpp-style

無料

C++ naming, formatting, static analysis, and RTTI rules for LuisaCompute.

日本語の概要は準備中です。原文の説明を表示しています。

LuisaGroup/LuisaCompute1,0532026年10月10日 更新

debug

無料

Debug crashes and test failures via stack-traces, host/device logging, and DSL buffer inspection.

日本語の概要は準備中です。原文の説明を表示しています。

LuisaGroup/LuisaCompute1,0532026年10月10日 更新

git

無料

Show uncommitted changes and commit history via git. Use when: (1) inspecting what changed in a file before committing, (2) checking diff of tracked/new/deleted files, (3) reviewing a specific commit's changes, (4) listing files changed in a commit.

日本語の概要は準備中です。原文の説明を表示しています。

LuisaGroup/LuisaCompute1,0532026年10月10日 更新

LuisaGroup のスキルをすべて見る

このスキルの問題を報告する