Part of FireMonkey Animation Demos (repo: omonien/AetherOrbits).
This unit is the reusable core: copy it into any FMX project for a VSync-aligned frame loop.
FMXAnimation.GameLoop.pas is intentionally demo-agnostic:
- Uses:
System.Classes,System.Diagnostics,System.SysUtils,FMX.Types,FMX.Ani - Does not use: forms, Skia, scene types, or any demo-specific code
Copy the unit into another FMX project, create a TGameLoop, assign OnUpdate /
OnRender, call StartLoop. No other unit from this repository is required.
TGameLoop = class(TAnimation)
protected
procedure ProcessAnimation; override; // ← the whole point
end;In Delphi 13, FireMonkey animations are advanced by the platform Display Link
Service. That service is tied to the display refresh (VSync). For each
active TAnimation, FMX invokes ProcessAnimation on those ticks.
Therefore:
- You do not invent your own frame clock.
- You plug into the same pipeline FMX already uses for property animations.
- You get VSync-aligned callbacks without third-party libraries.
| Hook | Role |
|---|---|
TAnimation.ProcessAnimation |
Framework entry point (Display Link) — required core |
| Fixed timestep + accumulator | Portable game-loop math inside that entry point |
OnUpdate / OnRender |
Consumer callbacks — no knowledge of scene/UI in the loop |
- High-resolution timing (
TStopwatch) for real frame delta - Max-frame clamp (debugger / Alt-Tab spiral-of-death guard)
- Fixed timestep updates (Glenn Fiedler pattern)
- One render/invalidate notification per processed frame
- Optional Preferred-FPS pacing (see below)
If you only remember one line from this project: the kernel is the
ProcessAnimation override on a TAnimation subclass under Delphi 13’s
Display Link.
Do not treat these as the same knob:
| Concept | Source | Role |
|---|---|---|
| Display-link / VSync | FMX Display Link | How often ProcessAnimation is called |
| Preferred FPS | GlobalPreferredFramesPerSecond (demo radios: 30/60/120) |
How often this loop should do update+render |
| FixedTimeStep | TGameLoop.FixedTimeStep (default 1/60) |
Simulation/physics step size only |
| Platform | Display-link call rate | Preferred honouring |
|---|---|---|
| iOS / macOS | CADisplayLink | Usually respects preferred range |
| Windows (DWM) | DwmFlush ≈ monitor Hz |
Preferred interval is largely ignored by FMX |
So on Windows, choosing 30 in the UI still yields ~60 display-link callbacks unless the game loop itself paces.
When PaceToPreferredFps is True (default), ProcessAnimation returns early until at least roughly 1 / GlobalPreferredFramesPerSecond seconds of wall time have elapsed since the last processed frame. That makes Preferred 30 actually measure ~30 FPS on Windows while leaving FixedTimeStep alone.
- Preferred cannot exceed the display-link ceiling (120 preferred on a 60 Hz panel stays ~60).
- Unit tests that call
ProcessAnimationback-to-back without real time should setPaceToPreferredFps := False. - Restarting the loop after changing preferred still helps Mac/iOS CADisplayLink re-apply its range; pacing covers the Windows DWM gap.
This repository’s demo does use Skia (see the main README section Where Skia fits), but only for drawing:
- Skia is not a black-box game engine that replaces
TGameLoop. - Skia does not own the frame clock; Display Link /
ProcessAnimationdoes. - Skia does give you access to efficient rendering backends (CPU raster,
GPU paths where available) through an explicit canvas API (
ISkCanvas, paints, shaders).
OnRender in this unit is deliberately abstract — typically “invalidate a
surface”. Whether that surface is painted with Skia, the classic FMX canvas, or
something else is the consumer’s choice and stays outside this unit.
TAnimation.Start (FMX.Ani) only subscribes to the Display Link when:
Root <> nil(animation is in the FMX object tree — setParentto the form), and- Duration is not treated as “immediate”, and
- If
Parentis anIControl, that control is Visible.
If Root = nil, FMX runs a one-shot immediate animation (ProcessAnimation once) and sets Running := False. Symptom: first paint looks correct, then everything freezes.
TGameLoop.Create parents itself to the owner when the owner is a TFmxObject. Call StartLoop from OnShow (or after the form is visible), not only from OnCreate.
StartLoop raises EInvalidOpException if Root is still nil — fail loud instead of a silent freeze.
- FMX + Skia quirks — Metal, Root, hybrid UI vs self-rendered HUD