Since version 1.9.5 Prysm supports a new CSS property: coh-composition-id. This property allows for completely detaching the element from the default rendering flow and letting the user handle the drawing of the element. This feature is intended for achieving “real 3D” effects with the UI by taking the transformation data that the SDK provides and composing elements at any world position and orientation, not just on the UI plane.

Custom compositor interface

A custom compositor can be set using the cohtml::View::SetCustomSceneCompositor API which would then receive callbacks when the composition needs to be drawn. You can also specify arbitrary user data that will be passed back through the interface callbacks.


By default, renoir::ISublayerCompositor callbacks are invoked on the Main and the Render Thread/Layout Thread.

  • OnCompositionAdded- Main Thread
  • OnCompositionRemoved - Main Thread
  • OnCompositionVisibility - Main Thread
  • OnDrawSubLayer - Render Thread/Layout Thread

In some cases, it might be required to have them called on the Main Thread, for example, if you want to compose the sublayer with the Material API of an engine that requires all calls to be made from the Main Thread.

You can configure what thread calls the renoir::ISublayerCompositor callbacks during the initialization of the SDK. To do that, you need to initialize the Cohtml SDK with the following parameters:

  • When initializing the Library with Library::Initialize:
    • Set LibraryParams::UseDedicatedLayoutThread to false
  • When creating a View with System::CreateView:
    • Set ViewSettings::ExecuteLayoutOnDedicatedThread to false
    • Set ViewSettings::ExecuteCommandProcessingWithLayout to true

OnCompositionAdded callback

The callback is called on the Main Thread.

During Advance of the view when style for composition-id is matched to the element you’ll receive the OnCompositionAdded callback with the composition ID and the ID of the view which owns that composition.

You can use that callback to prepare all resources needed for the composition.

void SimpleCompositor::AddDrawData(unsigned viewId, const char* compositionId)
    std::lock_guard<std::mutex> l(m_DrawDatasMutex);
    // Here you can do some work which has to be done on Main Thread.
    // For example you can create resources needed for composition rendering.
    m_ViewDrawData.emplace(std::string(compositionId), renoir::ISubLayerCompositor::DrawData());

OnCompositionRemoved callback

The callback is called on the Main Thread.

