NAP
System

Overview

NAP enables you to connect and exchange data between various types of external hardware in a generic fashion. The system is designed to make it easy to re-use specific parts or components for future projects and keep app specific code local to your project. The underlying system provides you with all the handles to get up ad running in no time. But it's important to understand what parts contribute to the overall system architecture. Below you see a dumbed-down schematic of an application built with NAP. This schematic shows some of the key components of the NAP system architecture:

Let's start reading the graph top to bottom, left to right. At the top we see app.json. This file contains project specific settings and is loaded by both your application and Napkin on initialization. Every app.json points to an object.json file, which defines the content of your application. You use Napkin to edit this content - your application loads this content.

The application runner runs My NAP App until you tell it to quit. Your NAP App gives you high level programmatic control over what:

  • To render
  • To forward
  • To update

Core is the heart of every NAP application and manages (among other things) modules. Core is also the gateway to the ResourceManager. Every NAP application requires a Core object. That's the reason you explicitly create one and give it to the AppRunner that runs your application. When creating Core you also create a ResourceManager. The resource manager does a lot of things but most importantly: it makes your life easy. It creates all the objects that are associated with your application, initializes them in the right order and keeps track of any content changes. When a change is detected, the resource manager automatically patches the system without having to re-compile your application. The initialization call of your application is the perfect place to load the file and check for content errors. ]() Modules are libraries that expose building blocks. You can use these building blocks to construct your application. Most modules expose specific building blocks, for example. The MIDI module exposes MIDI receiving and sending objects, a generic interface to create and extract MIDI events and a service that deals with the MIDI library. Core loads all available modules automatically and initializes them in the right order. After a module is loaded all the building blocks are registered and the module can be initialized. You, as a user, don't have to do anything.

The diagram has four resources from three different modules:

After initializing core (and therefore all modules) the building blocks can be created by the resource manager. We add the building blocks as individual resources to our JSON file and tell the resource manager to load the file and voila:

  • You have a midi port that is open and listening for midi messages
  • Two windows are visible on screen
  • The serial port is ready to communicate with your microcontroller

You might notice that working this way saves you from typing many lines of code. You don't have to declare objects in C++ or have to worry about the right order of initialization. You can directly access the resources and start building what you had in mind.

Modules & Services

Following the modular design of NAP: all functionality is split into modules. Each module contains specific blocks of functionality that can be used as a:

The specifics of these objects are discussed in separate sections. Every module gets compiled into a dynamically linkable library (DLL). NAP loads all available modules automatically when your application starts. Each module has the option to expose a service. A service is a rather abstract concept and can be used in many different ways. Let's look at an example to understand what a service does. The render service manages, among other things, the following:

  • It initializes the render system and terminates it on exit.
  • It processes system events such as resizing a window.
  • It provides a high-level render interface for all compatible resources, components and entities.

In a more abstract sense: a service can be used to perform system-wide operations such as initializing a library or keeping track of specific resources. A service receives update(), init() and shutdown() calls. Update is called every frame, init is called before the application is initialized and shutdown is called directly after stopping the app.

It is possible that a service wants to use functionality from other services. NAP takes care of the correct order of initialization if you tell the system what other services your module depends on by implementing the getDependentServices() call. This function returns a list of services your module depends on with as a result that init, update and shutdown are called in the right order.

Apps

The main entrypoint for running an application is the AppRunner. This objects requires two objects: an application to run and an event handler. The event handler forwards system events to the application. These events include mouse and keyboard input. Every application needs to be derived from BaseApp and every event handler needs to be derived from AppEventHandler.

The easiest way to set up a new project is to:

  • Derive a new applicaton from App
  • Use the default GUIAppEventHandler to pass input events to your application
  • Give both of them to the AppRunner and start the loop
// Main loop
int main(int argc, char *argv[])
{
// Create core
nap::Core core;
// Create app runner
// Start running: This will initialize the engine, register all the modules and start the application loop
if (!app_runner.start(error))
{
nap::Logger::fatal("error: %s", error.toString().c_str());
return -1;
}
// Return if the app ran successfully
return app_runner.exitCode();
}

Core

Most of the building blocks of NAP are grouped into modules with one exception: the functionality that is present in the core library. Core is a shared library, which is always loaded and can be used everywhere. Every application must have exactly one Core instance. The Core object manages all services and is the main gateway to all the available resources. These resources are kept and maintained by the resource manager.

