Upload Your Own Data#
Goal. Get a lookup table, a filter kernel or any authored array onto the GPU, either built in Lua or read from a file.
Before you start. Choose a Resource’s Backing.
The Shape#
Build the data in Lua and hand it to array with a dtype:
local keys = cuda.array({ 0xA5, 0x5A, 0x12, 0x34 }, cuda.uint)
local keys = slang.array({ 0xA5, 0x5A, 0x12, 0x34 }, slang.uint)
Then bind keys in the launch configuration as a buffer.
How It Works#
array takes the data from wherever you have it. Given a Lua table and a dtype it creates a
device buffer holding that data. Given a resource descriptor it binds a buffer that already exists,
which is what Read a Sensor Composite does with a sensor’s channels. Given an asset input and a dtype
it creates a device buffer from that file’s bytes, which is the next section.
All three forms are on cuda and on slang.
With a vector dtype such as float3, each element of the table is a table of its components, so
cuda.array({ { 1, 2, 3 }, { 4, 5, 6 } }, cuda.float3) holds two float3 values.
ones and zeros take a shape and a dtype and create a filled buffer with no Lua data to
build; full also takes the value to fill it with. The shape counts elements, so with a vector
dtype every component of every element holds the value, and a Lua integer fills a 64-bit integer
buffer with all its bits.
Build the table once. Wrap the call in static, or it is rebuilt on every frame,
because the launch script runs on every frame, and a launch script has an instruction budget. The
upload does not depend on it: an array whose bytes stay the same from frame to frame stays on the
GPU and is not copied again. Refer to Cache Work Across Frames.
From a File#
Data too large to write out in Lua comes from a file instead. Declare an asset
value-input on the shader definition and point it at the file:
# A raw binary lookup table: 256 little-endian uint32 entries.
uniform asset inputs:lut = @LutInvert.bin@
The launch script reads it with the same array call, giving the dtype the file’s bytes should be
read as. SPG resolves the path and uploads the contents:
cuda.array(inputs["lut"], cuda.uint) -- -> const unsigned int* lut
slang.StructuredBuffer(slang.array(inputs["lut"], slang.uint))
-- -> StructuredBuffer<uint> g_Lut
On CUDA the call is complete on its own. On Slang a binder still has to wrap it, because Slang binds by declared type.
The file is read as raw bytes. Nothing parses a format or converts between types, so the dtype has to match how the file was written.
The file need not be a local one. An asset value-input is resolved exactly as the GPU
source is, so a lookup table may sit beside the scene, inside a .usdz package, or behind a
URI the Omniverse client can reach. Refer to Where Assets May Live.
Verify It Worked#
Upload data whose effect is exact and reversible, then reproduce it on the host. A per-channel
XOR key is the usual choice: out[..., c] must equal in[..., c] ^ K[c] for every pixel and
every channel. Approximate data, a blur kernel say, hides an off-by-one in the upload because the
result still looks right.
When It Goes Wrong#
The values are wrong but structured: the dtype does not match what the GPU code reads, or, for a file, does not match how the file was written.
The data is right on the first frame only, or is rebuilt every frame: refer to Cache Work Across Frames.
The script is stopped part way through, and the node’s graph with it: building the table exceeded the launch script’s instruction budget. Wrap it in
staticso it runs once, or move the work into the GPU code.