Application development

There are several ways to add or customize your Kanzi application behavior and functionality:

  • Using configuration you can set the most common startup and runtime parameters for your application. For example, you can configure the application resolution, the kzb files that the application uses, and so on. See Configuring your application.

  • Use code behind to add behavior to UI elements in your application. Code behind is the easiest way to add functionality without leaving Kanzi Studio and without creating solutions or project files. You can observe the behavior that is implemented in code behind immediately in the Preview. See Programming Activities with Code Behind.

  • Derive a C++ class from the Application class which is the base class for all Kanzi applications. Override the various virtual functions of the Application class to react to different points in the lifecycle of the application, such as initialization and uninitialization. For example, when the application starts, when the application loads its UI content, and so on. See Reacting to application lifecycle events.

  • Use the MainLoopScheduler class to which the Application class delegates processing of input, ticking of animations, laying out and rendering content, ending with update of the screen through repeated control flow. You can add tasks that execute in the main loop of the application to add recurring or one-time functionality relative to the stages of the main loop of the application. See Modifying the main loop logic.

  • Develop Kanzi Engine plugins which can implement functionality that you can reuse between applications.

Configuring your application

Using configuration you can set the most common startup and runtime parameters for your application. For example, you can configure the application resolution, the kzb files that the application uses, and so on.

You can configure your Kanzi application:

  • In the C++ application override the Application::onConfigure function of your application class. Kanzi calls this function as part of application initialization before it reads the application.cfg and before it initializes the graphics subsystem. Use this function to configure application properties.

  • In application.cfg by setting the parameters for Kanzi Studio projects without recompiling your application or even without a C++ application.

The configuration you specify in application.cfg overrides the configuration you specify in Application::onConfigure.

For example, to set in application.cfg which kzb file to load

# Loads the kzb file named my_application.kzb.
BinaryName = "my_application.kzb"

To set the kzb file to load in Application::onConfigure

// Loads the kzb file named my_application.kzb.
configuration.binaryName = "my_application.kzb";

For a list of all the configuration settings you can use for your Kanzi application, see Application configuration reference.

Reacting to application lifecycle events

Adding startup logic

You can define the startup logic of your application in these functions:

  • Kanzi calls the Application::onStartup function once, immediately after starting the application, but before it loads the initial UI contents. Here you can add application start-up logic that requires modifying already initialized Kanzi objects.

  • Kanzi calls the Application::onProjectLoaded function once, immediately after loading the initial UI contents.

    Usually you insert here initialization code that depends on the content that has already been loaded. For example, you can attach initial message handlers or listeners, populate the UI with data, and so on.

    For example, this code attaches a handler to a Button after Kanzi loads the node tree:

    void onProjectLoaded() override
    {
        // Get reference to the Screen node.
        ScreenSharedPtr screen = getScreen();
        kzAssert(screen);
    
        // Look up reference to a button by alias.
        Button2DSharedPtr button = screen->lookupNode<Button2D>("#Button");
        kzAssert(button);
    
        // Attach a handler to the button.
        button->addMessageHandler(Button2D::ClickedMessage, [this](auto& arguments) { this->buttonHandler(arguments); });
    }
    

This diagram shows the callbacks for startup and shutdown logic.

../../_images/reacting-to-lifecycle-events.svg

Observing application state

You can observe the application state to find out whether an application is running, minimized, or suspended.

A Kanzi application is always in one of these states:

  • MainLoopState::Running state. This is the normal mode of operation of a Kanzi application. This is the initial state of an application.

  • MainLoopState::Paused state. The platform can impose rules on application behavior when it is out of focus or minimized. To mark such state, a platform backend can set the application to the MainLoopState::Paused state.

    In this state, Kanzi stops normal input handling and rendering, and waits to be put back to the MainLoopState::Running state. Additionally, it is typical for applications to halt heavy work when in this state.

  • MainLoopState::Quitting state. This state indicates that either application or the platform requested the application to quit by calling MainLoopScheduler::quit. In this state, applications must avoid starting work, unless that work is related to uninitialization.

