Color management: Implement shader to convert to scene linear

Previous conversion to scene linear was done in the display transform
shader. Having a separate shader to convert texture to scene linear
allows drawing input textures with different color spaces into a
viewport and apply display transform on all of them.

Currently unused, but is required for !138094. It could also be used
in the future to avoid host-side linearization in the image engine.

Internally it uses a lot of the same logic for shader caching and
binding, but the code is refactored a bit to make it easier to have
a stronger separation in the future if needed.

Ref #138094

Pull Request: https://projects.blender.org/blender/blender/pulls/138308
This commit is contained in:
Sergey Sharybin
2025-05-02 16:51:46 +02:00
committed by Sergey Sharybin
parent a26ed85adf
commit 5b29ba488f
6 changed files with 262 additions and 71 deletions

View File

@@ -333,9 +333,16 @@ bool OCIO_gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
use_white_balance);
}
void OCIO_gpuDisplayShaderUnbind()
bool OCIO_gpuToSceneLinearShaderBind(OCIO_ConstConfigRcPtr *config,
const char *from_colorspace_name,
const bool use_predivide)
{
impl->gpuDisplayShaderUnbind();
return impl->gpuToSceneLinearShaderBind(config, from_colorspace_name, use_predivide);
}
void OCIO_gpuShaderUnbind()
{
impl->gpuShaderUnbind();
}
void OCIO_gpuCacheFree()

View File

@@ -211,7 +211,10 @@ bool OCIO_gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
const bool use_overlay,
const bool use_hdr,
const bool use_white_balance);
void OCIO_gpuDisplayShaderUnbind(void);
bool OCIO_gpuToSceneLinearShaderBind(OCIO_ConstConfigRcPtr *config,
const char *from_colorspace_name,
bool use_predivide);
void OCIO_gpuShaderUnbind(void);
void OCIO_gpuCacheFree(void);
const char *OCIO_getVersionString(void);

View File

