Rendering Pipeline

Explore the aether-renderer crate's rendering architecture, from frame scheduling and LOD policies to the wgpu GPU backend with PBR shading and shadow mapping.

Overview

The aether-renderer crate implements Aether's rendering pipeline in two layers: a policy and scheduling layer that makes frame-budget decisions independent of any GPU API, and a wgpu GPU backend that executes draw calls via Vulkan, Metal, DX12, or WebGPU. This separation lets the engine run headless (for servers or CI) while using the same scheduling logic as real-time rendering.

Frame Scheduling

The frame scheduler estimates per-frame workload and selects a quality mode that fits the target budget -- critical for VR where dropping below 90 FPS causes discomfort.

use aether_renderer::config::{FramePolicy, FrameBudget, FrameContext};
use aether_renderer::scheduler::{FrameScheduler, FrameModeInput};

let policy = FramePolicy {
    budget: FrameBudget {
        target_ms: 11.1,       // ~90 FPS
        gpu_headroom: 0.35,
        cpu_headroom: 0.30,
    },
    ..Default::default()
};

let context = FrameContext { fps: 90, draw_calls: 2000, visible_entities: 5000, gpu_ms: 8.5 };
let (mode, reason, workload) = FrameScheduler::decide_mode(&FrameModeInput {
    context, base_policy: policy,
});
// mode: Ultra | High | Balanced | Safe
// reason: BudgetAvailable | BudgetPressure | Overloaded | ThermalGuard

The workload is classified into buckets (Comfortable, Elevated, Constrained, Critical) that map to quality modes.

Stereo and Foveated Rendering

VR requires two views per frame. Single-pass multiview rendering draws both eyes in one render pass, cutting draw call overhead roughly in half:

use aether_renderer::config::StereoConfig;
let stereo = StereoConfig { enabled: true, views_per_frame: 2, multiview: true, single_draw_per_eye: true };

Foveated rendering reduces pixel density at view edges where headset lenses produce lower resolution, saving 20-40% fragment shading cost:

use aether_renderer::config::{FoveationConfig, FoveationTier};
let foveation = FoveationConfig {
    tier: FoveationTier::Tier2,
    center_ratio: 1.0,    // Full resolution at center
    edge_ratio: 0.45,     // 45% at edges
    ..Default::default()
};

The Adaptive tier uses real-time eye tracking to center the high-resolution region on gaze.

Level of Detail

LOD selection uses distance thresholds with hysteresis to prevent visible popping:

use aether_renderer::config::{LODPolicy, LODLevel, LodCurve};

let policy = LODPolicy { near: 10.0, mid: 35.0, far: 80.0, very_far: 160.0, hysteresis_ratio: 0.08 };
let new_lod = LodCurve::select(&policy, distance, current_lod);

The 8% hysteresis band means an L0 object at 10m stays L0 until it passes 10.8m, preventing rapid switching.

Shadow Mapping

Cascaded shadow maps divide the view frustum into slices at independent resolutions. When memory is constrained, the scheduler clamps resolutions to a byte budget:

use aether_renderer::config::ShadowCascadeConfig;
let shadows = ShadowCascadeConfig {
    enabled: true, num_cascades: 4, base_resolution_px: 2048,
    far_distance_m: 150.0, cascade_resolutions: [4096, 2048, 1024, 512],
};
let clamped = FrameScheduler::cascade_resolution_budget(&shadows, 4_000_000);

GPU Backend (wgpu)

Initialization

use aether_renderer::gpu::GpuContext;
let gpu = GpuContext::new().await?; // Returns Err if no GPU available

The backend is selectable via AETHER_GPU_BACKEND env var (vulkan, metal, dx12, gl, or auto).

Vertex Format and PBR Materials

All meshes use a 32-byte vertex: 3 floats position, 3 floats normal, 2 floats UV. Materials follow metallic-roughness PBR with albedo, metallic, roughness, and emissive parameters. The WGSL fragment shader combines texture sampling, a directional light, and cascade shadow sampling with PCF.

Render Pass Sequence

Each frame executes: (1) update uniform buffers (camera, model, material, light/shadow), (2) shadow pass with depth-only pipeline per cascade, (3) forward PBR pass with 4x MSAA, (4) MSAA resolve to surface, (5) present.

Instanced Batching

Draw calls are grouped by material via MaterialBatchKey to minimize state changes:

use aether_renderer::batching::batch_instances_by_key;
let batches = batch_instances_by_key(&instances);
for batch in &batches {
    // One instanced draw call per material group
}

Progressive Mesh Streaming

Large meshes stream progressively -- a coarse base mesh refines as bandwidth allows. Requests carry priority levels (Low, Medium, High) so nearby objects refine first.

Environment Variables

VariableDefaultPurpose
AETHER_GPU_BACKEND"auto"Force wgpu backend
AETHER_MSAA_SAMPLES4MSAA sample count
AETHER_SHADOW_MAP_SIZE2048Base shadow map resolution
AETHER_MAX_TEXTURE_SIZE4096Maximum texture dimension

Key Types Reference

TypeModulePurpose
FramePolicyconfigComposite rendering policy settings
FrameSchedulerschedulerWorkload estimation and mode selection
FrameModeschedulerUltra / High / Balanced / Safe quality
StereoConfigconfigMultiview stereo settings
FoveationConfigconfigFoveated rendering tiers and parameters
LODPolicyconfigDistance thresholds and hysteresis
ShadowCascadeConfigconfigCascade count, resolution, distance
ClusterLightingConfigconfigTile-based light clustering settings
GpuContextgpuwgpu device, queue, and surface
MaterialBatchKeybatchingMaterial-based draw call grouping