This example from the helloworld demo shows how to:

  • Retrieve initialized services
  • Extract loaded content, including: the render window, camera, mesh etc.
bool HelloWorldApp::init(utility::ErrorState& error)
{
// Retrieve services
mRenderService = getCore().getService<nap::RenderService>();
mSceneService = getCore().getService<nap::SceneService>();
mInputService = getCore().getService<nap::InputService>();
mGuiService = getCore().getService<nap::IMGuiService>();
// Extract loaded resources
mRenderWindow = mResourceManager->findObject<nap::RenderWindow>("Window0");
// Find the world and camera entities
ObjectPtr<Scene> scene = mResourceManager->findObject<Scene>("Scene");
mWorldEntity = scene->findEntity("World");
mCameraEntity = scene->findEntity("Camera");
// Find the mesh
mWorldMesh = mResourceManager->findObject("WorldMesh");
return true;
}

The Resource Manager

This object is responsible for loading the JSON file that contains all the resources that are necessary for your application to run. On startup, the nap::AppRunner calls nap::ResourceManager::loadFile for you, together with the data file linked to by the project configuration. All the objects declared inside that file are created and initialized by the resource manager. We call these objects 'resources'. Every loaded resource is owned by the resource manager. This means that the lifetime of a resource is fully managed by the resource manager and not by the client (ie: you).

Every resource has an identifier. In the example above we use various identifiers to find specific resources in the application after load.

Every resource is derived from Resource. Every resource carries an identifier that the resource manager uses to identify an object. The most important task of a resource is to tell the resource manager if initialization succeeded using the init function. A good example of a resource is the Image. On initialization the image will try to load a picture from disk and store the result internally. Initialization will fail if the picture doesn't exist or isn't supported. If that's the case the resource manager will halt execution, return an error message and as a result stop further execution of your program. This is the point where NAP tries to validate data for you.

The resource manager and init structure are further explained in the resource section.

Events

NAP uses events to signal the occurrence of an action to the running application. Events often originate from an external environment and are handled by their respective services. When the event is generated asynchronously the service makes sure it is consumed before making it available to potential listeners (often components) on the main thread. This is the general design pattern behind event handling in NAP. Input, OSC and Midi events are handled this way. This also ensures that new messages don't stall the running application.

Configuration

Some services are configurable, including the audio, render and gui service. Every service that is configurable is initialized using a class derived from ServiceConfiguration. The render service is initialized using a render service configuration and the audio service is initialized using an audio service configuration.

All service configurable settings are stored in a config.json file, which should be placed next to the executable. This file is not placed in the data folder because service configurable settings are system specific, not application specific. You might want to select a different audio output port, change the gui font size or disable high dpi rendering. If no config.json file is provided the system defaults are used.

Use Napkin to generate and edit service config files.

config.json example:

{
"Objects":
[
{
"Type": "nap::IMGuiServiceConfiguration",
"mID": "nap::IMGuiServiceConfiguration",
"FontSize": 17.0
},
{
"Type": "nap::RenderServiceConfiguration",
"mID": "nap::RenderServiceConfiguration",
"PreferredGPU": "Discrete",
"Layers":
[
"VK_LAYER_KHRONOS_validation"
],
"Extensions": [],
"VulkanMajor": 1,
"VulkanMinor": 0,
"EnableHighDPI": true,
"ShowLayers": false,
"ShowExtensions": true,
"AnisotropicSamples": 8
},
{
"Type": "nap::audio::AudioServiceConfiguration",
"mID": "nap::audio::AudioServiceConfiguration",
"InputChannelCount": 1,
"OutputChannelCount": 2,
"AllowChannelCountFailure": true,
"SampleRate": 44100.0,
"BufferSize": 1024,
"InternalBufferSize": 1024
}
]
}
nap::RenderWindow
Definition: renderwindow.h:43
nap::icon::error
constexpr const char * error
Definition: imguiservice.h:55
nap::utility::ErrorState
Definition: errorstate.h:19
nap::shader::main
constexpr const char * main
Definition: renderglobals.h:29
nap::InputService
Definition: inputservice.h:32
nap::RenderService
Definition: renderservice.h:275
nap::Core
Definition: core.h:82
nap::IMGuiService
Definition: imguiservice.h:165
nap::AppRunner
Definition: apprunner.h:33
nap::SceneService
Definition: sceneservice.h:16