This diagram shows the states of a Kanzi application.

../../_images/states.svg

To find out the application state, call MainLoopScheduler::getState.

For example, to prevent adding a task when an application is quitting:

void doSomethingRepeatedly()
{
    doSomething();

    // If a condition is met...
    if (someCondition() &&
        // ...and the application is not quitting...
        getState() != MainLoopState::Quitting)
    {
        // ...add another task to the task dispatcher.
        getDomain()->getTaskDispatcher()->submit([this]() { this->doSomethingRepeatedly(); });
    }
}

Reacting to application state changes

Use these functions to react to application state changes:

Function

Description

Application::onSuspend

Kanzi calls this function in a frame where no rendering was performed, to determine whether to suspend the application and for what duration. The default implementation calculates appropriate timeout based on active animations, timers, and resources waiting for deployment.

Use this function to customize application suspension.

Application::onPause

Kanzi calls this function when the application main loop enters the MainLoopState::Paused state.

Application::onResume

Kanzi calls this function when the application main loop returns from the MainLoopState::Paused to the MainLoopState::Running state.

Application::onShutdown

Kanzi calls this function immediately before the application uninitialization.

Modifying the main loop logic

The main loop consists of a sequence of stages, where each stage consists of a sequence of tasks. A task is any callable item, including functions, function objects, and lambdas.

using Task = function<void(chrono::nanoseconds)>;
public interface Task {
    void handle(Duration lastFrameDuration);
}
interface Task {
    fun handle(lastFrameDuration: Duration?)
}

When Kanzi executes a task, it passes the last frame duration as an argument to the task.

These are the default stages in Kanzi:

  • In the input stage Kanzi handles the input events.

  • In the user stage Kanzi handles update logic: deploys resources, loads kzb files and prefabs, and executes your task dispatcher and timer tasks.

  • In the animate stage Kanzi ticks the active animations.

  • In the layout stage Kanzi performs layout the node tree.

  • In the render stage Kanzi renders the node tree.

  • In the present stage Kanzi presents the node tree.

This diagram shows the control flow of the Kanzi main loop.

../../_images/main-loop.svg

In an application you can remove, replace, or insert tasks in any main loop scheduler stage. See MainLoopScheduler.

Adding recurring tasks

When you want Kanzi to execute a task every frame, you can add it to any stage of the main loop using MainLoopScheduler::appendTask and MainLoopScheduler::prependTask, with option MainLoopScheduler::TaskRecurrence::Recurring.

For example, you can use a recurring task to call the update function of an external library:

void onStartup() override
{
    // From the main loop Animate stage, tick an external physics engine.
    getMainLoopScheduler().appendTask(AnimateStage,
                                      kzMakeFixedString("TickPhysics"),
                                      MainLoopScheduler::TaskRecurrence::Recurring,
                                      [this](auto deltaTime) { this->getPhysicsEngine()->tick(deltaTime); });
}
void onStartup()
{
    // From the main loop Animate stage, tick an external physics engine.
    MainLoopScheduler mls = getDomain().getMainLoopScheduler();
    mls.appendTask(MainLoopScheduler.AnimateStage, "TickPhysics", TaskRecurrence.Recurring,
        new MainLoopScheduler.Task() {
            @Override
            public void handle(Duration lastFrameDuration)
            {
                getPhysicsEngine().tick(lastFrameDuration);
            }
        });
}
fun onStartup() {
    // From the main loop Animate stage, tick an external physics engine.
    domain.mainLoopScheduler.appendTask(
        MainLoopScheduler.AnimateStage, "TickPhysics", TaskRecurrence.Recurring
    ) { lastFrameDuration -> getPhysicsEngine().tick(lastFrameDuration) }
}

Adding one-time tasks

