A multi-platform framework for fast and easy demo development.
The framework abstracts away all the boilerplate & OS specific code of allocating windows, creating the context, texture loading, shader compilation, render loop, animation ticks, benchmarking graph overlays etc. Thereby allowing the demo/benchmark developer to focus on writing the actual 'demo' code.
Therefore demos can be developed on PC or Android where the tool chain and debug facilities often allows for faster turnaround time and then compiled and deployed without code changes for other supported platforms.
The framework also allows for ‘real’ comparative benchmarks between the different OS and windowing systems, since the exact same demo/benchmark code run on all of them.
- Console. A freestyle project that runs in a console like environment.
- G2D (early access)
- OpenCL (early access)
- OpenCV (early access)
- OpenGL ES 2, 3, 3.1
- OpenVG
- OpenVX (early access)
- Vulkan (early access)
- Window. A freestyle project that runs in a window based environment.
- Android NDK
- Linux with various windowing systems (Yocto).
- Ubuntu 22.04
- Windows 10+
- Written in a limited subset of C++ 17 and uses RAII to manage resources.
- Uses a limited subset of STL to make it easier to port.
- No copyleft restrictions from GPL / L-GPL licenses.
- Allows for direct access to the expected API’s (EGL, OpenGL ES 2, OpenGL ES 3, OpenVG, OpenCV, etc)
- Package based architecture that ensures your app only has dependencies to the libs it uses.
- Content pipeline:
- Automatically compile Vulkan shaders during build.
- Services
- Keyboard, mouse and GamePad.
- Persistent data manager
- Assets management (models, textures)
- Defines a standard way for handling
- Init, shutdown & window resize.
- Program input arguments.
- Input events like keyboard, mouse and touch.
- Fixed time-step and variable time-step demo implementations.
- Logging functionality.
- Provides optional helper classes for commonly used tasks:
- Matrix, Vector3, GLShader, GLTexture, etc
- Easy access to optional libs like: GLM, GLI, RapidJSON and Assimp
See the setup guides for your platform:
For details about the build system see the FslBuildGen document.
While writing this we currently have forty-three OpenGL ES 2 samples, seventy-nine OpenGL ES 3.x samples, seventy-two Vulkan samples, eight OpenVG samples, two G2D samples, sixteen OpenCL samples, two OpenCV samples, three OpenVX sample and ten other samples. Which is 235 sample applications.
The demo framework currently runs on at least four platforms so using a traditional approach we would have to maintain 235 * 4 = 940 build files for the samples alone. Maintaining 940 or even just 235 build files would be an extremely time consuming and error prone process. So ideally, we wanted to use a build tool that supported
- Minimalistic build description files, that are used to ‘auto generate’ real build files.
- Proper package dependency support.
- A good developer experience
- co-existence of debug, release and other variants.
- re-use of already build libs for other samples.
- full source access to all used packages inside IDE’s.
- Support for Windows, Ubuntu, Yocto and Android NDK.
- Ensure a similar source layout for all samples.
The common go-to solution for C++ projects these days would be to use CMake, but when we started this project it was not nearly as widely used, the build files were not as minimalistic as we would like, the package dependencies were not handled as easy as we would have liked, it had no Android NDK support and the sample layout would have to be manually enforced.
As no existing system really fit, we made the controversial choice of creating our own minimalistic system. It uses a minimalistic build-meta-data file with no scripting capabilities (as we want everything to be build the same way). From the meta data files + the content of the folders ‘include’ and ‘source’ we then generate the platform dependent build files using various templates that can be changed in one location. This means that once the ‘build-meta-data’ file has been created it would basically never have to change unless the source code gets new dependencies to other packages.
Over the years, we been using this approach we have not really touched any of the build-meta-data files (Fsl.gen) since they were initially created. If we ever needed any build file changes we could instead update the template in one location and have it affect all samples right away.
Here is an example ‘Fsl.gen’ build-meta-data file for the GLES2.Blur sample app:
<?xml version="1.0" encoding="UTF-8"?>
<FslBuildGen xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../FslBuildGen.xsd">
<Executable Name="GLES2.Blur" NoInclude="true">
<ImportTemplate Name="DemoAppGLES2"/>
<Dependency Name="EnvironmentMappingShared"/>
</Executable>
</FslBuildGen>
It basically specifies that this directory contains an executable package with no include directory, that it uses the ‘DemoAppGLES2’ template and has a dependency on a package called ‘EnvironmentMappingShared’.
Another example is the ‘Fsl.gen’ file for the FslGraphics package which has had lots of files added over the years, but its build file has been untouched.
<?xml version="1.0" encoding="UTF-8"?>
<FslBuildGen xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../FslBuildGen.xsd">
<Library Name="FslGraphics">
<Dependency Name="FslBase"/>
</Library>
</FslBuildGen>
It specifies a ‘library’ (static library), with a dependency to ‘FslBase’ and a windows visual studio project id.
While CMake has improved a lot since we initially looked, it would requires more manual work to keep the samples up to date than our current solution.
It's worth mentioning that its entirely possible generate 'CMakeLists.txt' with this system, in fact we do just that internally for the android gradle+cmake build.
Operating System | Build system |
---|---|
Android | gradle + cmake (Android Studio can be used with the generated projects) |
Ubuntu | cmake (ninja) |
Windows | cmake (Visual studio 2022 x64) |
Yocto | cmake (ninja) |
Is a cross-platform build-file generator. Which main purpose is to keep all build files consistent, in sync and up to date. See the FslBuildGen document for details.
Extends the technology behind FslBuildGen with additional knowledge about how to execute the build system for a given platform. So basically, FslBuild works like this
- Invoke the build-file generator that updates all build files if necessary.
- Filter the builds request based on the provided feature and extension list.
- Build all necessary build files in the correct order.
FslBuild comes with a few useful arguments
Argument | Description |
---|---|
--ListRequirements | List the requirement tree and exit. |
--ListVariants | List all variants. |
--RequireFeatures | The list of features that are required for a executable package to be build. For example [OpenGLES2] to build all executables that use OpenGLES2. |
--UseFeatures | Allows you to limit what’s build based on a provided feature list. For example [EGL,OpenGLES2]. This parameter defaults to all features. |
--UseExtensions | The list of available extensions to build for. For example [OpenGLES3.1:EXT_geometry_shader,OpenGLES3.1:EXT_tessellation_shader] to allow the OpenGLES3.1 extensions EXT_geometry_shader and EXT_tessellation_shader. You can also specify * for all extensions (default). |
--Variants | Define the variants you wish to build (if any). For yocto for example you select the window system and build type using --Variants [config=Debug,WindowSystem=FB] |
--BuildTime | Time the build and print the result and the end. |
-t 'sdk' | Build all demo framework projects |
-v | Set verbosity level |
-- | arguments written after this is send directly to the native build system. |
- Don’t modify the auto-generated files. The FslBuild scripts are responsible for creating all the build files for a platform and verifying dependencies. Since all build files are auto generated you can never modify them directly as the next build will overwrite your changes. Instead add your changes to the ‘Fsl.gen’ files as they control the build file generation!
- The ‘Fsl.gen’ file is the real build file.
- All include and source files in the respective folders are automatically added to the build files.
This only runs the content builder part of the build process.
FslBuildContent.py
Build the current directories package content.
Generate a new project of the specified type. This is basically a project wizard that will prepare a new project directory with a basic template for the chosen project type.
FslBuildNew.py GLES2 FlyingPigsApp
Create the FlyingPigsApp sample app directory using the GLES2 template.
Package build environment checker. Based on what features the package uses this will try to detect setup errors. It also has the capability to scan the source for common mistakes and it can check if the proper License.json file is provided for screenshots.
FslBuildCheck.py
Check the current build environment to see if the package can be build.
FslBuildCheck.py --scan
Scan the current package and see if there is any common mistakes with for example include guards, tabs, etc.
A new *work in progress tool that helps keep the README.md files similar and that fills out various areas of the root README.md file.
FslBuildDoc.py
The following description of the demo application details uses a GLES2 demo named ‘S01_SimpleTriangle’ as example. It lists the default methods that a demo should implement, the way it can provide customized parameters to the windowing system and how asset management is made platform agnostic.
This is a list of the methods that every Demo App is most likely to override .
// Init
S01_SimpleTriangle(const DemoAppConfig& config)
// Shutdown
~S01_SimpleTriangle()
// OPTIONAL: Custom resize logic (if the app requested it). The default logic is to
// restart the app.
void ConfigurationChanged(const DemoWindowMetrics& windowMetrics)
// OPTIONAL: Fixed time step update method that will be called the set number of times
// per second. The fixed time step update is often used for physics.
void FixedUpdate(const DemoTime& demoTime)
// OPTIONAL: Variable time step update method.
void Update(const DemoTime& demoTime)
// Put the rendering calls here
void Draw(const FrameInfo& frameInfo)
When the constructor is invoked, the Demo Host API will already be setup and ready for use, the demo framework will use EGL to configure things as requested by your EGL config and API version.
It is recommended that you do all your setup in the constructor.
This also means that you should never try to shutdown EGL in the destructor since the framework will do it at the appropriate time. The destructor should only worry about resources that your demo app actually allocated by itself.
The ConfigurationChanged method will be called if the screen metrics changes.
Is a fixed time-step update method that will be called the set number of times per second. The fixed time step update is often used for physics.
Will be called once before every draw call and you will normally update your animation using delta time. For example if you need to move your object 10 units horizontally per second you would do something like
_positionX += 10 * demoTime.DeltaTime;
Should be used to render graphics.
Depending on what your demo is doing, you might use one or the other - or both. It’s actually a very complex topic once you start to dig into it, but in general anything that need precision and predictable/repeatable calculations, like for example physics, often benefits from using fixed time steps. It really depends on your algorithm and it’s recommended to do a couple of google searches on fixed vs variable, since there are lots of arguments for both. It’s also worth noting that game engines like Unity3D support both methods.
The methods will be called in this order
- Events (if any occurred)
- ConfigurationChanged
- FixedUpdate (0-N calls. The first frame will always have a FixedUpdate call)
- Update
- Draw After the draw call, a swap will occur.
The framework supports loading files from the Content folder on all platforms.
Given a content folder like this:
Content/Texture1.bmp
Content/Stuff/Readme.txt
You can load the files via the IContentManager service that can be accessed by calling
std::shared_ptr<IContentManager> contentManager = GetContentManager();
You can then load files like this:
// *** Text file ***
// Read it directly into a new string
const std::string content = contentManager->ReadAllText("Stuff/Readme.txt");
// *** Binary file ***
// Read the content directly into a new vector
const std::vector<uint8_t> content = contentManager->ReadBytes("MyData.bin");
// Read the content into a existing vector
std::vector<uint8_t> content;
contentManager->ReadAllBytes(content, "MyData.bin");
// *** Bitmap file ***
// Read the content directly into a new bitmap
const Bitmap bitmap = contentManager->ReadBitmap("Texture1.bmp", PixelFormat::R8G8B8_UINT);
// Read the content into a existing bitmap object.
// Beware the bitmap object will be resized and format changed as needed, but some memory could be reused.
Bitmap bitmap;
contentManager->Read(bitmap, "Texture1.bmp", PixelFormat::R8G8B8_UINT);
// *** Texture file ***
// Read the content directly into a new texture
const Texture texture = contentManager->ReadTexture("Texture1.bmp", PixelFormat::R8G8B8_UINT);
// Read the content directly into a existing texture object.
// Beware the texture object will be resized and format changed as needed, but some memory could be reused.
Texture texture;
contentManager->Read(texture, "Texture1.bmp", PixelFormat::R8G8B8_UINT);
If you prefer to control the loading yourself, you can retrieve the path to the files like this:
IO::Path contentPath = contentManager->GetContentPath();
IO::Path myData = IO::Path::Combine(contentPath, "MyData.bin");
IO::Path readmePath = IO::Path::Combine(contentPath, "Stuff/Readme.txt");
IO::Path texture1Path = IO::Path::Combine(contentPath, "Texture1.bmp");
You can then open the files with any method you prefer. Both methods work for all supported platforms.
This is done in the S01_SimpleTriangle_Register.cpp file.
namespace Fsl
{
namespace
{
// Custom EGL config (these will per default overwrite the custom settings. However an exact EGL config can be used)
static const EGLint g_eglConfigAttribs[] =
{
EGL_SAMPLES, 0,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 0, // buffers with the smallest alpha component size are preferred
EGL_DEPTH_SIZE, 24,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE,
};
}
// Configure the demo environment to run this demo app in a OpenGLES2 host environment
void ConfigureDemoAppEnvironment(HostDemoAppSetup& rSetup)
{
DemoAppHostConfigEGL config(g_eglConfigAttribs);
DemoAppRegister::GLES2::Register<S01_SimpleTriangle>(rSetup, "GLES2.S01_SimpleTriangle", config);
}
}
Since the demo framework is controlling the main method, you need to register your application with the Demo Host specific registration call (in this case the OpenGL ES2 host), for the framework to register your demo class.
To register a demo for OpenGLES 3.X you would use the GLES3 register method:
DemoAppRegister::GLES3::Register<S01_SimpleTriangle>(rSetup, "GLES3.S01_SimpleTriangle", config);
Per default the app is destroyed and recreated when a resolution change occurs. It is left up to the DemoApp to save and restore demo specific state.
The demo app can request an exit to occur, or it can be terminated via an external request. In both cases one of the following things occur.
- If the app has been constructed and has received a FixedUpdate, then it will finish its FixedUpdate, Update, Draw, swap sequence before its shutdown.
- If the app requests a shutdown during construction, the app will be destroyed before calling any other method on the object (and no swap will occur).
The app can request an exit to occur by calling:
GetDemoAppControl()->RequestExit(1);
All demos support various command line arguments. Use –h on a demo for a complete list
Argument | Description |
---|---|
-h | Show the command line argument help. |
--Stats | Show a performance graph. |
--LogStats | Log various stats to the console. |
--Window | Run inside a window instead of using the fullscreen. Used like this --Window [0,0,640,480] the parameters specify (x,y,width,height). |
--ScreenshotFrequency | Create a screenshot at the given frame frequency. |
--ExitAfterFrame | Exit after the given number of frames has been rendered |
--ContentMonitor | Monitor the Content directory for changes and restart the app on changes. WARNING: Might not work on all platforms and it might impact app performance (experimental) |
All apps support these keys per default, but can override then if they chose to do so. Beware that some platforms might not support the given 'key' type and therefore the functonality is unsupported.
Key | Function |
---|---|
Escape | Exit the app. |
F4 | Take a screenshot (If supported by the test service) |
F5 | Restart the app. |
All samples support time stepping which can be useful for debugging. It might not be available on platforms that don't support the given keys. Also beware that apps can override these keys if they chose to do so.
Key | Function |
---|---|
Pause | Pause the sample. |
PageDown | Move forward one timestep. |
Delete | Toggle between normal and Slow 2x playback |
End | Toggle between normal and Slow 4x playback |
Insert | Toggle between normal and fast 2x playback. |
Home | Toggle between normal and fast 4x playback. |
See Console applications
See Console.System applications
See FslResearch applications
See G2D applications
See GLES2 applications
See GLES2.System applications
See GLES2.UI applications
See GLES3 applications
See GLES3.System applications
See GLES3.UI applications
See OpenCL applications
See OpenCV applications
See OpenVG applications
See OpenVG.System applications
See OpenVX applications
See Vulkan applications
See Vulkan.System applications
See Vulkan.UI applications
See Window applications
See Window.System applications