Skip to content

Core

The core module contains fundamental engine components.

AbstractApplication

Base class for all Freya applications. Inherits from skr::IApplication and provides the main application loop.

class MyApp final : public fra::AbstractApplication
{
    void StartUp() override;
    void Update() override;
    void ShutDown() override;
};

Lifecycle Methods

  • StartUp() - Called once before the main loop begins
  • Update() - Called every frame (pure virtual, must be implemented)
  • ShutDown() - Called once after the main loop ends
  • Run() - Starts the main application loop

Protected Members

Member Type Description
mWindow skr::Arc<Window> Main window
mRenderer skr::Arc<Renderer> Main window renderer
mEventManager skr::Arc<EventManager> Main window event manager
mDeltaTime float Time since last frame (seconds)
mMainScope skr::Arc<skr::ServiceScope> Skirnir scope for the main window

Multi-window

Each window is a Skirnir service scope. Shared across windows (singletons): IPlatform, Instance, Device, MeshPool, TexturePool, MaterialPool. Per window (scoped): Window, Surface, SwapChain, CommandPool, Renderer, LightService, IndirectDrawSystem, shadows / pick / IBL.

// Resolve scoped services from the main window scope (not the root provider).
auto lights = GetMainServiceProvider()->GetService<fra::LightService>();

// F10-style secondary view with its own camera / lights, shared meshes.
auto game = CreateWindow([](fra::FreyaOptionsBuilder& o) {
    o.SetTitle("Game").SetWidth(1280).SetHeight(720).SetFullscreen(false);
});

void UpdateSecondaryWindow(const skr::Arc<fra::Window>& window) override
{
    auto renderer = GetRenderer(*window);
    auto lights = GetWindowServices(*window)->GetService<fra::LightService>();
    // ... upload instances using the shared MeshPool, then EndFrame()
}

// Close with the window itself — the app loop tears down its scope.
game->Close();

Run() pumps platform events once per frame, updates the main window, then each live secondary window. Window::Close() (or the OS close button) destroys the native window immediately; the next frame drops the window scope.

Bootstrap: resolve Device / PhysicalDevice for the first time from a window scope (AbstractApplication does this). Do not resolve them from the root provider — DeviceBuilder needs a Surface.

Renderer

Main rendering coordinator. Handles frame management, swap chain, and an ordered list of IFrameStage adapters that drive the deferred stack.

mRenderer->BeginFrame();
// Camera::Apply / lights → Scene::Upload
mRenderer->EndFrame(); // EndScene + Present

Scene (retained GPU proxy)

fra::Scene is the app-facing retained instance list. Prefer it over raw UploadSceneInstances (Advanced / tooling).

fra::Scene::Instance ground {};
ground.mesh     = groundMesh;
ground.material = groundMat;
ground.mobility = fra::Mobility::Static; // do not SetTransform every frame
scene.Add(ground);

fra::Scene::Instance actor {};
actor.mesh     = mesh;
actor.material = mat;
actor.mobility = fra::Mobility::Dynamic; // default
auto id = scene.Add(actor);

// Per frame: mutate dynamics, then Upload (dirty-aware).
scene.SetTransform(id, model);
scene.Upload(*renderer);
State What Upload does
Unchanged FiF slot commit only (skip rebuild / skip Copy if slot current)
Transforms / bones / flags Patch host GPU tables (no re-sort, no material create-info walk)
Add / Remove / Clear Full rebuild + sort by entityId

Draw submission remains GPU-driven (compute cull → multi-draw indirect). Mobility is a contract hint: static props should stay off the per-frame SetTransform/Get path so the scene can stay clean.

For tooling without a retained list, use RendererAdvanced::UploadSceneInstances.

Frame path

Frame stages

Default order:

Pick → Shadow → DeferredGeometry → Ssao → Lighting → Taa → Bloom → Composite

#include <Freya/Freya.hpp>

mRenderer->InsertFrameStage("Bloom", cell->MakeStage());

See Flexibility.

Key Methods

Method Description
BeginFrame() Start a new frame
EndScene() Run frame stages
Present() Submit the command buffer and present
EndFrame() EndScene() + Present()
RebuildSwapChain() Recreate swap chain (e.g., on resize)
InsertFrameStage / ReplaceFrameStage Insert a Freya-built stage
SetVSync(bool) Enable/disable vertical sync
SetSamples(uint32_t) Set MSAA sample count
SetDrawDistance(float) Set render distance
SetInstanceModels(const mat4*, size_t) Legacy instance matrices (with DrawInstanced)
UploadSceneInstances(span) Advanced/tooling full upload (prefer Scene::Upload)
GetCurrentFrameIndex() Get current frame index
GetFrameCount() Get total frame count
CalculateProjectionMatrix(float near, float far) Calculate projection matrix
ClearProjections() Clear all projection matrices
UpdateProjection(ProjectionUniformBuffer&) Update projection data
GetGpuAnimPass() GPU skinning pass (optional)
GetBillboardDraw() CPU billboard queue for the frame

Apps that do not customize the frame graph can keep calling EndFrame().

BillboardDraw

Per-frame queue of camera-facing quads (Renderer::GetBillboardDraw()). Cleared each BeginFrame. Use Quad for raw instances, or helpers:

Helper Notes
HealthBar(pos, w, h, fill01, bg, fg, align = Cylindrical) Background + left-aligned fill; same clip/blend/layer path
Text(pos, utf8, font, height, color, …, align = Cylindrical) SDF glyphs via FontAtlas

BillboardAlign::Screen faces the camera fully; Cylindrical yaws only (same path as Billboard::align / billboard.vert). Call sites that omit align keep cylindrical nameplates.

auto& bb = mRenderer->GetBillboardDraw();
bb.HealthBar(head, 0.85f, 0.08f, hp, bg, fg); // cylindrical
bb.HealthBar(head, 0.85f, 0.08f, hp, bg, fg, fra::BillboardAlign::Screen);

Window

Window management and input handling. Events are routed by IPlatform (SdlPlatform) so multiple windows do not steal each other's input.

mWindow->IsRunning();      // Check if window is running
mWindow->Update();         // Advance timing / FPS title (events via PumpEvents)
mWindow->GetDeltaTime();   // Get delta time in seconds

UniformBuffer

Uniform buffer for shader data.

LightService

Manages analytical lights (point, directional, spot, area) and uploads them to a shared UBO used by DeferredCompressed lighting.

FreyaOptions::maxLights (default 64, see kMaxLights / MAX_LIGHTS) caps how many lights AddLight accepts. Shader arrays are fixed at FREYA_MAX_LIGHTS (64) entries.

Light types

Type Factory Notes
Point MakePointLight(pos, color, radius, intensity) Attenuates by distance
Directional MakeDirectionalLight(dir, color, intensity) Direction is normalized
Spot MakeSpotLight(pos, dir, color, radius, innerRad, outerRad, intensity) Cone angles in radians; stored as cosines
Area MakeAreaLight(center, normal, tangent, halfW, halfH, color, intensity) Rect panel; LTC in Deferred lighting

Spot/inner and outer cutoffs on Light are cosines of the cone half-angles. The spot factory converts radians for you.

For area lights, outerCutoff stores half-width and halfHeight the half-extent along the bitangent (cross(normal, tangent)).

GPU packing (LightUniformBuffer, std140 SoA)

  • lightPositions[i] — xyz position / area center, w = LightType
  • lightColorsAndRadius[i] — rgb color, w radius
  • lightDirectionsAndCutoff[i] — xyz direction / area normal, w innerCutoff (cos)
  • lightOuterCutoffAndIntensity[i] — x outerCutoff (spot cos) or halfWidth (area), y intensity, z halfHeight (area), w = castShadows (0/1)
  • lightAreaTangents[i] — xyz area tangent
  • viewPosition, lightCount

Set Light::castShadows = true on directional / spot / point lights that should write shadow maps. Area lights never cast shadows. Shadow budgets and quality come from FreyaOptions (see Shadows below).

Usage

auto lights = serviceProvider->GetService<fra::LightService>();

lights->AddLight(fra::MakeDirectionalLight(
    glm::vec3(-0.4f, -1.0f, -0.3f), glm::vec3(1.0f), 0.4f));

const fra::LightHandle point =
    lights->AddLight(fra::MakePointLight(
        glm::vec3(0.0f, 5.0f, 0.0f),
        glm::vec3(1.0f, 0.4f, 0.3f),
        50.0f,
        0.5f));

