Skip to content

Platform Layer

Lumina targets Windows and Linux. The platform layer is thin and mostly lives in Runtime/Platform, Runtime/Core/Windows, and Runtime/Input. Generic declarations sit in Platform/GenericPlatform.h and Platform/Platform.h, with per-platform implementations under Platform/Windows and Platform/Linux.

The split is narrower than the directory listing suggests. Windowing, input and the filesystem are portable already, because they go through GLFW and std::filesystem rather than through the OS directly, so what actually needs a second implementation is process spawning (WindowsPlatformProcess.cpp / LinuxPlatformProcess.cpp) and crash handling (WindowsCrashHandler.cpp / LinuxCrashHandler.cpp). Crash reporting to a hosted service is Windows-only; on Linux the handler still writes a local report to Binaries/<Platform>/CrashDumps and says so at startup.

FWindow (Core/Windows/Window.h) wraps GLFW. GLFWwindow is forward declared; the full glfw3.h is pulled in only by translation units that need it, through GLFWInclude.h.

FUIntVector2 GetExtent();
float GetContentScale(); // 1.0 = 96 DPI, drives editor UI scaling
FUIntVector2 GetMonitorResolution();
void SetWindowPosition / SetWindowSize / SetCursorMode / SetTitleBarHovered
bool ShouldClose / IsWindowMinimized / IsWindowMaximized
void Minimize / Restore / Maximize / Close / CancelClose
GLFWwindow* GetWindow(); // for the Vulkan surface and the ImGui GLFW backend

Rules:

  • GLFW calls are main thread only. That includes RHI::CreateSurface, which is why the surface is created on the main thread and handed to the render side.
  • CancelClose() clears the close flag the OS set (the X button, Alt+F4). The editor uses it to stay alive when the user cancels the unsaved-changes prompt.
  • FWindow::OnWindowResized is a static multicast delegate. FApplication, FRenderManager, and the input viewport registry all subscribe.
  • Windowing::GetPrimaryWindowHandle() returns the main window. Windowing::SetCursorModeForNativeWindow and Windowing::IsNativeWindowFocused take a native handle, so mouse capture and focus resolution can target a dragged-out preview window rather than always the primary one.

The frame loop skips all world updates while the window is minimized.

FEventProcessor (Events/EventProcessor.h) dispatches FEvent objects to IEventHandler implementations in layer order. EInputLayer values are:

LayerValueHandler
Viewport1000FInputViewportRegistry
EditorChrome500The development tool UI
Default0Everything else

Higher values are dispatched first, and OnEvent returning true consumes the event. That ordering is what lets a focused viewport take input before editor chrome sees it.

Input is viewport-scoped, not global. FInputViewport (Input/InputViewport.h) represents one rectangle that can receive input, and owns an FInputContext holding the actual key, axis, and mouse state.

A viewport carries:

  • Its window rectangle and its render target size, which are different when the render target is scaled.
  • Hovered and focused flags.
  • The CWorld it drives.
  • The native window handle it is currently drawn into, set each frame by the editor.

FInputViewportRegistry is the singleton that owns the set and tracks three distinct pointers:

PointerMeaning
HoveredThe viewport under the cursor.
FocusedThe viewport with keyboard focus.
ActiveThe viewport receiving game input.

Plus IsGameInputFocused() / SetGameInputFocused(), which is how PIE takes exclusive input, and GetRawInput() for unfiltered device state.

EndFrame(DeltaSeconds) runs after FEngine::Update and advances edge states (pressed and released transitions) for the next frame.

Two things that are easy to get wrong:

  • Setting the mouse mode on a context does not touch the window cursor. ReapplyActiveCursorMode() pushes it through to the OS.
  • With multiple platform windows, OS focus is authoritative and exactly one window has it. IsNativeWindowFocused disambiguates which preview window should receive game input.

In a game build, FApplication creates a single primary viewport covering the window and marks it hovered, focused, and active. In an editor build there is no primary viewport at all; every tool owns and registers its own.

  • SKey (Input/Key.h) is the key identity type, reflected so bindings can be edited in the property grid.
  • SInputAction is a named action and SInputMappingContext is a named layer of them. FInputActionMap caches both and is rebuilt from CInputSettings whenever those settings are saved (FCoreDelegates::OnSettingsSaved). Every rebuild bumps a serial, which is what invalidates cached action indices.
  • FInputActionMap::UpdateContext evaluates every action into the querying FInputContext once per frame, before the world update, so every read within a frame sees one consistent snapshot.
  • FInputContext also owns the pushed mapping-layer stack. FInputActionMap::PassesGate walks it top down: the first layer listing an action allows it, and a layer with bBlockLower that does not list it stops there. With an empty stack, bRunsInUI plus EInputMode decide, as before.
  • EInputMode selects game, UI, or mixed routing, and still gates raw device reads independently of the layer stack.
  • Input/InputQuery.h is the gameplay surface. Input::GetReceivingContext is the single definition of “is this world receiving input” (a viewport exists, game input is focused, and it is the active viewport); every other query goes through it, so the action and raw-device families cannot disagree.
  • FInputActionHandle caches an action’s index against the map’s serial, and re-resolves from the name when it moves, so a rebind does not invalidate gameplay code holding one.
  • SInputComponent is a tag, not a store: it marks which entities’ scripts receive input. SInputSystem reads it, dispatches OnInput, and polls C# SInputAction bindings. It stores no device state.
  • FInputProcessor is a thin facade over the active context, for engine code outside a world.

