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 beginsUpdate()- Called every frame (pure virtual, must be implemented)ShutDown()- Called once after the main loop endsRun()- 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
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 = LightTypelightColorsAndRadius[i]— rgb color, w radiuslightDirectionsAndCutoff[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 tangentviewPosition,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.