Skip to content

benoitfragit/MFDStudio

Repository files navigation

MFD

MFD is a C++/CMake toolkit for building 2D multi-function display windows.

It combines:

  • raylib
  • ImGui
  • rlImGui
  • EnTT
  • nlohmann_json
  • Protocol Buffers over UDP for runtime control

The project is built around one simple workflow:

  1. describe a window, its pages, and reusable reticles in JSON
  2. render the active page in 2D
  3. drive the window locally or from an external client
  4. receive strobe feedback and framebuffer readback when needed

Automated runtime and JSON-loading checks are also built with the project through GoogleTest.

Start Here

If you are new to the project, use this order:

  1. Quick Start
  2. Core Concepts
  3. JSON Reference
  4. Tutorial Index

If you already know what you want:

What You Can Do

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 RGBA32 CPU buffer
  • project physical or user-space coordinates on the client without changing the window runtime

High-Level Architecture

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]
Loading

Read this as:

  • JSON files define the authoring model
  • SceneRegistry holds 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

Threading Model

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.

Supported Primitive Types

The reticle model supports:

  • text
  • time
  • line
  • circle
  • rectangle
  • ellipse
  • square
  • diamond
  • triangle
  • polyline
  • bezier

Notes:

  • type: "heure" is accepted as an alias for time
  • time is a text-like primitive that formats the current clock value
  • rectangle and ellipse are dedicated primitives, separate from square

Coordinate System

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 page
  • x > 0 goes right
  • y > 0 goes up
  • reticles are authored independently from the window pixel size

This means a page remains valid when the window is resized.

Blink Model

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 slow to fast immediately re-attaches it to the new synchronized group

Project Layout

  • mfd_api Public API library and runtime core.
  • mfd_window Generic top-level window host loading any window JSON from the command line.
  • examples/mfd_demo Preset launcher for assets/windows/demo_pages.json.
  • examples/mfd_demo_minimal Preset launcher for assets/windows/demo_pages_minimal.json.
  • examples/mfd_demo_cockpit Preset launcher for assets/windows/demo_pages_cockpit.json.
  • examples/client_mockup_minimal Headless cockpit client showing the public UDP API from one plain main loop.
  • examples/client_mockup GUI client used to send commands and inspect strobe feedback.
  • mfd_editor Visual editor for pages and reticles.
  • mfd_api/tests GoogleTest-based automated tests for JSON loading and runtime rules.
  • assets/windows Root window JSON files.
  • assets/pages Page JSON files.
  • assets/reticles Reusable reticle templates.
  • docs Quick start, concept pages, and tutorials.

Build

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-win32

Dependencies are fetched by CMake.

Automated Tests

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 JsonLoader and SceneRegistry

Commands:

cmake --preset vs2022-win32
cmake --build --preset debug-win32

.\build\vs2022-win32\mfd_api\tests\Debug\mfd_api_tests.exe

There is also a convenience target:

cmake --build --preset debug-win32 --target mfd_api_tests_run

JSON Model

For the exact field reference, use:

Window JSON

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 defaultPage field 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"
  ]
}

Page 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"
        }
      ]
    }
  ]
}

Reticle JSON

Reusable templates live in assets/reticles.

The project ships a working example using rectangle, ellipse, text, and time in assets/reticles/status_clock.json.

Public API Entry Points

Load a window

#include "mfd/io/JsonLoader.h"

mfd::JsonLoader loader;
const mfd::LoadedWindowConfiguration loaded =
    loader.LoadWindowConfiguration("assets/windows/demo_pages.json");

Drive a scene locally

#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);

Drive a window remotely

#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);

Host a window with background UDP I/O

#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()));

Send dynamic reticles in bulk

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.

Project client-side user space to page space

#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

Strobe

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::CommandClient
  • mfd::CreateFeedbackReceiverChannel(...)
  • mfd::DeserializeStrobeStatusFeedback(...)

See the full workflow in docs/tutorials/06_strobe_control_and_feedback.md.

Framebuffer Capture

The renderer side exposes mfd::OpenGlFramebufferReader.

#include "mfd/render/OpenGlFramebufferReader.h"

mfd::Rgba32Framebuffer capture = mfd::OpenGlFramebufferReader::ReadRgba32();

The returned buffer contains:

  • width
  • height
  • typed Rgba8Pixel pixels
  • 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.

Tools

  • examples/mfd_demo Thin preset launcher built on top of mfd_window.
  • examples/mfd_demo_cockpit Thin cockpit preset launcher built on top of mfd_window.
  • examples/mfd_demo_minimal Thin minimal preset launcher built on top of mfd_window.
  • mfd_window Generic runtime launcher accepting --window <json>.
  • examples/client_mockup_minimal Minimal headless client feeding the cockpit demo over the public API.
  • examples/client_mockup Manual test client and radar simulator.
  • mfd_editor Visual authoring tool for templates and pages.

Documentation Style

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.

Third-Party Licenses

An inventory of external dependencies and the copied third-party license texts used by the current build is available in:

About

A Multi-Functional Display studio

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors