Extended Setup Guide

This guide is meant to explain the process of setting up Prysm in detail. It takes into account multiple platforms, maintaining multiple views at once, running multiple threads for different types of tasks, logging, and various other things that are not needed for your initial setup. If you want to use an easier version of the setup below, you can check out the Quickstart Guide.

The sample applications coming with Prysm are available in the Samples folder. The Samples/Common folder contains platform-independent code that can be consulted on integrating Cohtml in a C++ application. Central in all samples is the CohtmlApplication class that resides in the Common/CohtmlApplication folder. This class is the one in charge of initializing, running and closing Cohtml. All the platform-specific code is in the samples themselves for Android, iOS and Windows Desktop.

The Android Studio sample project can be opened by selecting the Samples/Android folder. The iOS/MacOS sample applications can be compiled with the corresponding Samples/CohtmlSamples.xcworkspace files. For Windows and other all other platforms use the Samples/CohtmlSamples.sln for the respective platform. Platform-specific projects require Visual Studio 2017 with the corresponding SDK installed.

Integration

In general, an application that uses Cohtml usually goes through the following steps:

On the UI thread:

  • Initialize Cohtml
  • Create Views
  • Send events (input) & update UI (loop)
  • Destroy Views and uninitialize Cohtml

On the Render thread:

  • Create System renderer
  • Create View renderers
  • Paint UI in textures (loop)
  • Compose the UI as a HUD or in the game world (loop)
  • Free rendering resources and destroy renderers

All the operations are shown in code in the CohtmlApplication.cpp file. Let’s explore each one of them:

Building

To build an object using Cohtml you must:

  • Add the Cohtml headers in the include paths of the project. The headers are in the include/cohtml folder.
  • Link against the Cohtml library (in the lib/Platform folder)
  • Link against the rendering backend (in the lib/Platform folder)
  • On some platforms, you also have to link against a JavaScript virtual machine, such as V8 or ChakraCore (in the lib/Platform folder)
  • On some platforms, you also have to link against the Renoir graphics library (in the lib/Platform folder). If no Renoir library is present, this means that it is included in the CohtmlCombined (or similar) library, so no additional linking is needed.

On Windows the relevant link dependencies are:

  • cohtml.WindowsDesktop.lib
  • RenoirCore.WindowsDesktop.lib
  • dx11backend.lib or dx12backend.lib (or another Windows rendering backend)
  • You also have to distribute the dynamic libraries that Cohtml uses in the application folder:
    • cohtml.WindowsDesktop.dll
    • RenoirCore.WindowsDesktop.dll
    • v8.dll
    • v8_libbase.dll
    • v8_libplatform.dll
    • v8_zlib.dll
    • HttpServer.WindowsDesktop.dll (optional)
    • MediaDecoders.WindowsDesktop.dll (optional)

On Xbox One GDK the relevant link dependencies are:

  • cohtml.XboxOne.lib
  • RenoirCore.XboxOne.lib
  • dx12backend.lib (or another Xbox One GDK rendering backend)
  • You also have to distribute the dynamic libraries that Cohtml uses in the application folder:
    • cohtml.XboxOne.dll
    • RenoirCore.XboxOne.dll
    • v8.dll
    • v8_libbase.dll
    • v8_libplatform.dll
    • v8_zlib.dll
    • MediaDecoders.XboxOne.dll (optional)
    • HttpServer.XboxOne.dll (optional)

On Xbox Series X and Series S the relevant link dependencies are:

  • cohtml.Scarlett.lib
  • RenoirCore.Scarlett.lib
  • dx12backend.lib (or another Xbox Series X and Series S rendering backend)
  • You also have to distribute the dynamic libraries that Cohtml uses in the application folder:
    • cohtml.Scarlett.dll
    • RenoirCore.Scarlett.dll
    • v8.dll
    • v8_libbase.dll
    • v8_libplatform.dll
    • v8_zlib.dll
    • MediaDecoders.Scarlett.dll (optional)
    • HttpServer.Scarlett.dll (optional)

On PlayStation 4 the relevant link dependencies are:

  • libcohtml.ORBIS_stub_weak.a
  • libRenoirCore.PS4_stub_weak.a
  • GNM.a (or another PlayStation 4 rendering backend)
  • You also have to distribute the dynamic libraries that Cohtml uses in the application folder:
    • libcohtml.Orbis.prx
    • libRenoirCore.PS4.prx
    • HttpServer.Orbis.prx (optional)
    • MediaDecoders.Orbis.prx (optional)

On PlayStation 5 the relevant link dependencies are:

  • libcohtml.Prospero_stub_weak.a
  • libRenoirCore.PS5_stub_weak.a
  • RenoirAGCShaderLib.a
  • AGCBackend.a (or another PlayStation 5 rendering backend)
  • You also have to distribute the dynamic libraries that Cohtml uses in the application folder:
    • libcohtml.Prospero.prx
    • libRenoirCore.PS5.prx
    • HttpServer.Prospero.prx (optional)
    • MediaDecoders.Prospero.prx (optional)

