# How the Acceleration Structure (Accel) System Optimizes Ray Tracing in luisarender

> Discover how the Accel system in luisarender optimizes ray tracing with a two-level hierarchy, fast-path queries, and dynamic updates. Improve your rendering performance.

- Repository: [LuisaGroup/luisarender](https://github.com/luisagroup/luisarender)
- Tags: internals
- Published: 2026-03-06

---

**The Accel system in luisarender optimizes ray tracing by constructing a two-level hierarchy (BLAS for meshes, TLAS for instances), using opaque instance flags to enable fast-path intersection queries, and supporting dynamic transform updates without full rebuilds.**

The **acceleration structure (Accel) system** in luisarender leverages the **Luisa Compute** framework to minimize ray-scene intersection costs on modern GPUs. It abstracts hardware-specific ray tracing APIs into a unified interface that handles both static geometry and dynamic objects. By separating mesh-level acceleration (BLAS) from scene-level instancing (TLAS), the system enables efficient culling and traversal for complex scenes.

## Architecture of the Accel System

The **Accel** class implements a two-level acceleration hierarchy common in modern GPU ray tracing.

- **Bottom-Level Acceleration Structure (BLAS)**: Built per-mesh geometry and stored in GPU memory. Each mesh instance references its BLAS handle.
- **Top-Level Acceleration Structure (TLAS)**: Contains instance records pointing to BLAS handles, along with 4×3 transformation matrices and visibility flags.

In [`src/base/geometry.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/geometry.h), the `Geometry` class holds the TLAS as a member variable `_accel`, while individual meshes maintain their BLAS references through `mesh.resource`.

## Building the Acceleration Structure

Scene construction occurs in [`src/base/geometry.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/geometry.cpp) within the `Geometry::build` method. The process allocates the TLAS, registers every mesh instance, and compiles the structure on the GPU.

```cpp
// src/base/geometry.cpp - Geometry::build excerpt
_accel = _pipeline.device().create_accel({});          // Allocate TLAS

for (auto shape : shapes) { 
    _process_shape(shape, ...);                        // Emplace BLAS instances
}

command_buffer << _instance_buffer.copy_from(_instances.data())
               << _accel.build();                     // Compile TLAS on GPU

```

The TLAS is **rebuilt only once** after all mesh instances have been enqueued. The `_accel.emplace_back()` call inserts each instance with four parameters: the BLAS reference, object-to-world matrix, visibility boolean, and an **opaque flag**.

## Opaque vs. Transparent Instance Optimization

The **Accel** system uses per-instance opacity metadata to select between fast-path and full-path ray queries. When building the TLAS, the geometry tracks whether any instance might contain transparency via the `_any_non_opaque` flag.

### Fast-Path Ray Queries (All Opaque)

If `_any_non_opaque` is `false`, every instance is fully opaque. The renderer invokes the hardware-accelerated `intersect` method directly, returning the closest hit without additional shading work.

```cpp
// src/base/geometry.cpp - Geometry::trace_closest fast path
auto hit = _accel->intersect(ray_in, {});
return Var<Hit>{hit.inst, hit.prim, hit.bary};

```

This path minimizes register pressure and memory bandwidth by avoiding callback execution and additional texture lookups for opacity evaluation.

### Full-Path with Alpha-Skip (Transparent Geometry)

When transparency is present, the system uses the `traverse` API with a surface-candidate callback. This allows the renderer to evaluate material opacity before committing to a hit, enabling stochastic alpha testing.

```cpp
// src/base/geometry.cpp - Geometry::trace_closest ray-query version
auto rq_hit = _accel->traverse(ray, {})
    .on_surface_candidate([&](compute::SurfaceCandidate &c) noexcept {
        $if (!this->_alpha_skip(c.ray(), c.hit())) { 
            c.commit(); 
        };
    })
    .trace();

```

The `_alpha_skip` method hashes the hit coordinates to generate a random value and compares it against the material's opacity. If the test fails, the ray continues traversal, effectively skipping transparent surfaces without casting separate shadow rays.

## Dynamic Transform Updates

Static geometry bakes instance transforms into the TLAS at build time. For animated or moving objects, the **Accel** system supports per-instance transform updates without rebuilding the entire structure.

In [`src/base/geometry.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/geometry.cpp), the `Geometry::update` method iterates over dynamic instances and applies new matrices:

```cpp
// src/base/geometry.cpp - Geometry::update excerpt
for (auto t : _dynamic_transforms) {
    _accel.set_transform_on_update(t.instance_id(), t.matrix(time));
}
command_buffer << _accel.build();   // Rebuild TLAS only for transformed instances

```

The `set_transform_on_update` call marks specific instance records for modification. The subsequent `build` command only updates the TLAS instance buffer, not the underlying BLAS data. For scenes with fewer than 128 dynamic instances, updates run on the CPU; larger counts use a parallel thread pool.

## Backend-Specific Optimisations

The **Accel** abstraction handles backend differences transparently. On **DirectX**, a known ray-query bug prevents reliable transparent traversal, so the renderer falls back to explicit iterative ray marching in `trace_closest` (lines 26-48 in [`src/base/geometry.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/geometry.cpp)).