const fra::LightHandle spot =
    lights->AddLight(fra::MakeSpotLight(
        glm::vec3(0.0f, 8.0f, 4.0f),
        glm::vec3(0.0f, -1.0f, -0.5f),
        glm::vec3(0.9f, 0.95f, 1.0f),
        60.0f,
        glm::radians(12.0f),
        glm::radians(22.0f),
        1.0f));

lights->AddLight(fra::MakeAreaLight(
    glm::vec3(0.0f, 6.0f, 0.0f),
    glm::vec3(0.0f, -1.0f, 0.0f),
    glm::vec3(1.0f, 0.0f, 0.0f),
    3.0f,
    1.5f,
    glm::vec3(1.0f, 0.95f, 0.9f),
    4.0f));

// Per-frame: position-only or full replace (LightHandle, not raw indices)
lights->UpdateLightPosition(point, glm::vec3(2.0f, 5.0f, 0.0f));

if (const auto* current = lights->GetLight(spot))
{
    fra::Light updated = *current;
    updated.position   = glm::vec3(1.0f, 6.0f, 2.0f);
    updated.direction  = glm::normalize(-updated.position);
    // Light::type is fra::LightType (enum), not float
    lights->UpdateLight(spot, updated);
}

AddLight returns a null LightHandle when the pool is full (operator bool / IsValid()). RemoveLight / UpdateLight / GetLight take handles.

Renderer::UpdateCamera refreshes the light UBO for the current frame when the light service is present (also uploads iblIntensity for IBL).

IBLService

Provides split-sum image-based lighting: an equirectangular environment map with GGX importance-sampled specular mips (roughness → LOD), a convolved irradiance map, a BRDF integration LUT, and parametric LTC LUTs used by rectangular area lights. Built at startup from FreyaOptions::environmentMapPath (Radiance .hdr via stbi_loadf; maps wider than 1024px are downsampled for irradiance; specular prefilter bakes at ≤512px wide). Default path is ./Resources/Environments/studio_small_09_4k.hdr (copied from the repo-root Resources/ into example binary dirs). Set the path to empty to force the procedural sky; if the file is missing, the procedural sky is used as well.

Resource Role
Environment Specular IBL via textureLod (GGX prefiltered mips)
Irradiance Diffuse IBL (CPU hemisphere convolution)
BRDF LUT Specular split-sum scale/bias
LTC matrix/ampl Linearly Transformed Cosines for area lights

Configure with SetIblIntensity / SetEnvironmentMapPath on FreyaOptionsBuilder. Deferred lighting bindings 7–9 sample IBL; 10–11 are LTC.

Shadows

ShadowPass runs before deferred geometry each frame and produces:

Target Technique Limit
Directional Cascaded shadow maps (2D array, multiview) shadowCascadeCount (1–4)
Spot Perspective depth map (2D array) maxSpotShadows (0–4)
Point Cube array (multiview 6 faces) maxPointShadows (0–2)

Configure via FreyaOptionsBuilder: SetShadowQuality presets (Low / Medium / High / Ultra) or individual setters (SetShadowCascadeCount, SetShadowMapResolution, SetShadowBias, SetMaxSpotShadows, SetMaxPointShadows, SetShadowSampleCount, SetShadowPointResolutionDivisor, SetShadowSpotResolutionDivisor, SetShadowPointUpdatePeriod).

Spot/point map size defaults to cascade resolution / 2. Point cubes rebuild every shadowPointUpdatePeriod frames when the light is stable. | Preset | Resolution | Cascades | Spot | Point | Soft taps | |--------|------------|----------|------|-------|-----------| | Low | 512² | 2 | 2 | 2 | 4 | | Medium | 1024² | 3 | 4 | 2 | 8 | | High | 2048² | 4 | 4 | 2 | 16 | | Ultra | 4096² | 4 | 4 | 2 | 16 |

Defaults without a preset: 4 cascades, 2048², bias 0.002, 4 spot / 2 point slots, 16 soft-shadow taps. Spot/point budgets of 0 keep a 1×1 descriptor stub instead of a full-resolution atlas.

Lighting shaders multiply each light’s radiance by a PCF shadow factor (hardware compare samplers). Deferred lighting bindings 12–15 hold the shadow UBO and cascade / spot / point maps.

DeferredCompressedPass

Deferred rendering pass with G-buffer compression.