Linear Space Rendering Pipeline

Linear Space Rendering Pipeline

With version 1.42, Prysm becomes capable of handling SRGB textures passed as a user render target (RT) (the texture given to cohtml::ViewRenderer::SetRenderTarget). If the given user RT is an SRGB texture, the Renoir rendering library will linearize all incoming colors and do all color math and blending in linear space instead of gamma-corrected SRGB color space.

cohtml::View view = ...;
ViewRendererSettings settings;
cohtml::ViewRenderer viewRenderer = systemRenderer->CreateViewRenderer(view, settings);

renoir::Texture2D desc;
desc.Width = 1280;
desc.Height = 720;
desc.Samples = 1;

// The SRGB format activates the linear pipeline.
// This could also be renoir::PF_R16G16B16A16 or renoir::PF_R32G32B32A32
desc.Format = renoir::PF_R8G8B8A8_SRGB;

void* nativeTexObject = ...;
void* nativeDepthStencilObject = ...;

viewRenderer->SetRenderTarget(nativeTexObject, nativeDepthStencilObject, desc);

Renoir uses the standard formula for converting from gamma-encoded SRGB space to linear space:

static float SrgbToLinear(float srgb)
{
	float linear = srgb;
	if (linear <= 0.04045f)
	{
		linear = linear / 12.92f;
	}
	else
	{
		linear = std::powf((linear + 0.055f) / 1.055f, 2.4f);
	}
	return linear;
}

where the used gamma value is 2.4.

To note is that this has no bearing on how the colors are given in the HTML/CSS. From the frontend’s perspective, all colors in the CSS are defined in a standard gamma-corrected sRGB space. This means that there are no changes required to the CSS/HTML to make the linear rendering pipeline work. The linearization in the rendering library is enabled as long as the provided user RT is an SRGB texture with the new renoir::PF_R8G8B8A8_SRGB pixel format or a floating point texture with renoir::PF_R16G16B16A16 or renoir::PF_R32G32B32A32 format.

When the linear pipeline is in use, all intermediate render targets that Renoir allocates will also be SRGB textures. The linearized colors will be used for blending but ultimately the textures will contain gamma-corrected SRGB values. The encoding (on texture write) and the decoding (on texture read) are handled by the graphics API.

The Advantage of using a linear rendering pipeline is that the blending operations as well as interpolations between colors (like for gradients) can be done more accurately.

The following diagram illustrates the new rendering flow visually:

Defining linear space colors in CSS

As a part of the ability to use the linear pipeline, Prysm now also allows the definition of linear space colors straight into the CSS. The standard syntax of doing that is through the color(srgb-linear <color-values>). This tells Prysm to interpret the <color-values> as a linear color. This is useful when front-end developers want to define a color in an alternative color space rather than using gamma-corrected SRGB.

For example

<html>

<head>
<style>
#rect {
	width: 100px;
	height: 100px;
	background-color: color(srgb-linear 0.21 0.0 0.0 1.0)
}
</style>
</head>

<body>
<div id="rect"></div>
</body>

</html>

tells Prysm to interpret the given color as a color in linear space.

It is important to note however, that no matter how the color is being specified in the CSS, the rendering library of Prysm will get the gamma-corrected version of the color. This ensures that all colors given by Cohtml to Renoir are in the same gamma-corrected SRGB space. Those colors are linearized only if the corresponding user RT for the cohtml::View is an SRGB texture. This prevents users from accidentally using colors in different color spaces while drawing on the same render target.

This is in contrast to the behavior in Chrome. For example, in Chrome one would get different results if they are drawing two gradients with colors given in different color spaces. This is not the case in Prysm where all colors used by the rendering library will be consistently in a single color space.

Handling of images

All images decoded and allocated on the GPU images by Prysm are in renoir::PF_R8G8B8A8. Renoir will never allocate those images as SRGB textures. This is due to Prysm sharing resources (including images) between views and we don’t want to introduce some confusing logic of how the user RT of one view can affect how the images are allocated for all other views. Hence, for consistency Renoir always uses the renoir::PF_R8G8B8A8 format when possible when decoding images from .png files. Texture container formats like DDS and KTX have information about the format of the contained texture and when Renoir loads images from such formats, it will use the given format.

As a consequence, it is important to keep in mind that when using the linear rendering pipeline, some images might be loaded as simple renoir::PF_R8G8B8A8 textures even though the appropriate thing to do would be to load them as renoir::PF_R8G8B8A8_SRGB. This has to be worked around manually by the user code’s rendering backend if the use case is such that some of the image decoding is left to Renoir. This will generally not be a problem when all of the images in the UI are user images loaded in textures by client’s code.

Switching between linear and SRGB space rendering

Cohtml supports switching between the rendering modes at runtime. There are, however, several things that the user code has to do to make the switch possible. This has to do with how Cohtml and the rendering library Renoir loads and caches resources based on the color space of the rendering pipeline. For everything to work properly Cohtml has to unload most resources and reload them again. This can be achieved with the provided cohtml::View API.

The following snippet demonstrates the intended way of performing runtime switch between linear and SRGB space rendering.

cohtml::System* system = ...;
cohtml::View* view = ...;
cohtml::ViewRenderer* viewRenderer = ...;
// ....

renoir::Texture2D desc;
// ....
desc.Format = renoir::PF_R8G8B8A8_SRGB;
void* nativeTexObject = ...;
void* nativeDepthStencilObject = ...;
// update the user RT and enable the linear pipeline
viewRenderer->SetRenderTarget(nativeTexObject, nativeDepthStencilObject, desc);
// ....

// release any cached images
system->ClearCachedUnusedImages();

// unload the document so that every resource is loaded again
view->UnloadDocument();
// clear the caches so that any left over non-SRGB textuers are removed
view->QueueClearCaches(ICACHE_Shadows | ICACHE_Textures | ICACHE_SVGSurfaces | ICACHE_BackdropFilterSurfaces);
// reload the document so that everything is rendered again 
view->Reload();

This reloads the whole document and its resources. However, the JavaScript state will be lost as well and the windows.onload event will be fired again. If this is not wanted, we can simply clear the relevant caches:


// release any cached images
system->ClearCachedUnusedImages();

// clear the caches so that any left over non-SRGB textuers are removed
view->QueueClearCaches(ICACHE_Shadows | ICACHE_Textures | ICACHE_SVGSurfaces | ICACHE_BackdropFilterSurfaces);