Three layers:

  • Platform/Filesystem and FileHelper for native file access (LoadFileIntoString, and so on).
  • Runtime/FileSystem for the virtual file system and the pak-backed filesystem.
  • Runtime/Paths for the canonical directories: engine directory, engine install directory, project directory, project content and scripts directories.

Paths::InitializePaths() runs in FApplication::PreInitStartup, before anything reads a file.

Platform/Process wraps process creation and control. PlatformProcess is what launches the build tools, opens the generated solution, and runs project file regeneration from the editor.

Platform/Time/PlatformTime.h is the engine’s only clock. There is no std::chrono in engine code, and the older Platform::GetTime() is gone.

CallMeaning
PlatformTime::Cycles()The raw monotonic counter. QPC on Windows, CLOCK_MONOTONIC elsewhere. Platform-defined units.
PlatformTime::Seconds()Monotonic seconds since process start. What the frame loop and every profiler use.
PlatformTime::ToSeconds/ToMilliseconds/ToMicroseconds(Delta)Turn a cycle delta into a unit.
PlatformTime::UtcNanoseconds() / UtcSeconds()Wall clock since the Unix epoch.
PlatformTime::LocalTime(Ns) / UtcTime(Ns) / LocalNow()Broken-down FDateTime for stamps and file names.
PlatformTime::Sleep(Seconds), SleepMilliseconds, SleepMicrosecondsBlocking waits.
PlatformTime::YieldThread()Gives up the rest of the time slice.
PlatformTime::FStopwatchMeasures a span in cycles, converting only when asked.

Measure spans with the monotonic side and timestamp with the wall clock side. UtcNanoseconds jumps when the system clock is set, so a duration computed from it can come out negative.

PlatformTime::EnableHighResolutionTiming() / DisableHighResolutionTiming() bracket the engine’s lifetime so the frame limiter’s sleeps land with 1 ms granularity.

It is YieldThread, not Yield, because windows.h defines Yield as an empty macro and PlatformTime::Yield() would expand to PlatformTime::.

CrashHandler::Install() is the very first call in LuminaMain, before the global state object exists, so a crash during initialization is still captured.

It installs a structured exception handler that writes a minidump plus a symbolized callstack into CrashDumps/. CrashHandler::Shutdown() removes it at the end of LuminaMain.

GPU faults are separate; see Vulkan Backend for device fault and Aftermath.

HangWatchdog::Start() runs immediately after the crash handler. A background thread watches for HangWatchdog::Heartbeat(), which the main thread calls at the top of every FEngine::Update. If the heartbeat stops advancing, the watchdog dumps every thread’s callstack.

Subsystems whose work rides a pool worker rather than a thread of their own can register a reporter so they still show up in a dump, since the watchdog cannot find them by thread.

Core/Threading/Sync.h provides the locks, and they are the engine’s own, not aliases over the standard library:

TypeBacking
FMutexSRWLOCK on Windows, a pthread mutex elsewhere. 8 bytes and constexpr-constructible, against roughly 80 for std::mutex. Not recursive.
FSharedMutexThe same SRWLOCK, taken shared or exclusive.
FRecursiveMutexDepth counted on top of FMutex with an atomic owner id.
FScopeLock, FWriteScopeLock, FRecursiveScopeLockTScopeLock<T> over the three.
FReadScopeLockShared lock, with a TryToLock overload and OwnsLock().
FUniqueLockReleasable and retakeable, with a DeferLock overload. What a condition-variable wait needs.
FConditionVariableWait, WaitFor(Lock, Seconds), NotifyOne, NotifyAll, plus predicate overloads.
FOnceFlag and CallOnce(Flag, Body)One-time initialization. The losing threads block until the winner finishes, not merely starts.
FThreadAn OS thread that owns its callable. Join or detach it; the destructor detaches.

WaitFor returns false only on timeout, so a spurious wake returns true. Use the predicate overload, which re-checks.

Core/Threading/Thread.h provides the rest:

  • Threading::Initialize(MainThreadName) and Shutdown, called by FApplicationGlobalState.
  • SetThreadName(Name, GroupHint), which also assigns the Tracy timeline group. EThreadGroup values (Main 0, Physics 10, Audio 20, Worker 100, Fiber 200, Other 1000) control ordering: lower sorts higher, and threads sharing a hint are grouped.
  • SetThreadPerformanceHint(), which opts a thread out of EcoQoS power throttling.
  • InitializeThreadHeap() / ShutdownThreadHeap() for the rpmalloc per-thread heap.

Prefer the fiber-aware locks from TaskSystem/FiberSync.h for anything a job may contend. See Task System.

SymptomCause
Crash creating a window or surfaceA GLFW call off the main thread.
Cursor mode does not applyThe context was changed without ReapplyActiveCursorMode().
Input goes to the wrong viewport with multiple windowsNative window focus not consulted; OS focus is authoritative.
A spin loop suddenly costs secondsSleep(0) where YieldThread() belongs. It hands over the whole quantum, not just the rest of the slice.
A duration comes out negativeMeasured with UtcNanoseconds, which jumps when the clock is set. Use Seconds() or Cycles().
A condition-variable wait returns early and the code proceedsWaitFor returns true on a spurious wake. Use the predicate overload.
Editor closes despite a cancelled save promptCancelClose() was not called, so the OS close flag is still set.
Nothing updates after minimizingExpected: the frame loop skips world updates while minimized.
No crash dump for an early crashThe crash occurred before CrashHandler::Install, which is the first line of LuminaMain.
Hang dump does not show the culpritThe stalled work is on a fiber with no OS thread. Look for the registered reporter output instead.