Write a Node With No Input#

Goal. Build an AOV out of authored values alone, with no AOV coming in.

Before you start. Types and Values, and a node that runs.

The Shape#

Declare typed attributes and an output, and no opaque inputs: port at all:

CheckerKernel.usda, from the runnable generating-node example#
def Shader "CheckerKernel"
{
    uniform token info:implementationSource = "sourceAsset"
    uniform asset info:spg:sourceAsset = @CheckerKernel.cu@
    uniform token info:spg:sourceAsset:subIdentifier = "checker"

    # Every input here is a typed attribute. There is no `opaque inputs:` port,
    # because this node reads no AOV: it generates its output from these values.
    #
    # The colours are exact at 8 bits, so the host can predict the output byte
    # for byte without depending on how a float is rounded into a texture.
    int inputs:width = 512
    int inputs:height = 512
    int inputs:squareSize = 32
    float3 inputs:colorA = (1.0, 1.0, 1.0)
    float3 inputs:colorB = (0.0, 0.0, 0.2)

    opaque outputs:Checker
}

The shader definition is the same either language, apart from what info:spg:sourceAsset points at.

How It Works#

A resource-input is optional; a node needs no AOV to run. Nothing else changes: the launch script is still required, the entry point is still named the same way, and the output is published like any other.

Nothing hands you a shape, so state one. A node with a resource-input can take its width and height from that input’s descriptor. This one has no input descriptor to read, so the size comes from somewhere you control, which usually means typed attributes:

CheckerKernel.cu.lua, from the runnable generating-node example#
-- Launch script for a node with no resource-input.
--
-- Nothing hands this node a resource, so there is no descriptor to take a shape
-- from. The size comes from typed USD attributes instead, like every other
-- value the node uses.
function checker(inputs, outputs)
    local width  = inputs["width"].value
    local height = inputs["height"].value
    assert(width > 0 and height > 0, "width and height must be positive")
    assert(inputs["squareSize"].value > 0, "squareSize must be positive")

    outputs["Checker"] = cuda.image(width, height, cuda.uchar4)

    return cuda.kernel({
        args = {
            cuda.int(width),                        -- -> int width
            cuda.int(height),                       -- -> int height
            cuda.int(inputs["squareSize"]),         -- -> int squareSize
            cuda.array(inputs["colorA"]),           -- -> const float* colorA
            cuda.array(inputs["colorB"]),           -- -> const float* colorB
            cuda.SurfaceObject(outputs["Checker"]), -- -> cudaSurfaceObject_t output
        },
        block = { 16, 16 },
        grid  = { math.ceil(width / 16), math.ceil(height / 16) },
    })
end
CheckerKernel.slang.lua, from the runnable generating-node example#
-- Launch script for a node with no resource-input, on Slang.
--
-- Nothing hands this node a resource, so there is no descriptor to take a shape
-- from. The size comes from typed USD attributes instead. The values are packed
-- into one parameter block, in the order the shader's Params struct declares.
function checker(inputs, outputs)
    local width  = inputs["width"].value
    local height = inputs["height"].value
    assert(width > 0 and height > 0, "width and height must be positive")
    assert(inputs["squareSize"].value > 0, "squareSize must be positive")

    outputs["Checker"] = slang.image(width, height, slang.uchar4)

    return slang.dispatch({
        bind = {
            slang.ParameterBlock(
                slang.int(inputs["width"]),            -- -> int    width
                slang.int(inputs["height"]),           -- -> int    height
                slang.int(inputs["squareSize"]),       -- -> int    squareSize
                slang.float3(inputs["colorA"]),        -- -> float3 colorA
                slang.float3(inputs["colorB"])         -- -> float3 colorB
            ),
            slang.RWTexture2D(outputs["Checker"]),     -- -> RWTexture2D<float4> g_OutChecker
        },
        -- The shader's [numthreads(16, 16, 1)] places the dispatch.
    })
end

Verify It Worked#

Build something you can recompute exactly on the host, then compare pixel for pixel. The checkerboard example draws its pattern from an authored square size and two authored colours, rebuilds the same pattern in NumPy, and prints pixels differing from the host-computed pattern: 0. Anything above zero means a value did not arrive or the launch did not cover the output.

When It Goes Wrong#

  • Nothing appears: nothing asked for the output, so the node never ran. Give it a RenderVar and list it in orderedVars. Refer to Nothing Appears.