Skip to content

Renderer Overview

The Astra renderer is a modular, ECS-driven rendering system built on top of an abstracted GraphicsAPI. It is composed of six major subsystems:

RenderTarget

Everything begins with a RenderTarget. This class acts as the backend context and owner for the entire renderer. It is created once at startup and persists for the application's duration.

On startup, it instantiates the active GraphicsAPI backend and brings up the foundational objects required for the rendering lifecycle. For modern APIs like Vulkan, this includes selecting a physical device and creating the logical interface. For OpenGL, the driver and context creation handle this implicitly.

The RenderTarget establishes the window surface, manages the swap chain, and sets the baseline graphics pipeline state. It provides the context for GPU resources, but it does not own every object. Backend-specific specializations like OpenGLShader or VulkanGraphicsPipeline are created on-demand by the passes or managers that use them.

Once initialized, the RenderTarget hands off frame execution to the RenderGraph, which schedules and dispatches passes each frame.

RendererAPI

RendererAPI provides static utility methods for viewport management, depth/cull state, and draw call submission. It is a thin wrapper; backend differences are resolved at the resource level (shaders, textures, etc.), not within the API itself.

RenderGraph

The RenderGraph is a frame graph that manages passes and their resource dependencies. It automates several complex tasks:

  • Topological Sorting - Orders passes based on read-after-write dependencies.
  • Dead Pass Culling - Removes passes whose outputs are never consumed.
  • Resource Aliasing - Reuses memory for transient resources with non-overlapping lifetimes.
  • Transient Allocation - Manages per-frame GPU memory for framebuffers and storage buffers.

Passes are declared via RenderGraphBuilder and then compiled into an execution order. Each RenderPass implements a discrete step - such as shadow mapping or post-processing - and declares its resource requirements.

Pass Orchestration

Passes are grouped by stage and executed based on the graph's derived order:

Execution order is not fixed rather derived each frame by the RenderGraph using Kahn's algorithm, a topological sort driven by read-after-write dependencies. Each pass declares which resources it reads and which it writes; the graph builds a dependency edge whenever a pass reads a resource written by another. Such order follows this precedence:

Given passes A and B, if A reads a resource written by B, then A depends on B:

INFO

A resource in this context refers to a graph attachment - a transient GPU resource (framebuffer, texture, or buffer) managed by the graph for its lifetime.

Wait, culling dead passes? What about compute shaders?

Compute passes that write to a buffer with no downstream reader would normally be culled by default. To prevent this, a pass can be marked as non-cullable, forcing the RenderGraph to retain it regardless of whether its outputs are consumed.

The renderer is designed this way because in practice dependency inference alone doesn't scale well - even a straightforward deferred rendering setup with a skybox, shadow mapping, and debug passes can produce an ambiguous or suboptimal graph. Correctness and performance come first:

When does inference fall short, or what if we just don't want it?

Inference can sometimes produce an ambiguous order - for example, two passes that share no resource dependencies may be scheduled arbitrarily relative to each other, which can lead to incorrect results when an implicit ordering is expected. In such cases, a priority dependency can be used to manually hint the desired order. This is done either by explicitly registering a dependency between passes via pass->add_dependency(other_pass), or by assigning a priority to the pass directly.

WARNING

Hint is the operative word here:

Manual dependencies are applied alongside inference, not instead of it. For true manual ordering, one of the following must hold:

  • inference is disabled;
  • a dependency handle set is passed as a parameter to add_dependency;
  • all dependencies are declared explicitly on the pass

Otherwise inferred edges may still reorder passes in ways that contradict the intended manual order.

Entities & Components

The scene is a collection of entities (objects, cameras, lights, skyboxes, post-processing quads, text) each holding a set of typed components (mesh, transform, material, resource, camera, light, post-processing). The EntityManager and ComponentManager provide iteration and lookup.

ResourceManager

A generational slot-based pool for GPU resources: Shader, Texture2D, Texture3D, Model, Material, Font. Resources are declared as descriptors first (CPU-side metadata) and loaded asynchronously on demand per backend.

Supported Backend

BackendVersionStatusPlatformNotes
OpenGL4.5SupportedWindows, LinuxPrimary backend
Vulkan1.3OngoingWindows, Linux, macOS
DirectX 12UltimateUnsupportedWindowsPlanned
Metal3UnsupportedmacOS, iOS
WebGPU-UnsupportedWebPlanned

Namespace

All renderer types live in the astralix namespace.