GPU scene representation and upload
4648adc · 2026-07-26 ↗
The final repository checkpoint moves scene traversal and light transport to
the GPU. Four articles use commit 4648adc: this one covers the host-to-device
scene representation, followed by separate articles for path tracing and
ReSTIR. Accumulation and Hydra transfer are mentioned only where they affect
the lifetime or measurement of those algorithms.
flowchart LR
H["Hydra synchronization<br/>CPU objects"] --> F["flattened GPU scene"]
F --> AS["BLAS and TLAS"]
F --> B["materials, lights, textures"]
AS --> K["path-tracing kernels"]
B --> K
Why a second scene representation is required
The CPU scene contains virtual interfaces, Pixar types, containers, pointers, and ownership relationships that are not a shader ABI. GPU kernels require plain data with defined size and alignment, buffer addresses, texture handles, and an acceleration structure.
The conversion is not a byte copy. It performs:
- mesh-instance expansion;
- subset-to-material association;
- transformation into the RHI matrix convention;
- material deduplication;
- texture extraction and upload;
- analytic-light conversion;
- environment separation;
- BLAS reuse and TLAS instance construction.
Each operation must preserve an identity used later to reconstruct a hit.
Matrix convention at the ABI boundary
OpenUSD matrices in this renderer use row-vector transforms with translation in row 3. The RHI acceleration-structure instance expects a row-major 3×4 matrix for column-vector multiplication. The conversion is explicit:
template <typename Matrix4> [[nodiscard]] float3x4 _toRhiTransform(const Matrix4 &m)
{
const gsl::span<const typename Matrix4::ScalarType> d{m.data(), 16};
float3x4 out{};
for (int r{0}; r < 3; ++r)
{
for (int c{0}; c < 3; ++c)
{
out.m[r][c] = static_cast<float>(d[(c * 4) + r]);
}
out.m[r][3] = static_cast<float>(d[12 + r]);
}
return out;
}
The upper-left 3×3 block is transposed and the translation is moved into the
fourth component of each output row. An ordinary memcpy would compile and
produce a matrix-shaped buffer, but instances would be transformed according
to the wrong convention.
The same file avoids using a native shader matrix inside raw light data.
Instead it stores three float4 rows, whose scalar layout is explicit on both
sides of the ABI.
Expanding instances and subsets
The builder walks visible meshes. Point-instanced meshes expand one GPU instance for every instance transform and mesh subset:
for (IMesh *mesh : scene.GetMeshes())
{
if (!mesh->IsVisible())
{
continue;
}
if (!mesh->GetInstancerId().IsEmpty())
{
if (IInstancer * instancer{scene.GetInstancer(mesh->GetInstancerId())})
{
const VtMatrix4dArray transforms{instancer->ComputeInstanceTransforms(mesh->GetId())};
for (const auto &transform : transforms)
{
for (const auto &subset : mesh->GetSubsets())
{
appendInstance(*mesh, subset, GfMatrix4f(transform) * mesh->GetTransform());
}
}
}
continue;
}
for (const auto &subset : mesh->GetSubsets())
{
appendInstance(*mesh, subset, mesh->GetTransform());
}
}
This excerpt preserves the exact loop from
gpu_scene_builder.cpp.
The multiplication order is part of the OpenUSD row-vector convention. A test
must use a non-commuting pair of transforms—such as rotation followed by
translation—because two translations would not expose a reversed order.
Geometry identity and BLAS reuse
For each subset, MeshKey is derived from the stable address of the subset’s
index array and paired with GeometryVersion. Instances that share geometry
can therefore reuse one bottom-level acceleration structure while retaining
different transforms and object IDs.
This key is valid only under the lifetime contract stated in the source: the subset index array address remains stable until topology changes, and topology changes increment the geometry version or rebuild the scene snapshot. Pointer identity is not a persistent asset identifier and must not cross that lifetime.
Material and object identifiers
Materials are deduplicated by CPU object identity for the uninterrupted render:
const IMaterial &material{scene.GetMaterial(matId)};
auto [it, inserted]{
materialIndex.try_emplace(
&material,
static_cast<std::uint32_t>(_materials.size()))};
// ...
flat.MaterialId = it->second;
flat.ObjectId = scene.GetSceneObjectId(mesh.GetId());
flat.DoubleSided = mesh.IsDoubleSided();
MaterialId indexes compact GPU shading data. ObjectId preserves scene
identity for temporal-history validation. They answer different questions: two
objects may share one material, while one object can contain subsets with
different materials.
Preview Surface scalar parameters are resolved through the CPU material implementation after texture bindings are cleared. Textures are then uploaded separately with a material ID and slot. Diffuse data uses the color interpretation selected by the texture factory; normal, metallic, and roughness data are uploaded as linear data.
Lights and environment
CPU TfToken sampling strategies are converted once to small integer values
because shader code does not depend on RTTI-enabled Pixar token types. Point,
distant, and rectangle lights occupy the regular light array. A dome light is
uploaded as environment data rather than duplicated in that array.
The mapping function has a defined fallback for an unrecognized strategy. Its assertion records the unexpected value in debug builds, while the return value avoids undefined behavior in an optimized build. Adding a new light type therefore requires updating both the CPU mapping and shader dispatch.
Validation requirements
A scene-upload test should isolate:
- vertex and index byte layout;
- matrix convention with non-commuting transforms;
- material sharing across different objects;
- multiple subsets on one mesh;
- point-instancer expansion;
- texture slot and color-space flags;
- light strategy and environment separation;
- object/material IDs reconstructed from a ray hit.
A plausible final image is insufficient for this boundary. Swapping object and material IDs can still produce a shaded image while invalidating temporal reuse and material attribution.
Caption to keep: The GPU scene is a flattened ABI representation of the renderer scene; tests must preserve geometry, transform, material, light, and object identity separately.