On Android the relevant link dependencies are:

  • libCohtmlCombined.Android.a
  • libv8_monolith.a
  • libgles3backend.a or libVulkanBackend.Android.a (or another backend)
  • You might also want to distribute the following dynamic libraries that Cohtml uses in the application folder:
    • libMediaDecoders_Android.so (optional)
    • libHttpServer_Android.so (optional)

On iOS you have to link against:

  • libCohtmlCombined.iOS.a
  • libv8_monolith.a
  • libgles3backend.iOS.a or libMetalBackend.iOS.a
  • libMediaDecoders.iOS.a (optional, can be replaced with libMediaDecodersEmpty.iOS.a)

Initialization

bool CohtmlApplication::Initialize(const Options& options)
{
    // Initialize the application logging system.
    m_Logger.reset(new Logger("TestApp.log"));
    gLogger = m_Logger.get();

    // Cohtml initilization begin here
    using namespace cohtml; // All Cohtml code is in the 'cohtml' namespace

    // 1. We need to initilize the Cohtml Library
    LibraryParams params;
    params.LoggingSeverity = cohtml::Logging::Info; // What logging messages we want to receive
    params.Allocator = &m_MemoryTracker; // All memory allocations go through a user-supplied allocator
    params.WritableDirectory = options.WritableDirectory; // We have to provide a writable folder where Cohtml will output logged inforamtion
    params.FileSystemReader = options.FileReader; // The file reader will be used to read local resources like fonts.
    params.UseDedicatedLayoutThread = options.MakeDedicatedLayoutThread; // If we'll be doing the Layout work in an auxilliary thread or in the Advance method
    params.SharedLibraryLocation = options.SharedLibraryLocation;
    // If we simulate a task system - we want to be notified when there are new Cohtml work tasks we have to
    // execute in helper threads. This is not needed if we dedicate worker threads completely to Cohtml and is
    // actually a performance penalty.
    if (m_TaskSchedulerMode == TaskScheduler::Mode::SimulateTaskSystem)
    {
        params.OnWorkAvailable = &OnWorkAvailable;
        params.OnWorkAvailableUserData = this;
    }

    // 2. Initialize the library now.
    m_Library = Library::Initialize(COHTML_LICENSE_KEY, params);
    if (!m_Library)
    {
        APP_LOG(Error, "Unable to initialize COHTML Library!");
        return false;
    }

    // 2.1 Start the Task Scheduler - it will start doing Cohtml work on auxilliary threads
    // The Task Scheduler in these samples can be used as an example on how to integrate
    // Cohtml in an engine's task (job) system. It supports different modes in order to better
    // illustrate the options available for integration.
    m_TaskScheduler.reset(new TaskScheduler(m_TaskSchedulerMode, m_Library, m_HasDedicatedLayoutThread));

    // 3. We have to create a "System". Each system can hold multiple "Views" and provide
    // a common context for them.
    cohtml::SystemSettings sysSettings;
    // The resource handler is used for loading View-specific resources from local files.
    if (options.ResourceHandler)
    {
        sysSettings.ResourceHandler = options.ResourceHandler;
    }
    else
    {
        sysSettings.ResourceHandler = &m_ResourceLoader;
    }

    m_System = m_Library->CreateSystem(sysSettings);
    if (!m_System)
    {
        APP_LOG(Error, "Unable to initialize COHTML System!");
        return false;
    }

    m_Views.resize(options.ViewsCount);
    for (auto i = 0u; i < options.ViewsCount; ++i)
    {
        auto& current = m_Views[i];
        // 4. Now create the View. The View holds a single UI instance - the HTML DOM, styles and a
        // JavaScript context. This could be the whole HUD of the application.
        cohtml::ViewSettings viewSettings;
        viewSettings.Width = options.Width; // The logical Width of the View
        viewSettings.Height = options.Height; // The logical Height of the View
        // Called when the View is completely advance and laid-out
        // note: This can be called on any thread!
        // For example attached is &Application::WakeRenderingThread;
        viewSettings.OnViewAdvanceComplete = options.OnViewAdvanceComplete;
        // Passed to the OnViewAdvanceComplete callback
        viewSettings.UserData = m_UserData;

        // 5. Set up the listener for this view.
        // The Listener gets notifications for various events of the view, such as when the view can accept JavaScript bindings.
        current.Listener.Application = this;
        viewSettings.Listener = &current.Listener;

        // 6. Create our View now
        current.View = m_System->CreateView(viewSettings);
        if (!current.View)
        {
            APP_LOG(Error, "Unable to create COHTML View!");
            return false;
        }
        current.Listener.View = current.View;
        current.ScriptCreatedCallback = options.OnScriptCreated;
        current.ShowVirtualKeyboardCallback = options.OnShowVirtualKeyboard;
        // This method can be very useful for debugging. Enable to render dirty rects and regions.
        //current.View->ShowPaintRectangles(true); // useful for debugging

        // 7. Load a page in the View. Local pages need to have the special "coui" protocol.
        // For instance a valid page to load is: "coui://uiresources/HUD.html"
        if (options.InitialURL != nullptr && strlen(options.InitialURL) > 0)
        {
            current.View->LoadURL(options.InitialURL);
        }
        else
        {
            APP_LOG(Error, "Initial URL is not set!");
            return false;
        }

        if (options.RedrawAll)
        {
            // Method useful for debugging. Enable to re-draw everyting on every frame.
            current.View->ContinuousRepaint(true);
        }

        current.NameplatesPtr.reset(new Nameplates(current.View));
    }

    APP_LOG(Info, "Initialized application!");
    return true;
}

