Aurora Engine


A modular C++23 game engine built around a custom RHI, multi-queue render graph, ECS modules, and asynchronous rendering, compute, and transfers.

Declare intent, derive synchronization

Aurora's Render Graph lets a pass describe which resources it reads, writes, or presents without manually placing API barriers. The graph uses those declarations to determine execution order, resource lifetime, and queue synchronization before command recording begins.

This separation matters in a compute-heavy engine. Graphics, compute, and transfer work can use dedicated queues where available, while pass code remains concerned with rendering rather than queue-family ownership or backend-specific resource states.

  1. Builder

    • Declare pass access
    • Create virtual resources
    • Import external resources
  2. Compiler

    • Derive dependencies
    • Cull dead passes
    • Resolve barriers and queue transfers
  3. Executor

    • Acquire transient resources
    • Inject synchronization
    • Record and submit passes

Pass boundary

A render feature contributes passes through a small, type-erased interface. During configuration, the pass declares attachments and resource access and captures the data needed for later. During execution, it receives resolved resources and a command encoder. Templates retain each pass's strongly typed data while allowing the graph to store heterogeneous work.

Render-pass contract - C++
template <typename Data, typename T>
concept DataRenderPass = requires(
    T pass,
    PassBuilderHelper &builder,
    std::decay_t<Data> &data,
    const std::decay_t<Data> &const_data,
    const FrameGraphExecuteContext &ctx
) {
    { pass.configure(builder, data) };
    { pass.execute(const_data, ctx) };
    { pass.get_name() } -> std::same_as<std::string_view>;
    { pass.get_domain() } -> std::same_as<QueueDomain>;
};
Mandelbulb pass configuration - C++
static void
configure(aurora::PassBuilderHelper &builder, MandelbulbPassData &data)
{
    auto &blackboard = builder.blackboard();
    const auto &targets = blackboard.require<aurora::SceneTargetResources>();

    builder.color_attachment(
        targets.color,
        bifrost::LoadOp::Clear,
        bifrost::StoreOp::Store
    );
    data.width = targets.width;
    data.height = targets.height;
    data.view = blackboard.require<aurora::CameraView>();
}
Mandelbulb pass execution - C++
void
execute(const MandelbulbPassData &data, const aurora::FrameGraphExecuteContext &ctx)
{
    auto &encoder = ctx.encoder();

    encoder.set_pipeline(background_pipeline_->get_pipeline());
    bifrost::push_constants(
        encoder,
        bifrost::ShaderStage::Vertex | bifrost::ShaderStage::Fragment,
        0,
        &background_constants
    );
    encoder.draw(3);

    encoder.set_pipeline(proxy_pipeline_->get_pipeline());
    encoder.draw(36);
}

Compile the frame

The builder creates lightweight virtual resources or imports external ones such as a swapchain image. Resource accesses become edges in a directed acyclic graph. Compilation derives a topological order and removes passes whose outputs cannot affect an exported or presented result.

The compiler also determines first and last use. Compatible transient resources can be requested from a reusable pool rather than allocated for every pass, xxHash is used to determine this based on the descriptor, while imported resources retain their physical identity throughout compilation and execution.

States, barriers, and queues

A resource-state tracker follows every buffer and texture access. Same-queue hazards produce the required transition before a pass. Cross-queue dependencies are split into release and acquire operations, with submission dependencies carrying completion between queue domains.

The graph operates on Bifrost's backend-neutral usage and queue concepts.

Memory

The Graph makes heavy use of Arena allocators, and pre-allocating and re-using memory, to optimize it's compilation and execution speed. Where possible it makes an attempt to avoid allocations entirely.

Execution

Execution resolves virtual handles, acquires transient allocations, injects the precomputed barriers, invokes each pass, and submits the resulting queue batches. Focused tests verify that imported resources preserve strong physical identity, barriers resolve to the exact resource, and render-command completion and failure wake the correct waiting frames.

The graph is currently rebuilt each frame and uses a straightforward topological order for it's execution. Caching graphs, only recompiling when dirty, and optimizing the sorting/scheduling for better graphics/compute overlap are future optimizations that I will be working on, along with automatic parallel command encoding.