When you want Kanzi to execute a task only once, you can add it to any stage of the main loop using MainLoopScheduler::appendTask and MainLoopScheduler::prependTask, with recurrence option MainLoopScheduler::TaskRecurrence::OneTime.

For example, to capture a screen shot of the output when the user presses the Enter key:

void onKeyInputEvent(const KeyEvent& keyEvent) override
{
    // First invoke the base class implementation.
    Application::onKeyInputEvent(keyEvent);

    // If the user presses the Enter key:
    if (keyEvent.getKeyUp() == LogicalKey::Enter)
    {
        // Force re-rendering.
        getScreen()->invalidateDraw();

        // Add a post-rendering task to capture the screen output.
        getMainLoopScheduler().appendTask(RenderStage,
                                          kzMakeFixedString("CaptureScreenshot"),
                                          MainLoopScheduler::TaskRecurrence::OneTime,
                                          [this](auto) { this->captureScreen(); });
    }
}
@Override
public boolean onKeyUp(int keyCode)
{
    boolean handled = super.onKeyUp(keyCode);

    if (keyCode == KeyEnter)
    {
        // Force re-rendering.
        getScreen().invalidateDraw();

        // Add a post-rendering task to capture the screen output.
        MainLoopScheduler mls = getDomain().getMainLoopScheduler();
        mls.appendTask(MainLoopScheduler.RenderStage, "CaptureScreenshot",
            TaskRecurrence.OneTime, new MainLoopScheduler.Task() {
                @Override
                public void handle(Duration lastFrameDuration)
                {
                    captureScreen();
                }
            });
    }

    return handled;
}
override fun onKeyUp(keyCode: Int): Boolean {
    val handled = super.onKeyUp(keyCode)
    if (keyCode == KeyEnter) {
        // Force re-rendering.
        screen.invalidateDraw()

        // Add a post-rendering task to capture the screen output.
        domain.mainLoopScheduler.appendTask(
            MainLoopScheduler.RenderStage, "CaptureScreenshot",
            TaskRecurrence.OneTime
        ) { captureScreen() }
    }
    return handled
}

Overriding tasks

When you want to modify the default approach that Kanzi uses to process input, performs layout and renders content, update the screen, and perform other repeating tasks, you can replace existing tasks using MainLoopScheduler::replaceTask.

For example, you can override the default rendering to apply a custom post-processing effect in the code. You can achieve this by first rendering the contents to an off-screen texture with the default rendering implementation, and then rendering that texture to the screen with a post-processing shader:

// An off-screen texture to hold the rendering result before applying the post-processing effect.
TextureSharedPtr m_offscreenTexture;

// A material that applies a full-screen post-processing effect in the fragment shader.
MaterialSharedPtr m_postProcessMaterial;

void onProjectLoaded() override
{
    // Create a framebuffer for off-screen rendering.
    m_offscreenTexture = createOffscreenTexture();

    // Acquire a material for applying a post-processing effect.
    m_postProcessMaterial = createPostProcessMaterial();

    // Get the token of the default rendering task from the render stage.
    auto& scheduler = getMainLoopScheduler();
    auto tasks = scheduler.getTaskInfo(RenderStage);
    auto itTask = find_if(cbegin(tasks), cend(tasks), [](const auto& taskInfo) { return taskInfo.name == "Render"; });
    kzAssert(itTask != cend(tasks));
    auto token = itTask->token;

    // Replace the default rendering task with a custom implementation.
    scheduler.replaceTask(RenderStage,
                          token,
                          [this](auto)
                          {
                              auto renderer = this->getRenderer3D();
                              auto coreRenderer = renderer->getCoreRenderer();

                              // Set the offscreen texture as the rendering target.
                              this->setRootCompositionTarget(this->m_offscreenTexture);

                              // Render to the off-screen texture using the default rendering logic.
                              this->render();

                              // Revert back to rendering to the screen.
                              this->setRootCompositionTarget(nullptr);
                              coreRenderer->resetActiveFramebuffer();

                              // Render the texture to the screen with the post-processing effect applied.
                              this->renderPostProcessed();

                              // Mark the current frame as rendered.
                              this->getMainLoopScheduler().setCurrentFrameRendered();
                          });
}

