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.
Windowing
Section titled “Windowing”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 scalingFUIntVector2 GetMonitorResolution();void SetWindowPosition / SetWindowSize / SetCursorMode / SetTitleBarHoveredbool ShouldClose / IsWindowMinimized / IsWindowMaximizedvoid Minimize / Restore / Maximize / Close / CancelCloseGLFWwindow* GetWindow(); // for the Vulkan surface and the ImGui GLFW backendRules:
- 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::OnWindowResizedis a static multicast delegate.FApplication,FRenderManager, and the input viewport registry all subscribe.Windowing::GetPrimaryWindowHandle()returns the main window.Windowing::SetCursorModeForNativeWindowandWindowing::IsNativeWindowFocusedtake 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.
Events
Section titled “Events”FEventProcessor (Events/EventProcessor.h) dispatches FEvent objects to
IEventHandler implementations in layer order. EInputLayer values are:
| Layer | Value | Handler |
|---|---|---|
Viewport | 1000 | FInputViewportRegistry |
EditorChrome | 500 | The development tool UI |
Default | 0 | Everything 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
CWorldit 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:
| Pointer | Meaning |
|---|---|
| Hovered | The viewport under the cursor. |
| Focused | The viewport with keyboard focus. |
| Active | The 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.
IsNativeWindowFocuseddisambiguates 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.
Actions and bindings
Section titled “Actions and bindings”SKey(Input/Key.h) is the key identity type, reflected so bindings can be edited in the property grid.SInputActionis a named action andSInputMappingContextis a named layer of them.FInputActionMapcaches both and is rebuilt fromCInputSettingswhenever those settings are saved (FCoreDelegates::OnSettingsSaved). Every rebuild bumps a serial, which is what invalidates cached action indices.FInputActionMap::UpdateContextevaluates every action into the queryingFInputContextonce per frame, before the world update, so every read within a frame sees one consistent snapshot.FInputContextalso owns the pushed mapping-layer stack.FInputActionMap::PassesGatewalks it top down: the first layer listing an action allows it, and a layer withbBlockLowerthat does not list it stops there. With an empty stack,bRunsInUIplusEInputModedecide, as before.EInputModeselects game, UI, or mixed routing, and still gates raw device reads independently of the layer stack.
Reading input
Section titled “Reading input”Input/InputQuery.his the gameplay surface.Input::GetReceivingContextis 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.FInputActionHandlecaches 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.SInputComponentis a tag, not a store: it marks which entities’ scripts receive input.SInputSystemreads it, dispatchesOnInput, and polls C#SInputActionbindings. It stores no device state.FInputProcessoris a thin facade over the active context, for engine code outside a world.
Filesystem
Section titled “Filesystem”Three layers:
Platform/FilesystemandFileHelperfor native file access (LoadFileIntoString, and so on).Runtime/FileSystemfor the virtual file system and the pak-backed filesystem.Runtime/Pathsfor 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.
Process
Section titled “Process”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.
| Call | Meaning |
|---|---|
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, SleepMicroseconds | Blocking waits. |
PlatformTime::YieldThread() | Gives up the rest of the time slice. |
PlatformTime::FStopwatch | Measures 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::.
Crash handling
Section titled “Crash handling”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.
Hang watchdog
Section titled “Hang watchdog”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.
Threading primitives
Section titled “Threading primitives”Core/Threading/Sync.h provides the locks, and they are the engine’s own, not
aliases over the standard library:
| Type | Backing |
|---|---|
FMutex | SRWLOCK on Windows, a pthread mutex elsewhere. 8 bytes and constexpr-constructible, against roughly 80 for std::mutex. Not recursive. |
FSharedMutex | The same SRWLOCK, taken shared or exclusive. |
FRecursiveMutex | Depth counted on top of FMutex with an atomic owner id. |
FScopeLock, FWriteScopeLock, FRecursiveScopeLock | TScopeLock<T> over the three. |
FReadScopeLock | Shared lock, with a TryToLock overload and OwnsLock(). |
FUniqueLock | Releasable and retakeable, with a DeferLock overload. What a condition-variable wait needs. |
FConditionVariable | Wait, 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. |
FThread | An 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)andShutdown, called byFApplicationGlobalState.SetThreadName(Name, GroupHint), which also assigns the Tracy timeline group.EThreadGroupvalues (Main0,Physics10,Audio20,Worker100,Fiber200,Other1000) 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.
Common failure modes
Section titled “Common failure modes”| Symptom | Cause |
|---|---|
| Crash creating a window or surface | A GLFW call off the main thread. |
| Cursor mode does not apply | The context was changed without ReapplyActiveCursorMode(). |
| Input goes to the wrong viewport with multiple windows | Native window focus not consulted; OS focus is authoritative. |
| A spin loop suddenly costs seconds | Sleep(0) where YieldThread() belongs. It hands over the whole quantum, not just the rest of the slice. |
| A duration comes out negative | Measured with UtcNanoseconds, which jumps when the clock is set. Use Seconds() or Cycles(). |
| A condition-variable wait returns early and the code proceeds | WaitFor returns true on a spurious wake. Use the predicate overload. |
| Editor closes despite a cancelled save prompt | CancelClose() was not called, so the OS close flag is still set. |
| Nothing updates after minimizing | Expected: the frame loop skips world updates while minimized. |
| No crash dump for an early crash | The crash occurred before CrashHandler::Install, which is the first line of LuminaMain. |
| Hang dump does not show the culprit | The stalled work is on a fiber with no OS thread. Look for the registered reporter output instead. |