Rendering initialization

bool CohtmlApplication::InitializeRenderThread(const RendererOptions& options)
{
    // 1. Create the System renderer. It holds resources shared by all ViewRenderers within a System
    cohtml::SystemRendererSettings sysRendSettings;
    m_SystemRenderer = m_System->CreateSystemRenderer(sysRendSettings);
    if (!m_SystemRenderer)
    {
        APP_LOG(Error, "Unable to create COHTML System renderer!");
        return false;
    }

    // 2. Set the rendering backend that will be used by the SystemRenderer
    // All Views created from the same System will share resources here (text atlases, textures etc.)
    m_SystemRenderer->RegisterRenderThread(options.Backend);

    auto i = 0u;
    for (auto& current : m_Views)
    {
        // 3. Create the ViewRenderer that will draw the page
        cohtml::ViewRendererSettings viewRendSettings;
        current.ViewRenderer = m_SystemRenderer->CreateViewRenderer(current.View, viewRendSettings);
        if (!current.ViewRenderer)
        {
            APP_LOG(Error, "Unable to create COHTML View renderer!");
            return false;
        }

        // 4. Set the user texture where the renderer will draw the View
        if (options.NativeTextures != nullptr)
        {
            current.ViewRenderer->SetRenderTarget(options.NativeTextures[i].ViewNativeTexture,
                options.NativeTextures[i].NativeDepthStencilTexture,
                options.NativeTextures[i].ViewNativeTextureWidth,
                options.NativeTextures[i].ViewNativeTextureHeight,
                options.NativeTextures[i].ViewNativeTextureSamples);
        }
        ++i;
    }

    m_HasInitializedRenderer = true;

    APP_LOG(Info, "Initialized Render Thread!");

    return true;
}

Looping

Each frame on the UI thread you have to “Advance” the Views - update its internal clock and let animations happen. Update position of nameplates.

void CohtmlApplication::Advance(double timeMiliseconds)
{
    if (!m_HasInitializedRenderer)
        return;

    for (auto& current : m_Views)
    {
        // Call each frame to drive the animations
        if (current.View)
        {
            auto frameId = current.View->Advance(timeMiliseconds);

            // This is a sample workload in the sample
            current.NameplatesPtr->Update(float(timeMiliseconds));

            // Post a render task on the render thread
            std::lock_guard<std::mutex> l(FramesMutex);
            current.FramesToPaint.push(frameId);
        }
    }

    //  Advance the System
    m_System->Advance(timeMiliseconds);
}

At the same time, on the Render thread, we have to let Cohtml update the UI textures.

void CohtmlApplication::Render()
{
    for (auto& current : m_Views)
    {
        // Call each render frame to draw the View
        if (current.ViewRenderer)
        {
            std::lock_guard<std::mutex> l(FramesMutex);
            while (!current.FramesToPaint.empty())
            {
                auto frameId = current.FramesToPaint.front();
                current.FramesToPaint.pop();

                current.ViewRenderer->Paint(frameId, true);
            }
        }
    }
}

When the Paint method has finished, we compose the UI texture on top of the application. This is done differently on the platforms. On iOS for instance consult the GameViewController.mm file where in the drawInRect method the UI texture is copied on the final iOS frame buffer.

Changing Views

You can load another page in the current View by using the Load method. It will load all resources on the new page and re-create the scripting context. You will have to re-bind all native methods upon a page change.

Uninitializing

The uninitialization of objects must happen in the following order:

On the Render thread:

  • cohtml::ViewRenderer::Destroy() of each View
  • If quitting: cohtml::SystemRenderer::FreeRenderingResources(); and cohtml::SystemRenderer::Destroy();

On the UI thread:

  • cohtml::View::Destroy() each View
  • cohtml::System::Destroy()
  • cohtml::Library::StopWorkers()

Finally, uninitialize the library. This call must be invoked on the same thread where the library was initialized(which could potentially be different than the UI thread)!

  • cohtml::Library::Uninitialize()

All resources that are owned by the application -> resource readers, logger, memory allocator and rendering backend must be destroyed after the Cohtml objects that use them.

Android Release Apk Signing

When building in Debug, your app should be automatically signed with an auto-generated debug keystore. When you build for Release, however, you have to sign the app yourself. To do that follow these steps in Android Studio:

  1. Click Build > Generate Signed APK on the menu bar.
  2. If you have a keystore you can use it by providing the path. If you do not have one, then click Create new and fill in the required information.
  3. Then on Generate Signed APK window, select the keystore that you have just created, enter the password and click Next.
  4. Select your apk destination folder, select release in build type and click finish.
  5. You should now be able to build your app in Release.