All other backends (**Vulkan**, **CUDA**, **Metal**) use the native `traverse` and `traverse_any` APIs, providing hardware-accelerated traversal with custom surface-candidate callbacks for optimal performance.

## Practical Implementation Example

The following example demonstrates the complete lifecycle of the **Accel** system in a rendering application:

```cpp
#include <luisa/render/geometry.h>
#include <luisa/render/pipeline.h>

// 1. Initialize pipeline and geometry
luisa::render::Pipeline pipeline{device_config};
luisa::render::Geometry geometry{pipeline};

// 2. Build acceleration structure from scene shapes
auto cmd_buffer = pipeline.command_buffer();
geometry.build(cmd_buffer, scene_shapes, /*time=*/0.f);

// 3. Trace rays using optimized queries
compute::Ray ray = compute::make_ray(
    make_float3(0.f, 1.f, 5.f),      // origin
    make_float3(0.f, -0.2f, -1.f));  // direction

auto hit = geometry.trace_closest(ray);

// 4. Update dynamic objects each frame
while (rendering) {
    geometry.update(cmd_buffer, current_time);
    // Render loop...
    current_time += 0.016f;
}

```

This pattern allocates the **TLAS** once, batches all mesh instances during construction, and leverages the fast-path intersection routines for opaque geometry while supporting dynamic transform updates for animated content.

## Key Source Files

| File | Description |
|------|-------------|
| [`src/base/geometry.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/geometry.h) | Definition of `Geometry` class, `Accel` member variables, and public API signatures. |
| [`src/base/geometry.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/geometry.cpp) | Implementation of `Geometry::build`, `Geometry::update`, `Geometry::trace_closest`, and alpha-skip logic. |
| [`src/base/shape.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/shape.h) | `AccelOption` interface and shape property flags (e.g., `property_flag_maybe_non_opaque`). |
| [`src/base/pipeline.h`](https://github.com/luisagroup/luisarender/blob/main/src/base/pipeline.h) | `Device::create_accel` method and pipeline integration. |
| [`src/base/interaction.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/interaction.cpp) | Post-hit interaction creation and opacity evaluation routines. |

## Summary

* The **Accel** system implements a two-level hierarchy (**BLAS** for meshes, **TLAS** for instances) to minimize ray-scene intersection costs.
* **Opaque instance flags** enable hardware-accelerated fast-path queries when no transparency exists, avoiding expensive callback overhead.
* **Alpha-skip logic** with stochastic hashing allows efficient traversal of transparent surfaces without casting separate shadow rays.
* **Dynamic transform updates** via `set_transform_on_update` enable animated objects while only rebuilding the TLAS instance buffer, not the underlying BLAS.
* **Backend-specific fallbacks** ensure robust performance across DirectX, Vulkan, CUDA, and Metal, with iterative ray marching for DirectX transparency bugs.

## Frequently Asked Questions

### How does the Accel system determine whether to use the fast-path or full-path ray query?

The system checks the `_any_non_opaque` boolean flag set during scene construction in [`src/base/geometry.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/geometry.cpp). If all instances are marked opaque (no transparency possible), `_any_non_opaque` remains false and `trace_closest` uses the single `intersect` call. If any instance might contain transparency, the flag becomes true and the renderer falls back to the `traverse` API with surface-candidate callbacks to evaluate opacity per-hit.

### What is the performance cost of updating dynamic objects in the Accel system?

Updating dynamic transforms incurs minimal overhead because `set_transform_on_update` only modifies the instance matrix buffer in the TLAS, not the underlying BLAS geometry. The subsequent `_accel.build()` call rebuilds only the TLAS instance records, which is significantly cheaper than reconstructing mesh-level acceleration structures. For scenes with fewer than 128 dynamic instances, updates run on the CPU; larger counts use a parallel thread pool.

### How does the Accel system handle transparent materials differently from opaque ones?

For opaque materials, the hardware returns the first intersection immediately via the `intersect` method. For transparent materials, the system uses the `traverse` method with an `on_surface_candidate` callback that evaluates the material's opacity at the hit point. The `_alpha_skip` function performs stochastic alpha testing using a hash of the hit coordinates to determine whether to commit the hit or continue traversal, effectively skipping transparent surfaces without casting separate shadow rays.

### Why does the DirectX backend use a different ray tracing path than Vulkan or CUDA?

The DirectX backend contains a known bug in its ray-query implementation that prevents reliable traversal when transparent geometry is present. To work around this, the `trace_closest` method in [`src/base/geometry.cpp`](https://github.com/luisagroup/luisarender/blob/main/src/base/geometry.cpp) falls back to explicit iterative ray marching for DirectX, manually stepping through the acceleration structure. Vulkan and CUDA backends use the native `traverse` and `traverse_any` APIs, which provide hardware-accelerated traversal with custom surface-candidate callbacks for optimal performance.