@@ -128,7 +128,14 @@ class IOCIOImpl {
{
return false;
}
virtual void gpuDisplayShaderUnbind() {}
virtual bool gpuToSceneLinearShaderBind(OCIO_ConstConfigRcPtr * /*config*/,
const char * /*from_colorspace_name*/,
const bool /*use_predivide*/)
{
return false;
}
virtual void gpuShaderUnbind() {}
virtual void gpuCacheFree() {}
virtual const char *getVersionString() = 0;
@@ -339,6 +346,7 @@ class OCIOImpl : public IOCIOImpl {
void OCIO_PackedImageDescRelease(OCIO_PackedImageDesc *id) override;
bool supportGPUShader() override;
/**
* Setup GPU contexts for a transform defined by processor using GLSL.
* All LUT allocating baking and shader compilation happens here.
@@ -346,7 +354,7 @@ class OCIOImpl : public IOCIOImpl {
* Once this function is called, callee could start drawing images
* using regular 2D texture.
*
* When all drawing is finished, gpuDisplayShaderUnbind must be called to
* When all drawing is finished, gpuShaderUnbind must be called to
* restore GPU context to its previous state.
*/
bool gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
@@ -364,7 +372,21 @@ class OCIOImpl : public IOCIOImpl {
const bool use_overlay,
const bool use_hdr,
const bool use_white_balance) override;
void gpuDisplayShaderUnbind() override;
/**
* Setup GPU contexts for a GPU-side transform form the given space to scene linear.
*
* Once this function is called, callee could start drawing images using regular 2D texture
* (in the same way as GPU_SHADER_3D_IMAGE_COLOR immediate mode shader).
*
* When all drawing is finished, gpuShaderUnbind must be called to restore GPU context to its
* previous state.
*/
bool gpuToSceneLinearShaderBind(OCIO_ConstConfigRcPtr *config,
const char *from_colorspace_name,
bool use_predivide) override;
void gpuShaderUnbind() override;
void gpuCacheFree() override;
const char *getVersionString() override;

View File

@@ -139,9 +139,19 @@ struct OCIO_GPUDisplayShader {
/** Error checking. */
bool valid = false;
bool equals(const char *input,
const char *view,
const char *display,
const char *look,
const bool use_curve_mapping) const
{
return (this->input == input && this->view == view && this->display == display &&
this->look == look && this->use_curve_mapping == use_curve_mapping);
}
};
static const int SHADER_CACHE_MAX_SIZE = 4;
static const int SHADER_CACHE_MAX_SIZE = 8;
std::list<OCIO_GPUDisplayShader> SHADER_CACHE;
/* -------------------------------------------------------------------- */
@@ -605,32 +615,33 @@ bool OCIOImpl::supportGPUShader()
return true;
}
static OCIO_GPUDisplayShader &getGPUDisplayShader(
OCIO_ConstConfigRcPtr *config,
const char *input,
const char *view,
const char *display,
const char *look,
OCIO_CurveMappingSettings *curve_mapping_settings)
static OCIO_GPUDisplayShader *getGPUDisplayShaderFromCache(const char *input,
const char *view,
const char *display,
const char *look,
const bool use_curve_mapping)
{
/* Find existing shader in cache. */
const bool use_curve_mapping = (curve_mapping_settings != nullptr);
for (std::list<OCIO_GPUDisplayShader>::iterator it = SHADER_CACHE.begin();
it != SHADER_CACHE.end();
it++)
{
if (it->input == input && it->view == view && it->display == display && it->look == look &&
it->use_curve_mapping == use_curve_mapping)
{
if (it->equals(input, view, display, look, use_curve_mapping)) {
/* Move to front of the cache to mark as most recently used. */
if (it != SHADER_CACHE.begin()) {
SHADER_CACHE.splice(SHADER_CACHE.begin(), SHADER_CACHE, it);
}
return *it;
return &(*it);
}
}
return nullptr;
}
/**
* Create default-initialized OCIO_GPUDisplayShader and put it to cache.
* The function ensures the cache has up to SHADER_CACHE_MAX_SIZE entries.
*/
static OCIO_GPUDisplayShader &gpuDisplayShaderCreateAndCache()
{
/* Remove least recently used element from cache. */
if (SHADER_CACHE.size() >= SHADER_CACHE_MAX_SIZE) {
SHADER_CACHE.pop_back();
@@ -640,6 +651,60 @@ static OCIO_GPUDisplayShader &getGPUDisplayShader(
SHADER_CACHE.emplace_front();
OCIO_GPUDisplayShader &display_shader = SHADER_CACHE.front();
return display_shader;
}
static void createGPUShaderDescriptors(OCIO_GPUDisplayShader &display_shader,
ConstProcessorRcPtr processor_to_scene_linear,
ConstProcessorRcPtr processor_to_display,
const bool use_curve_mapping)
{
GpuShaderDescRcPtr shaderdesc_to_scene_linear = GpuShaderDesc::CreateShaderDesc();
shaderdesc_to_scene_linear->setLanguage(GPU_LANGUAGE_GLSL_1_3);
shaderdesc_to_scene_linear->setFunctionName("OCIO_to_scene_linear");
shaderdesc_to_scene_linear->setResourcePrefix("to_scene");
processor_to_scene_linear->getDefaultGPUProcessor()->extractGpuShaderInfo(
shaderdesc_to_scene_linear);
shaderdesc_to_scene_linear->finalize();
GpuShaderDescRcPtr shaderdesc_to_display = GpuShaderDesc::CreateShaderDesc();
shaderdesc_to_display->setLanguage(GPU_LANGUAGE_GLSL_1_3);
shaderdesc_to_display->setFunctionName("OCIO_to_display");
shaderdesc_to_display->setResourcePrefix("to_display");
processor_to_display->getDefaultGPUProcessor()->extractGpuShaderInfo(shaderdesc_to_display);
shaderdesc_to_display->finalize();
/* Create GPU shader and textures. */
if (createGPUTextures(
display_shader.textures, shaderdesc_to_scene_linear, shaderdesc_to_display) &&
createGPUShader(display_shader.shader,
display_shader.textures,
shaderdesc_to_scene_linear,
shaderdesc_to_display,
use_curve_mapping))
{
display_shader.valid = true;
}
}
static OCIO_GPUDisplayShader &getGPUDisplayShader(
OCIO_ConstConfigRcPtr *config,
const char *input,
const char *view,
const char *display,
const char *look,
OCIO_CurveMappingSettings *curve_mapping_settings)
{
const bool use_curve_mapping = (curve_mapping_settings != nullptr);
/* Find existing shader in cache. */
OCIO_GPUDisplayShader *cached_shader = getGPUDisplayShaderFromCache(
input, view, display, look, use_curve_mapping);
if (cached_shader) {
return *cached_shader;
}
OCIO_GPUDisplayShader &display_shader = gpuDisplayShaderCreateAndCache();
display_shader.input = input;
display_shader.view = view;
display_shader.display = display;
@@ -668,35 +733,13 @@ static OCIO_GPUDisplayShader &getGPUDisplayShader(
/* Create shader descriptions. */
if (processor_to_scene_linear && processor_to_display) {
GpuShaderDescRcPtr shaderdesc_to_scene_linear = GpuShaderDesc::CreateShaderDesc();
shaderdesc_to_scene_linear->setLanguage(GPU_LANGUAGE_GLSL_1_3);
shaderdesc_to_scene_linear->setFunctionName("OCIO_to_scene_linear");
shaderdesc_to_scene_linear->setResourcePrefix("to_scene");
(*(ConstProcessorRcPtr *)processor_to_scene_linear)
->getDefaultGPUProcessor()
->extractGpuShaderInfo(shaderdesc_to_scene_linear);
shaderdesc_to_scene_linear->finalize();
GpuShaderDescRcPtr shaderdesc_to_display = GpuShaderDesc::CreateShaderDesc();
shaderdesc_to_display->setLanguage(GPU_LANGUAGE_GLSL_1_3);
shaderdesc_to_display->setFunctionName("OCIO_to_display");
shaderdesc_to_display->setResourcePrefix("to_display");
(*(ConstProcessorRcPtr *)processor_to_display)
->getDefaultGPUProcessor()
->extractGpuShaderInfo(shaderdesc_to_display);
shaderdesc_to_display->finalize();
/* Create GPU shader and textures. */
if (createGPUTextures(
display_shader.textures, shaderdesc_to_scene_linear, shaderdesc_to_display) &&
createGPUCurveMapping(display_shader.curvemap, curve_mapping_settings) &&
createGPUShader(display_shader.shader,
display_shader.textures,
shaderdesc_to_scene_linear,
shaderdesc_to_display,
use_curve_mapping))
{
display_shader.valid = true;
createGPUShaderDescriptors(display_shader,
*(ConstProcessorRcPtr *)processor_to_scene_linear,
*(ConstProcessorRcPtr *)processor_to_display,
use_curve_mapping);
if (display_shader.valid) {
display_shader.valid &= createGPUCurveMapping(display_shader.curvemap,
curve_mapping_settings);
}
}
@@ -711,25 +754,62 @@ static OCIO_GPUDisplayShader &getGPUDisplayShader(
return display_shader;
}
bool OCIOImpl::gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
const char *input,
const char *view,
const char *display,
const char *look,
OCIO_CurveMappingSettings *curve_mapping_settings,
const float scale,
const float exponent,
const float dither,
const float temperature,
const float tint,
const bool use_predivide,
const bool use_overlay,
const bool use_hdr,
const bool use_white_balance)
static OCIO_GPUDisplayShader &getGPUToLinearDisplayShader(OCIO_ConstConfigRcPtr *config,
const char *input)
{
/* Find existing shader in cache.
* Assume that empty names for display, view, and look are not valid for OCIO configuration, and
* so they can be used to indicate that the processor is used to convert from the given space to
* the linear. */
/* TODO(sergey): Using separate storage for to-linear processor caches might be better. */
OCIO_GPUDisplayShader *cached_shader = getGPUDisplayShaderFromCache(input, "", "", "", false);
if (cached_shader) {
return *cached_shader;
}
OCIO_GPUDisplayShader &display_shader = gpuDisplayShaderCreateAndCache();
display_shader.input = input;
display_shader.use_curve_mapping = false;
display_shader.valid = false;
OCIO_ConstProcessorRcPtr *processor_to_scene_linear = OCIO_configGetProcessorWithNames(
config, input, ROLE_SCENE_LINEAR);
OCIO_ConstProcessorRcPtr *processor_to_display = OCIO_configGetProcessorWithNames(
config, input, input);
/* Create shader descriptions. */
if (processor_to_scene_linear && processor_to_display) {
createGPUShaderDescriptors(display_shader,
*(ConstProcessorRcPtr *)processor_to_scene_linear,
*(ConstProcessorRcPtr *)processor_to_display,
false);
}
/* Free processors. */
if (processor_to_scene_linear) {
OCIO_processorRelease(processor_to_scene_linear);
}
if (processor_to_display) {
OCIO_processorRelease(processor_to_display);
}
return display_shader;
}
/* Bind the shader and update parameters and uniforms. */
static bool gpuShaderBind(OCIO_ConstConfigRcPtr *config,
OCIO_GPUDisplayShader &display_shader,
OCIO_CurveMappingSettings *curve_mapping_settings,
const float scale,
const float exponent,
const float dither,
const float temperature,
const float tint,
const bool use_predivide,
const bool use_overlay,
const bool use_hdr,
const bool use_white_balance)
{
/* Get GPU shader from cache or create new one. */
OCIO_GPUDisplayShader &display_shader = getGPUDisplayShader(
config, input, view, display, look, curve_mapping_settings);
if (!display_shader.valid) {
return false;
}
@@ -764,7 +844,7 @@ bool OCIOImpl::gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
if (use_white_balance) {
/* Compute white point of the scene space in XYZ.*/
float3x3 xyz_to_scene;
configGetXYZtoSceneLinear(config, xyz_to_scene.ptr());
OCIO_configGetXYZtoSceneLinear(config, xyz_to_scene.ptr());
float3x3 scene_to_xyz = blender::math::invert(xyz_to_scene);
float3 target = scene_to_xyz * float3(1.0f);
@@ -786,7 +866,63 @@ bool OCIOImpl::gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
return true;
}
void OCIOImpl::gpuDisplayShaderUnbind()
bool OCIOImpl::gpuDisplayShaderBind(OCIO_ConstConfigRcPtr *config,
const char *input,
const char *view,
const char *display,
const char *look,
OCIO_CurveMappingSettings *curve_mapping_settings,
const float scale,
const float exponent,
const float dither,
const float temperature,
const float tint,
const bool use_predivide,
const bool use_overlay,
const bool use_hdr,
const bool use_white_balance)
{
/* Get GPU shader from cache or create new one. */
OCIO_GPUDisplayShader &display_shader = getGPUDisplayShader(
config, input, view, display, look, curve_mapping_settings);
return gpuShaderBind(config,
display_shader,
curve_mapping_settings,
scale,
exponent,
dither,
temperature,
tint,
use_predivide,
use_overlay,
use_hdr,
use_white_balance);
}
bool OCIOImpl::gpuToSceneLinearShaderBind(OCIO_ConstConfigRcPtr *config,
const char *from_colorspace_name,
const bool use_predivide)
{
/* Get GPU shader from cache or create new one. */
OCIO_GPUDisplayShader &display_shader = getGPUToLinearDisplayShader(config,
from_colorspace_name);
return gpuShaderBind(config,
display_shader,
nullptr, /* curve_mapping_settings */
1.0f, /* scale */
1.0f, /* exponent */
0.0f, /* dither */
6500.0f, /* temperature */
10.0f, /* tint */
use_predivide,
false /* use_overlay */,
true, /* use_hdr */
false /* use_white_balance */);
}
void OCIOImpl::gpuShaderUnbind()
{
immUnbindProgram();
}

View File

@@ -486,6 +486,16 @@ bool IMB_colormanagement_setup_glsl_draw_from_space_ctx(const bContext *C,
ColorSpace *from_colorspace,
float dither,
bool predivide);
/**
* Configures GPU shader for conversion from the given space to scene linear.
* Drawing happens in the same immediate mode as when GPU_SHADER_3D_IMAGE_COLOR shader is used.
*
* Returns true if the GPU shader was successfully bound.
*/
bool IMB_colormanagement_setup_glsl_draw_to_scene_linear(const char *from_colorspace_name,
bool predivide);
/**
* Finish GLSL-based display space conversion.
*/

View File

@@ -4285,10 +4285,23 @@ bool IMB_colormanagement_setup_glsl_draw_ctx(const bContext *C, float dither, bo
return IMB_colormanagement_setup_glsl_draw_from_space_ctx(C, nullptr, dither, predivide);
}
bool IMB_colormanagement_setup_glsl_draw_to_scene_linear(const char *from_colorspace_name,
const bool predivide)
{
OCIO_ConstConfigRcPtr *config = OCIO_getCurrentConfig();
global_gpu_state.gpu_shader_bound = OCIO_gpuToSceneLinearShaderBind(
config, from_colorspace_name, predivide);
OCIO_configRelease(config);
return global_gpu_state.gpu_shader_bound;
}
void IMB_colormanagement_finish_glsl_draw()
{
if (global_gpu_state.gpu_shader_bound) {
OCIO_gpuDisplayShaderUnbind();
OCIO_gpuShaderUnbind();
global_gpu_state.gpu_shader_bound = false;
}
}