Compile It Yourself#
Goal. Build the GPU code with your own toolchain, or split it across files, instead of handing SPG one source file to compile.
Before you start. A working node in the language you are building.
The Shape#
info:spg:sourceAsset decides which of these happens. The extension is what SPG reads.
.cu is compiled by SPG on load. .ptx, .cubin and .fatbin are loaded already
compiled.
.slang is compiled by SPG on load. .slang-module and .spv are loaded already
compiled, and the two are not interchangeable.
How It Works#
What Stays the Same#
The launch script is still required, and still found the same way, by appending
.luato the source asset path.InvertKernel.ptxpairs withInvertKernel.ptx.lua. SPG has no other way to learn the output shapes or the binding order.The argument list still has to match what the code was compiled with. An artifact carries no signature SPG can check a CUDA
argslist against, so a mismatch shows up as the kernel reading the wrong memory rather than as an error at load.
What the Artifact Is#
A .ptx, .cubin or .fatbin is handed to the CUDA driver as it is, and NVRTC is
not involved. .ptx is portable and JIT-compiled by the driver for the target
architecture, .cubin targets one architecture and skips that JIT, and .fatbin
carries several architectures in one file.
info:spg:sourceAsset:subIdentifier still names the entry point, now resolved as a symbol
in the loaded module. The name has to appear in the binary exactly as written, so compile
the entry point as extern "C"; a mangled C++ symbol will not be found.
A CUDA node is one translation unit, compiled against a single include directory. That
directory is the CUDA installation SPG finds: CUDA_PATH or CUDA_HOME if either is set
in the environment, otherwise the headers bundled with the renderer. The directory holding
the .cu is never on the path, so #include "MyHelpers.cuh" beside the source does not
resolve. To split a node across files, compile it with nvcc and ship the .ptx,
.cubin or .fatbin.
The two forms are not interchangeable.
.slang-module holds Slang IR, which is target-agnostic: it is lowered to whichever
graphics backend is active when the shader is linked, so one artifact serves any of them. It
carries its entry point and its binding layout, so the node binds as a source node does.
.spv holds SPIR-V, already lowered for Vulkan, and carries no binding layout SPG can
read:
Everything reflection would have supplied has to be stated instead, which makes Bind Resources and Descriptor Spaces a prerequisite for this form rather than optional reading. What reflection fills in lists the items one by one.
Compute only. A ray-tracing pipeline is assembled from shader-database handles, which byte code has none of, so those stages take
.slangsource or a.slang-module. Refer to Trace the Scene.The entry point comes from the binary, not from
subIdentifier, because the compiler names it independently:slangcemitsmainwhatever-entrysays.
A shader can be split across files. import MyHelpers; resolves to
MyHelpers.slang beside the shader. A symbol is visible to the importing shader only if it
is declared public. Imports are resolved during a source compile, so an artifact that is
already compiled carries what it needs and does not re-resolve them.
The blur example keeps the tap loop its two passes share in a module beside the shader:
BlurTaps.slang, from the runnable blur example#// The tap loop both blur passes run, kept in its own module so neither entry
// point restates it. This file declares no entry point and is not a node: it is
// reached only through `import BlurTaps;` in BlurKernel.slang, which resolves to
// this file because it sits beside it.
//
// `public` is what makes a symbol visible to an importing module.
public float3 blurTap(Texture2D<float4> image,
StructuredBuffer<float> weights,
int radius,
int width,
int height,
int2 pixel,
int2 direction)
{
float3 sum = float3(0.0, 0.0, 0.0);
for (int t = -radius; t <= radius; ++t)
{
int2 at = clamp(pixel + t * direction, int2(0, 0), int2(width - 1, height - 1));
sum += weights[t + radius] * image.Load(int3(at, 0)).rgb;
}
return sum;
}
Where the Files May Live#
An artifact is named by info:spg:sourceAsset like any other source, so it may sit beside the
scene, inside a .usdz package, or behind a URI, and its launch script follows it. Refer to
Where Assets May Live.
Verify It Worked#
Render the same scene twice, once from source and once from the artifact, and compare the two outputs byte for byte. They must be identical. Compare the content rather than checking that the run completed: a node that loads and binds nothing still renders a plausible image from zeros.
When It Goes Wrong#
The entry point is not found: for CUDA the symbol has to be unmangled, so compile it as
extern "C".A
.spvon a ray-generation stage is rejected. Refer to Trace the Scene.An import does not resolve, on Slang: the module does not sit beside the shader, or its file name does not match the imported name.
An
#includedoes not resolve, on CUDA: one include directory is searched, never the directory the.cusits in. Check whatCUDA_PATHorCUDA_HOMEpoint at.