MFD is a C++/CMake toolkit for building 2D multi-function display windows.
It combines:
raylibImGuirlImGuiEnTTnlohmann_json- Protocol Buffers over UDP for runtime control
The project is built around one simple workflow:
- describe a window, its pages, and reusable reticles in JSON
- render the active page in 2D
- drive the window locally or from an external client
- receive strobe feedback and framebuffer readback when needed
Automated runtime and JSON-loading checks are also built with the project
through GoogleTest.
If you are new to the project, use this order:
If you already know what you want:
- create reticles: 01 Create Reticles From Primitives
- create pages and windows: 02 Create Pages And Windows
- test without writing code: 03 Test A Window With The Mockup
- drive a window from an external app: 04 Drive A Window From A Live Client Over UDP
- add and remove tracks or symbols at runtime: 05 Dynamic Reticles
- use strobe control and feedback: 06 Strobe Control And Feedback
- capture the framebuffer as RGBA32: 07 Framebuffer RGBA32 Capture
- project nautical-mile and radian data to page space on the client: 08 Project User Space To Page Space
- manage synchronized page-local blinking: 09 Page-Managed Blink
- drive the cockpit showcase: 10 Drive The Cockpit Demo
- use the mockup as a client API reference: 11 Use The Mockup As A Client API Reference
- run the automated runtime tests: 12 Run The Automated Runtime Tests
With the toolkit you can:
- load a window and its pages from JSON
- define one window-level text font from the root window JSON
- create reusable reticles from primitives
- mark one page as the default startup page with
defaultPage - activate a page by name
- black out the whole rendered window without clearing runtime state
- update reticle visibility, position, rotation, color, thickness, text, and letter spacing
- declare page-local blink types and switch reticles between synchronized blink groups
- add, update, and remove dynamic reticles
- control an optional page strobe
- receive strobe state and capture feedback from the window
- capture the current framebuffer as a typed
RGBA32CPU buffer - project physical or user-space coordinates on the client without changing the window runtime
flowchart LR
A[Window JSON] --> B[JsonLoader]
C[Page JSON files] --> B
D[Reticle library JSON] --> B
B --> E[SceneRegistry]
F[External client] -->|UDP protobuf commands| G[UdpRuntimeBridge worker]
G --> H[Render thread]
H --> I[CommandProcessor / EnTT dispatcher]
I --> E
E --> J[MfdRenderer / Canvas2D]
J --> K[Window]
H --> L[Strobe feedback snapshots]
L --> G
G -->|UDP protobuf feedback| F
K --> M[OpenGlFramebufferReader]
Read this as:
- JSON files define the authoring model
SceneRegistryholds the runtime state- a background UDP I/O worker receives commands and sends feedback
- the render thread drains queued commands and dispatches them through
CommandProcessor - the renderer draws only the active page
- strobe feedback can go back to the client without blocking rendering
- the framebuffer can be captured as
RGBA32
The default runtime architecture now uses two threads inside the window:
- one render thread
runs at the window frame rate, owns
SceneRegistry,CommandProcessor,EnTT, and rendering - one UDP I/O worker thread owns the UDP sockets, receives command packets, and sends strobe feedback packets
Important rule:
- the UDP worker thread does not modify the scene directly
- it only pushes decoded commands into a thread-safe queue
- the render thread drains that queue and applies the commands
This keeps raylib, OpenGL, and the scene state on the same thread while
still decoupling network I/O from rendering.
The reticle model supports:
texttimelinecirclerectangleellipsesquarediamondtrianglepolylinebezier
Notes:
type: "heure"is accepted as an alias fortimetimeis a text-like primitive that formats the current clock valuerectangleandellipseare dedicated primitives, separate fromsquare
Authoring and runtime work in normalized logical coordinates, not pixels.
y = +1
^
|
x = -1 <---+---> x = +1
|
v
y = -1
Rules:
- logical space is
[-1, 1] (0, 0)is the center of the pagex > 0goes righty > 0goes up- reticles are authored independently from the window pixel size
This means a page remains valid when the window is resized.
Blink is page-managed.
- each page can declare several named blink types
- each blink type has one effective duration in milliseconds
- reticles can opt in to blinking and optionally request one named type
- the runtime resolves the type once, then synchronizes by effective duration
This means that inside one page:
- two reticles using two different type names with the same duration blink in phase
- they appear and disappear at the same time
- changing a reticle from
slowtofastimmediately re-attaches it to the new synchronized group
mfd_apiPublic API library and runtime core.mfd_windowGeneric top-level window host loading any window JSON from the command line.examples/mfd_demoPreset launcher forassets/windows/demo_pages.json.examples/mfd_demo_minimalPreset launcher forassets/windows/demo_pages_minimal.json.examples/mfd_demo_cockpitPreset launcher forassets/windows/demo_pages_cockpit.json.examples/client_mockup_minimalHeadless cockpit client showing the public UDP API from one plainmainloop.examples/client_mockupGUI client used to send commands and inspect strobe feedback.mfd_editorVisual editor for pages and reticles.mfd_api/testsGoogleTest-based automated tests for JSON loading and runtime rules.assets/windowsRoot window JSON files.assets/pagesPage JSON files.assets/reticlesReusable reticle templates.docsQuick start, concept pages, and tutorials.
Visual Studio 2022 presets are provided for x64 and Win32.
cmake --preset vs2022-x64
cmake --build --preset debug-x64
cmake --build --preset release-x64
cmake --preset vs2022-win32
cmake --build --preset debug-win32
cmake --build --preset release-win32Dependencies are fetched by CMake.
GoogleTest is integrated into the default build through the mfd_api_tests
target.
Default behavior:
MFD_BUILD_TESTS=ON- the regular build compiles
mfd_api_tests - the suite focuses on
JsonLoaderandSceneRegistry
Commands:
cmake --preset vs2022-win32
cmake --build --preset debug-win32
.\build\vs2022-win32\mfd_api\tests\Debug\mfd_api_tests.exeThere is also a convenience target:
cmake --build --preset debug-win32 --target mfd_api_tests_runFor the exact field reference, use:
A root window file defines:
- title
- pixel size
- initial screen position
- target FPS
- optional text font file
- reticle library folder
- UDP command transport
- optional UDP feedback transport
- list of page JSON files
- optional
defaultPagefield naming the startup page
Example:
{
"title": "MFD EnTT Demo",
"size": [1600, 960],
"position": [80, 60],
"targetFps": 60,
"fontFile": "../fonts/ocr_a.ttf",
"reticleLibraryFolder": "../reticles",
"commands": {
"udp": {
"enabled": true,
"address": "127.0.0.1",
"port": 47220,
"maxPacketSize": 16384
}
},
"feedback": {
"udp": {
"enabled": true,
"address": "127.0.0.1",
"port": 47221,
"maxPacketSize": 4096
}
},
"defaultPage": "Radar",
"pages": [
"../pages/pfd.json",
"../pages/navigation.json",
"../pages/aircraft_centric.json",
"../pages/radar.json",
"../pages/tactical.json"
]
}Each page is defined in its own file.
Example:
{
"name": "Radar",
"title": "Radar",
"backgroundColor": "#061306FF",
"blinkTypes": [
{ "name": "slow", "durationMs": 1000 },
{ "name": "fast", "durationMs": 320 },
{ "name": "caution", "durationMs": 1000 }
],
"defaultBlink": "slow",
"view": {
"center": [0.0, 0.0],
"zoom": 1.0
},
"staticReticles": [
{
"id": "fixed_track_alpha",
"template": "radar_track",
"blink": "slow"
},
{
"id": "fixed_track_bravo",
"template": "radar_track",
"blink": "caution"
},
{
"id": "radar_caption",
"blink": "fast",
"elements": [
{
"type": "text",
"text": "Synchronized page blink"
}
]
}
]
}Reusable templates live in assets/reticles.
The project ships a working example using rectangle, ellipse, text, and
time in assets/reticles/status_clock.json.
#include "mfd/io/JsonLoader.h"
mfd::JsonLoader loader;
const mfd::LoadedWindowConfiguration loaded =
loader.LoadWindowConfiguration("assets/windows/demo_pages.json");#include "mfd/runtime/SceneRegistry.h"
mfd::SceneRegistry scene(loaded.document);
scene.SetActivePage("Radar");
scene.SetPageView("Radar", {{0.15f, -0.05f}, 1.8f});
scene.SetReticleVisible("Radar", "fixed_track_alpha", true);
scene.SetReticleBlinkType("Radar", "fixed_track_alpha", "fast");
scene.SetReticlePosition("Radar", "fixed_track_alpha", {0.25f, 0.10f});
scene.SetReticleRotation("Radar", "fixed_track_alpha", 18.0f);
scene.SetReticleColor("Radar", "fixed_track_alpha", {0, 255, 0, 255});
scene.SetReticleThickness("Radar", "fixed_track_alpha", 0.004f);
scene.SetReticleText("Radar", "fixed_track_alpha", "T01");
scene.SetReticleLetterSpacing("Radar", "fixed_track_alpha", 0.012f);#include "mfd/control/CommandClient.h"
mfd::WindowUdpCommandTransport udp;
udp.enabled = true;
udp.address = "127.0.0.1";
udp.port = 47220;
udp.maxPacketSize = 16384;
mfd::CommandClient client(udp);
client.ActivatePage("Radar");
client.SetReticleBlinkType("Radar", "fixed_track_alpha", "fast");
client.SetReticlePosition("Radar", "fixed_track_alpha", {0.20f, 0.35f});
client.SetReticleColor("Radar", "fixed_track_alpha", {77, 224, 255, 255});
client.SetReticleVisible("Radar", "fixed_track_alpha", true);#include "mfd/control/UdpRuntimeBridge.h"
mfd::CommandProcessor processor(scene);
mfd::UdpRuntimeBridge bridge(loaded.window.commandTransports,
loaded.window.feedbackTransports);
bridge.Start();
std::vector<mfd::UserCommand> commands;
commands.clear();
bridge.DrainReceivedCommands(commands);
processor.Submit(std::span<const mfd::UserCommand>(commands.data(), commands.size()));std::vector<mfd::DynamicReticleState> tracks;
tracks.push_back({
"track_001",
mfd::ReticlePatch {
.visible = true,
.position = mfd::Vec2 {0.20f, 0.35f},
.rotationDegrees = 15.0f,
.text = std::string {"AF001"}
}});
tracks.push_back({
"track_002",
mfd::ReticlePatch {
.visible = true,
.position = mfd::Vec2 {-0.40f, 0.10f},
.rotationDegrees = -30.0f,
.text = std::string {"AF002"}
}});
client.UpsertDynamicReticles("Radar", "radar_track", tracks);CommandClient uses compact Protocol Buffers payloads and automatically splits
oversized batches according to the configured UDP packet size.
#include "mfd/control/UserSpaceProjector.h"
mfd::UserSpaceFrame frame;
frame.userOrigin = aircraftPositionNm;
frame.pageAnchor = {0.0f, -0.3f};
frame.originRotationRadians = aircraftHeadingRadians;
frame.pageUnitsPerUserUnit = 0.04f; // 1 user unit becomes 0.04 page units
frame.userXAxisInPage = {0.0f, 1.0f}; // physical X -> page Y
frame.userYAxisInPage = {1.0f, 0.0f}; // physical Y -> page X
mfd::UserSpaceProjector projector(frame);
const mfd::Vec2 trackPagePosition = projector.ToPagePosition(trackWorldPositionNm);
const float trackPageRotationDegrees = projector.ToPageRotationDegrees(trackHeadingRadians);This keeps the window unchanged:
- the page still uses
[-1, 1] - the client can keep nautical miles and radians internally
- only the helper performs the conversion before sending the command
- if you do not need such a conversion, skip the helper and keep using page coordinates directly
A page can expose an optional strobe.
The client can:
- enable or disable it
- move it
The window can send feedback back to the client, including:
- active state
- actual position
- capture configuration
- magnetization state
- optional captured reticle information
Main APIs:
mfd::CommandClientmfd::CreateFeedbackReceiverChannel(...)mfd::DeserializeStrobeStatusFeedback(...)
See the full workflow in docs/tutorials/06_strobe_control_and_feedback.md.
The renderer side exposes mfd::OpenGlFramebufferReader.
#include "mfd/render/OpenGlFramebufferReader.h"
mfd::Rgba32Framebuffer capture = mfd::OpenGlFramebufferReader::ReadRgba32();The returned buffer contains:
widthheight- typed
Rgba8Pixelpixels Pixels()Bytes()
Bytes() exposes the framebuffer as a raw std::byte span, which is useful
when the next stage is byte-oriented, for example a shared-memory writer.
If you host a window through mfd::window::RunLauncher, you can also attach an
optional callback that receives the final RGBA32 buffer every frame:
#include <cstddef>
#include <iostream>
#include <span>
#include "mfd/window/WindowLauncher.h"
int main(int argc, char** argv)
{
mfd::window::LauncherConfig config;
config.applicationName = "mfd_demo_minimal";
config.defaultWindowFile = "assets/windows/demo_pages_minimal.json";
return mfd::window::RunLauncher(
argc,
argv,
config,
[](int width, int height, std::span<const std::byte> pixels)
{
static bool printed = false;
if (!printed)
{
std::cout << "Here we receive the pixel buffer." << '\n';
printed = true;
}
(void)width;
(void)height;
(void)pixels;
});
}The byte span is only valid during the callback. Copy it if another stage must keep the buffer after the callback returns.
See the full workflow in docs/tutorials/07_framebuffer_rgba32_capture.md.
examples/mfd_demoThin preset launcher built on top ofmfd_window.examples/mfd_demo_cockpitThin cockpit preset launcher built on top ofmfd_window.examples/mfd_demo_minimalThin minimal preset launcher built on top ofmfd_window.mfd_windowGeneric runtime launcher accepting--window <json>.examples/client_mockup_minimalMinimal headless client feeding the cockpit demo over the public API.examples/client_mockupManual test client and radar simulator.mfd_editorVisual authoring tool for templates and pages.
The public headers in mfd_api/include/mfd are documented with Doxygen using:
@brief@param@return@note
This makes the API readable from the IDE in addition to the Markdown guides.
An inventory of external dependencies and the copied third-party license texts used by the current build is available in: