The Lua Launch Script#
Everything a launch script may contain. For what a node is made of, refer to Overview.
SPG finds the script by appending .lua to the info:spg:sourceAsset path, so
GrayscaleKernel.cu is paired with GrayscaleKernel.cu.lua. The pairing is by file name and
cannot be redirected.
What follows is identical whichever language the GPU source is written in. For the parts that differ, refer to The cuda Lua Table and The slang Lua Table.
A Complete Script#
This script validates the input it was handed, takes the shape from it, describes the output, and returns the launch configuration.
GrayscaleKernel.cu.lua, from the runnable grayscale example#-- Launch script for GrayscaleKernel.cu. SPG calls this once per frame.
-- The function name must equal the subIdentifier ("grayscale").
function grayscale(inputs, outputs)
assert(#inputs["LdrColor"].shape == 2, "Input must be a 2D image")
assert(inputs["LdrColor"].dtype == cuda.uchar4, "Input must be uchar4")
-- shape is 1-indexed: [1] = height (rows), [2] = width (columns).
local height = inputs["LdrColor"].shape[1]
local width = inputs["LdrColor"].shape[2]
-- Allocate the output AOV before launching.
outputs["LdrGrayscale"] = cuda.image(width, height, cuda.uchar4)
return cuda.kernel({
-- args order/types must match the C signature exactly.
args = {
cuda.int(width), -- -> int width
cuda.int(height), -- -> int height
cuda.TextureObject(inputs["LdrColor"]), -- -> cudaTextureObject_t inputLdrColor
cuda.SurfaceObject(outputs["LdrGrayscale"]), -- -> cudaSurfaceObject_t outputLdrGrayscale
},
block = { 32, 32 },
grid = { math.ceil(width / 32), math.ceil(height / 32) },
})
end
GrayscaleKernel.slang.lua, from the runnable grayscale example#-- Launch script for GrayscaleKernel.slang. SPG calls this once per frame.
-- The function name must equal the subIdentifier ("grayscale").
--
-- Same job as GrayscaleKernel.cu.lua: validate the input, allocate the output,
-- describe the launch. The differences are the `slang` table (in place of
-- `cuda`) and that resources are bound as a list instead of kernel arguments.
function grayscale(inputs, outputs)
assert(inputs["LdrColor"].rank == 2, "Input must be a 2D image")
-- Allocate the output AOV. Grayscale keeps the input's shape and dtype.
outputs["LdrGrayscale"] = slang.image(inputs["LdrColor"].shape, inputs["LdrColor"].dtype)
return slang.compute({
-- bind order must match the resource declaration order in the shader.
bind = {
slang.Texture2D(inputs["LdrColor"]), -- -> Texture2D<float4> g_InLdrColor
slang.RWTexture2D(outputs["LdrGrayscale"]), -- -> RWTexture2D<float4> g_OutLdrGrayscale
},
})
end
Highlighted: the function, whose name has to match the entry point; the line that describes the output; and the return that carries the launch configuration.
What the Script Is Handed#
It has |
It does not have |
|---|---|
Descriptors for every bound resource: shape, dtype and rank |
The contents of those resources. No pixel, point or buffer value is readable here, and no result of this frame’s GPU work, which has not run yet. |
The value-inputs authored in USD, and |
The results of a previous frame’s GPU work. A node’s memory and a stateful output keep their contents for the node’s GPU code, not for the launch script. |
The frame and the scene, through the |
Anything the load did not publish, and any way to write the scene |
The practical consequence: a launch script cannot branch on data. A lidar’s
Counts channel, for example, says how many returns a sweep produced, but the script sees
only its descriptor. The count is passed to the GPU and the bound is applied there.
Function Signature#
The script defines one global function. Its name must match both the entry point in the GPU
source and info:spg:sourceAsset:subIdentifier in the USD shader definition. It receives
two tables:
Parameter |
Contents |
|---|---|
|
Unified map of all shader inputs. Contains both resource-inputs (from |
|
Map you populate with output descriptors. Keyed the same way, so USD |
The function returns the launch configuration produced by cuda.kernel(...) or by one of the
slang launch calls.
The values the cuda and slang functions return for a launch, the arguments such as
cuda.int(...), cuda.TextureObject(...) and slang.ParameterBlock(...) and the launches
of cuda.kernel{...} and slang.compute{...}, are userdata that read, write and iterate like
tables, with pairs and #. type() gives "userdata" for them, next() and the
table library do not take them, and tostring() names their kind, such as
SpgArgWrapper.
Every argument of a launch is an input, an output, a memory, a value or array data. An argument of another kind, such as a table the script builds with a name that carries no port prefix, or one that does not describe the resource it stands for, stops the node’s graph with an error naming the argument and its position among the launch’s arguments, where each value of a Slang parameter block counts as one.
Resource-Inputs and Value-Inputs#
Both kinds of port arrive in the same inputs table, and they are told apart by their
fields. Which port is which is settled in USD, as described in
Nodes and Ports.
Kind |
Fields |
|---|---|
Resource-input |
|
Value-input |
|
Test inputs["X"].value ~= nil to distinguish them. A value-input is a wrapper rather than
a number, so it cannot be used in arithmetic directly. Refer to Types and Values.
Shapes and Types#
A resource is described by three things, and every function that creates or inspects one is written in terms of them.
Term |
What it is |
|---|---|
|
A Lua table of the resource’s dimensions, one entry per dimension. |
|
How many dimensions it has, which is how many entries |
|
What one element is. |
A dtype says three things about an element: what kind of number it holds, how wide that
number is, and how many of them make one element. cuda.uchar4 is four unsigned 8-bit
numbers, so one RGBA pixel; cuda.float is a single 32-bit float.
A dtype compares, and it is callable. Comparing is how a script checks what it was handed,
inputs["Image"].dtype == cuda.uchar4. Calling one wraps a value for the GPU,
cuda.int(42).
Shapes are height-first: shape[1] is height and shape[2] is width. Functions that
take explicit dimensions, such as cuda.image(width, height, dtype), take them in the
opposite order; swapping them transposes the image.
A resource is also either texture-backed or buffer-backed, and that choice is fixed when it is created. Which one an output should be is stated when it is described, and the binder used for it has to agree.
Describing Outputs#
Every output declared in the USD shader definition must be assigned a descriptor in
outputs before the script returns. A descriptor states what the output has to be, its
shape and dtype, and is not the memory itself. An output left undescribed is never backed,
so the node has nothing to write into.
Which function describes it depends on how the output is backed. Refer to Choose a Resource’s Backing.
cuda.image takes width and height, in that order:
outputs["LdrGrayscale"] = cuda.image(width, height, cuda.uchar4)
slang.image takes width and height, or a shape table, which is how a texture of one or
three dimensions is asked for:
outputs["LdrGrayscale"] = slang.image(inputs["LdrColor"].shape, inputs["LdrColor"].dtype)
The available descriptor functions, and their exact signatures, are listed in The cuda Lua Table and The slang Lua Table.
The self Table#
self is the node itself: a global table SPG sets before every call, holding what the
launch states about the node rather than about its ports. It is new on every call, so nothing a
script puts there carries over to the next launch or to another node running the same script.
Field |
Description |
|---|---|
|
The node’s memory, by name: |
|
Set to |
An entry of self.memory that is not a descriptor, a key that is not a string, or a
self.memory that is not a table fails the launch with an error that names the memory, as an
argument the node cannot take does.
Global Variables#
Variable |
Description |
|---|---|
|
The current rendered frame. Refer to The rtx Lua Table. |
|
The RenderProduct the node’s prim sits under: its cameras, RenderVars, resolution and
tiling. |
|
The scene: one prim by path, or the attributes of many prims in one call. Refer to The rtx Lua Table. |
|
The names of a prim’s two transforms, for |
|
The node itself: its memory and its statefulness. Refer to The self Table. |
Logging Functions#
A launch script can write to the renderer log, visible in the console and in the renderer’s log file.
Function |
Description |
|---|---|
|
Log a message at INFO level. |
|
Log a message at WARNING level. |
|
Log arguments at VERBOSE level. |
|
Assert a condition. On failure it logs an error and raises it, which stops the node’s graph. |
Only warnings are visible by default. At the renderer’s default log level info and print
are not printed, so a script that reports through them looks silent whether it ran or not. Raise
the log level, or use warning while you are debugging.
The launch function runs once per rendered frame, not once per host step. A single step can render
several frames while the image converges, so anything logged unconditionally arrives many times
over. rtx.frameId counts those rendered frames.
Sandbox#
Launch scripts run in a restricted Lua 5.4 interpreter. Only the base, math,
string and table libraries are available. File, OS, module and coroutine access are
removed, as are pcall and xpcall. The string pattern functions string.find,
string.match, string.gmatch and string.gsub are removed too, because a single call
can run for an unbounded time; the rest of the string library is available.
Each script is bounded in memory, 10 MB. Loading the file and each call of the launch function
may run at most one million Lua instructions and write at most 64 KB of log output through
info, warning and print. table.move rejects a range longer than the number of
elements a table can hold within the memory limit. SPG’s own functions count the work they do
on a script’s data against the same limits: rtx.stage.query_attributes counts one instruction
per prim path and attribute name, and an array in the launch table counts its elements against
the instruction limit and its size against the memory limit. These limits are fixed.
A script that reaches one of these limits stops the node’s graph, including when the limit is
reached inside a function passed to cuda.static or slang.static. The log names the node
and the limit, and the graph retains that reason. The stopped node retries when one of its own
value-inputs changes or its graph is rebuilt. Writing an equal value or editing another node
keeps it stopped; refer to
a graph stopped after one failure. Keep the script to
validation, output descriptions and building the launch table; the work belongs in the GPU source.
A launch script must be Lua source text. A file of precompiled Lua bytecode, as luac
writes it, is rejected and its node’s graph stops.
Before a script runs, its source is scanned for forbidden constructs. The scan is a plain substring match over the whole file, including comments and identifiers, so a comment mentioning one of them is enough to have the script rejected. A rejected script produces no GPU work, which surfaces as an output AOV that never gets written.
Editor Support#
ovrtx ships a .luarc/ folder, under docs/spg/.luarc/, with type stubs for the Lua
Language Server declaring the globals SPG injects at runtime (cuda, slang, rtx,
self, info, warning, LOCAL_MATRIX, WORLD_MATRIX). Any editor with a Lua Language Server integration can use them for
completions, hover hints and diagnostics when authoring launch scripts.
To enable it for your own project:
Configure your editor with a Lua Language Server.
At the root of your workspace, the folder you open in the editor, create a
.luarc.jsonfile with the following contents:
{
"Lua.runtime.version": "Lua 5.4",
"Lua.workspace.library": [
"<path-to-spg-luarc>"
],
"Lua.workspace.checkThirdParty": false,
"Lua.diagnostics.globals": ["cuda", "slang", "rtx", "self", "info", "warning", "LOCAL_MATRIX", "WORLD_MATRIX"],
"Lua.diagnostics.disable": ["lowercase-global"]
}
Fill in the placeholder as follows:
Placeholder |
What to use |
|---|---|
|
Absolute path to the SPG |
Once the file is saved, the language server resolves cuda.kernel(...) and
slang.compute(...), recognizes the SPG globals, and stops flagging them as undefined. If
completions do not appear, restart the Lua Language Server so it picks up the new
configuration.