TextureSharedPtr createOffscreenTexture()
{
    // Get the off-screen texture size from the window size.
    // This assumes that the window size does not change during runtime.
    auto graphicsOutput = getGraphicsOutput();
    auto width = graphicsOutput->getWidth();
    auto height = graphicsOutput->getHeight();

    // Create a render target with color and depth attachments.
    auto createInfo = Texture::CreateInfoNode2DRenderTarget(width, height, GraphicsFormatR8G8B8A8_UNORM);
    createInfo.depthStencilFormat = GraphicsFormatD16_UNORM;
    auto texture = Texture::create(getDomain(), createInfo, "Offscreen Texture");
    return texture;
}

MaterialSharedPtr createPostProcessMaterial()
{
    // Acquire a custom post-processing effect.
    // Note that the default textured debug material copies the texture to the screen without any effect.
    return Material::acquireDebugMaterialTextured(*getResourceManager());
}

void renderPostProcessed()
{
    auto renderer = getRenderer3D();
    auto graphicsOutput = getGraphicsOutput();

    // Set the off-screen texture and other shader inputs for the post-processing material.
    m_postProcessMaterial->setTexture(StandardMaterial::TextureProperty, m_offscreenTexture);

    // Render the texture as a full-screen quad.
    auto windowSize = Vector2(static_cast<float>(graphicsOutput->getWidth()), static_cast<float>(graphicsOutput->getHeight()));
    renderer->drawViewportQuadWithTextureSpan(Vector2(0.0f, 0.0f), windowSize, *m_postProcessMaterial, Vector2(1.0f, 1.0f), ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f));
}

As an alternative to individually replacing tasks, you can redefine the whole main loop, by overriding Application::initializeMainLoopTasks. Kanzi attaches the default main loop tasks in this function. To allow copying, its implementation is intentionally exposed in the header.

../../_images/initializemainlooptasks.svg

Adding timers

When you want Kanzi to execute a task at regular time intervals, you can add the task before a stage of the main loop using MainLoopScheduler::prependTimer and after a stage of the main loop using MainLoopScheduler::appendTimer.

Use timer recurrence options to execute a timer only once (MainLoopScheduler::TaskRecurrence::OneTime) or repeatedly (MainLoopScheduler::TaskRecurrence::Recurring). When a recurring timer elapses multiple times in a single iteration of the Kanzi main loop, Kanzi executes that timer only once.

using TimerTask = function<void(chrono::nanoseconds, unsigned int)>;
public interface TimerTask {
    void handle(Duration elapsedDuration, int repeatCount);
}
interface TimerTask {
    fun handle(elapsedDuration: Duration?, repeatCount: Int)
}

When Kanzi executes a timer, it passes as arguments:

  • The elapsed duration rounded down to a multiple of the interval of the timer

  • The number of elapsed intervals

For example, to update time on a clock using a recurring timer:

void activateClock()
{
    // Add a recurring timer task that updates the clock once every second.
    m_clockTimer = getMainLoopScheduler().appendTimer(UserStage,
                                                      kzMakeFixedString("Clock"),
                                                      MainLoopScheduler::TimerRecurrence::Recurring,
                                                      chrono::seconds(1),
                                                      [this](auto elapsedDuration, auto repeatCount) {
                                                          this->updateClock(elapsedDuration * repeatCount);
                                                      });
}

