Forward+ Rendering: Solving DirectX Lighting Bottlenecks

Traditional forward rendering struggles with performance scaling when scenes contain hundreds or thousands of dynamic lights, resulting in an algorithmic bottleneck of \(O(\text{geometry} \times \text{lights})\) and excessive redundant calculations on occluded pixels. Forward+ rendering—also known as Tiled Forward Shading—solves this light accumulation bottleneck in DirectX by introducing a compute-shader-driven light culling phase before the final shading pass. By binning lights into screen-space tiles, Forward+ drastically reduces the number of light evaluations per pixel while preserving native forward-rendering advantages such as hardware multi-sample anti-aliasing (MSAA) and flexible material pipelines.

The Forward Shading Bottleneck

In a traditional forward rendering pipeline implemented in DirectX, scene geometry is rasterized, and lighting calculations occur inside the pixel shader for each fragment. If a scene contains \(M\) active lights and \(N\) fragments, the GPU potentially executes \(M \times N\) light iterations.

This architecture introduces two primary bottlenecks:

  1. Overdraw: When multiple opaque primitives overlap the same pixel, the pixel shader evaluates lighting for all overlapping fragments, even though only the front-most fragment is ultimately visible.
  2. Global Light Iteration: Without fine-grained spatial light management, the pixel shader must loop through every active light in the scene, testing distance and attenuation even if a light's radius of influence never reaches the rendered surface.

While deferred rendering addresses these issues by decoupling lighting from geometry, it consumes high memory bandwidth via Multi-Render Targets (G-Buffers), complicates transparency rendering, and disables native hardware MSAA.

The Three Stages of Forward+ in DirectX

Forward+ resolves the limitations of standard forward shading and deferred shading through a three-stage pipeline:

1. Depth Pre-Pass

The DirectX pipeline first renders the scene’s opaque geometry to a depth target using a minimal vertex shader and no pixel shader (or a depth-only state). This pass produces a full-resolution screen-space depth buffer and populates the hardware Early-Z buffer, ensuring that subsequent passes do not waste cycles on hidden surfaces.

2. Tile-Based Light Culling via DirectCompute

The screen is divided into a 2D grid of uniform tiles, typically \(16 \times 16\) pixels. A DirectX Compute Shader (CS) processes these tiles in parallel:

  • Frustum Extraction: Each compute thread group computes the minimum and maximum depth values for its assigned tile by sampling the depth buffer generated in the pre-pass. These depth bounds define a tight tile frustum in view space.
  • Light Intersection: The thread group iterates through the scene's light array (stored in a StructuredBuffer or constant buffer). Lights—modeled as bounding spheres for point lights or cones for spotlights—are tested for intersection against the tile's frustum.
  • Shared Memory Light Lists: Threads collaborate using thread group shared memory (groupshared) to construct a local list of light indices overlapping the tile.
  • Global Output: Once the intersection tests complete, the final list of active light indices for each tile is written to an unbounded RWStructuredBuffer, along with a tile grid offset buffer indicating the start index and light count per tile.

3. Shaded Forward Pass

The scene is rendered a second time with full materials and normal pixel shaders. However, unlike traditional forward rendering, the pixel shader does not iterate over all global lights.

Instead, the pixel shader calculates its screen-space tile coordinate from SV_Position:

  1. It samples the tile grid buffer to fetch the offset and count of lights affecting that specific \(16 \times 16\) region.
  2. It fetches light data exclusively for those indices from the culled light buffer.
  3. It accumulates lighting only from relevant sources.

Because the depth pre-pass already initialized the depth buffer, DirectX's hardware Early-Z feature automatically discards occluded geometry before pixel shading starts, eliminating the overdraw cost.

Performance Gains and Architectural Advantages

By shifting light culling to DirectCompute, Forward+ changes the lighting complexity from \(O(\text{geometry} \times \text{lights})\) to \(O(\text{geometry}) + O(\text{tiles} \times \text{lights})\). The number of light evaluations per pixel drops from thousands to only the handful of lights that physically intersect that pixel's tile frustum.

Additionally, because the final pass is still standard forward rendering, developers can use multiple unique material shaders, read anisotropic surface attributes without G-buffer storage constraints, support native hardware MSAA, and cleanly integrate alpha-blended transparent surfaces by reusing the culled light grids.