When the composition is no longer needed (e.g. the element is deleted from the HTML, you’ll receive the OnCompositionRemoved callback with the composition ID and the ID of the view which contains the composition.

You can use that callback for cleaning up any resources allocated for the specified composition.

void SimpleCompositor::OnCompositionRemoved(unsigned viewId, const char* compositionId)
    std::lock_guard<std::mutex> l(m_DrawDatasMutex);
    // Here you can do some work which has to be done on Main Thread.
    // For example you can destroy resources needed for composition rendering.

OnCompositionVisibility callback

The callback is called on the Main Thread.

At each frame, you will receive the OnCompositionVisibility callback with a boolean that is true if the element with the coh-composition-id is visible and false otherwise. An element is made invisible via the opacity, display, and visibility CSS properties.

You can use this to avoid drawing elements that would not be visible anyway, thus reducing the number of draw calls.

OnDrawSubLayer callback

Depending on ViewSettings::ExecuteCommandProcessingWithLayout option it can be called on different threads 
- If it's `true` the callback will come on Layout Thread
- If it's `false` the callback will come on Render Thread

The data available in the OnDrawSubLayer callback is the renoir::ISublayerCompositor::DrawData structure:

  • unsigned ViewId: The Id of the view which is drawing the composition.
  • const char* SubLayerCompositionId: Composition ID (coh-composition-id).
  • void* CustomSceneMetadata: Custom data set with cohtml::View::SetCustomSceneCompositor
  • Texture2DObject Texture: Backend texture ID to be sampled from.
  • Texture2D TextureInfo: Texture info for the sampled image (e.g. dimensions, format, etc.)
  • float2 UVScale: UV scaling is needed to sample from the correct location.
  • float2 UVOffset: UV offset is needed to sample from the correct location. Basically, the sampled point should be input.Additional.xy * UVScale.xy + UVOffset.xy
  • float4x4 FinalTransform: Transformation of the quad to be drawn.
  • Rectangle Untransformed2DTargetRect: Vertex positions of the quad to be drawn.
  • ColorMixingModes MixingMode: Color mixing mode with the background.
void SimpleCompositor::OnDrawSubLayer(const renoir::ISubLayerCompositor::DrawData& data)
    std::lock_guard<std::mutex> l(m_DrawDatasMutex);
    m_ViewDrawData[std::string(data.SubLayerCompositionId)] = data;
void SimpleCompositor::OnDrawSubLayer(const renoir::ISubLayerCompositor::DrawData& data)
    std::lock_guard<std::mutex> l(m_DrawDatasMutex);
    auto it = m_ViewDrawData.find(std::string(data.SubLayerCompositionId));
    if (it != m_ViewDrawData.end())
        m_ViewDrawData[std::string(data.SubLayerCompositionId)] = data;

Compositor Input

It’s possible to pass input events that will only trigger for a specific composition and not for regular elements or other compositions in the same view. To do that set the last optional argument of the given input function to the Id of the target composition. Coordinates for the event should be computed by using the Untransformed2DTargetRect and the UV coordinates from the event hit in the following manner:

CoherentMouseData.X = event->UVcoord.X * Untransformed2DTargetRect.Size.x + Untransformed2DTargetRect.Position.x;
CoherentMouseData.Y = event->UVcoord.Y * Untransformed2DTargetRect.Size.y + Untransformed2DTargetRect.Position.y;

View->MouseEvent(CoherentMouseData, nullptr, UserData, CompositionId);

Known Issues

  1. Composited elements placed outside of the viewport won’t be updated after their first draw.
  2. When a composited element is redrawn it also redraws intersecting elements at its original position in the document.
  1. Composited elements that become empty will remain dirty. This can either be caused by removing all children or hiding them via display, opacity, or visibility.

Sample use case

A common scenario for using the compositor is to achieve a “real 3D” UI. The feature allows you to specify 3D transformations in CSS and then use the information that the SDK passes to the compositor interface to render that element in your 3D world using the transformation that was specified in the CSS.

Usually, you’d create a single rectangle geometry to be stored on the GPU and reuse that for all composited surfaces, only changing the vertex transformation.

It is important to transform the geometry exactly into the Untransformed2DTargetRect positions, otherwise applying the FinalTransform matrix may have unexpected results.

Applying transformations example

Let’s take for example an engine that uses the center of an object as its transform-origin (as opposed to top-left being the transform-origin in HTML/CSS, by default). If the rectangle geometry on the GPU has bounds MeshBounds, then the following list of transformations has to be applied:

  • Scale the GPU geometry to the size of the rectangle, expected by the SDK
T1 = Scale(Untransformed2DTargetRect.Size / MeshBounds)
  • Offset the geometry so that its top-left corner coincides with the transform-origin. Any transformation applied will behave as if it was done in HTML/CSS now.
T2 = Translate(Untransformed2DTargetRect.Position + Untransformed2DTargetRect.Size / 2)
  • Apply the FinalTransform
T3 = `FinalTransform`
  • The geometry will now be in Cohtml View space, which has its transform-origin at the top-left. We have to modify it to the center again, as we’ll be using this geometry in the engine (which uses the object’s center as transform-origin, as we described in the beginning). Assuming the width/height of the view is known as ViewSize:
T4 = Translate(-ViewSize / 2)
  • The geometry is now positioned with the proper transform-origin that the engine uses but is still in Cohtml View space units. The last thing left to do is to scale that. Assuming that the parent object that has the in-world Cohtml View attached has bounds ParentBounds, that’s simply:
T5 = Scale(ParentBounds / ViewSize)

Finally, all of these must be combined. M1 = T1 * T2 * T3 * T4 is a simple matrix multiplication. Result = M1 * T5 must be completed as a transform multiplication in order to apply the scaling using the original axes of the element. In general, if a matrix is decomposed into a transform with components Q, S, T, where Q = quaternion, S = scale, and T = translation, the result of multiplying M1 * T5 would be

Result.Q = T5.Q * M1.Q
Result.S = M1.S * T5.S
Result.T = T5.Q * (T5.S * M1.T) + T5.T

Recompose that into a matrix and that’s what the final transform is.