void deactivateClock()
{
    // Remove the clock timer.
    getMainLoopScheduler().removeTimer(m_clockTimer);
}
void activateClock()
{
    MainLoopScheduler mls = getDomain().getMainLoopScheduler();

    // Add a recurring timer task that updates the clock once every second.
    mClockTimer = mls.appendTimer(MainLoopScheduler.UserStage, "Clock",
        TimerRecurrence.Recurring, Duration.ofSeconds(1), new MainLoopScheduler.TimerTask() {
            @Override
            public void handle(Duration elapsedDuration, int repeatCount)
            {
                updateClock(elapsedDuration.multipliedBy(repeatCount));
            }
        });
}

void deactivateClock()
{
    MainLoopScheduler mls = getDomain().getMainLoopScheduler();
    // Remove the clock timer.
    mls.removeTimer(mClockTimer);
}
fun activateClock() {
    // Add a recurring timer task that updates the clock once every second.
    mClocktimer = domain.mainLoopScheduler.appendTimer(
        MainLoopScheduler.UserStage, "Clock",
        TimerRecurrence.Recurring, Duration.ofSeconds(1)
    ) { elapsedDuration, repeatCount -> updateClock(elapsedDuration.multipliedBy(repeatCount.toLong())) }
}

fun deactivateClock() {
    // Remove the clock timer.
    domain.mainLoopScheduler.removeTimer(mClocktimer)
}

Balancing between UI responsiveness and resource loading

The memory that is required to contain all of the application resources can exceed the amount of RAM on your target device. You can break down your application into parts and define the navigation so that only some parts are loaded at the same time.

When your application navigates to a part, Kanzi receives a request to load the required resources. You can tell Kanzi to load these resources in loader threads instead of the UI thread. This way you can preserve the responsiveness of your application while Kanzi loads the resources.

While resources can be loaded in the loader threads, OpenGL requires that the GPU resources, such as textures, meshes, and materials are deployed in the UI thread where the resources are copied to the GPU memory.

Kanzi keeps the resources that it must deploy in a queue and by default deploys only one resource each frame. This way Kanzi keeps the UI responsive but can take longer to deploy the application resources.

You can use the DeploymentQueueBudget application configuration and keep loading resources each frame until the configured time period elapses. When your application must deploy many small resources, time budget greater than 0 can significantly decrease the time to deploy all resources.

You can define your own deployment logic by overriding the Application::progressDeploymentQueueOverride. Use the knowledge of the assets of your application or its execution environment to modify the behavior to deploy a different number of resources on a per-frame basis.

For example, when your application contains a lot of large resources, the default implementation of deploying one resource every frame can cause uneven framerate. When you define your own deployment logic you can throttle the deployment of resources:

void progressDeploymentQueueOverride() override
{
    ResourceManager* resourceManager = getResourceManager();

    // Deploy once and check whether any items remain in the queue.
    while (resourceManager->processDeploymentQueueItem() &&
           // Check whether conditions exists to continue deployment.
           shouldContinueDeployment())
    {
    };
}

To learn how to configure the deployment queue budget, see DeploymentQueueBudget.

To learn how to set the number of threads to use to load resources, see LoadingThreadCount.

Handling input

Handling global input

Usually you use UI controls and their messages to handle input. In some cases you want to handle input that is global. Kanzi calls Application::handleEvents for all input events, Application::onKeyInputEvent for keyboard events, and Application::onPointerInputEvent for mouse events. Use these functions to define the event handling logic in your application. Make sure to call the base implementation in your override.

For example, when you want to implement debug mode. See Adding one-time tasks.

Disabling input handling

A Kanzi application handles input by default. To optimize performance, you can disable input handling in an application that does not need it. When you disable input, Kanzi does not spend CPU resources on the input events that it receives from the platform. Keep in mind that when you disable input handling, the application cannot manage the key focus, either.

To disable input handling and key focus management, override the Application::setScreenOverride function:

// To disable input handling and key focus management, leave the implementation empty.
void setScreenOverride() override
{
}

Measuring application performance

The Kanzi performance profiling system enables you to measure the performance of your Kanzi application. You can measure the performance of:

See also

Application configuration reference

Measuring application performance

Best practices

Loading resources in parallel

Using kzb files