Compositor: Unify sRGB to Linear between CPU and GPU #118624

Merged
Thomas Dinges merged 13 commits from OmarEmaraDev/blender:unify-compositor-srgb-linear into main 2024-03-25 14:10:05 +01:00
5 changed files with 132 additions and 110 deletions

View File

@ -15,6 +15,7 @@
#include "GPU_shader.h"
#include "GPU_texture.h"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
@ -56,74 +57,6 @@ bool operator==(const CachedImageKey &a, const CachedImageKey &b)
* Cached Image.
*/
/* Returns a new texture of the given format and precision preprocessed using the given shader. The
* input texture is freed. */
static GPUTexture *preprocess_texture(Context &context,
GPUTexture *input_texture,
eGPUTextureFormat target_format,
ResultPrecision precision,
const char *shader_name)
{
const int2 size = int2(GPU_texture_width(input_texture), GPU_texture_height(input_texture));
GPUTexture *preprocessed_texture = GPU_texture_create_2d(
"Cached Image", size.x, size.y, 1, target_format, GPU_TEXTURE_USAGE_GENERAL, nullptr);
GPUShader *shader = context.get_shader(shader_name, precision);
GPU_shader_bind(shader);
const int input_unit = GPU_shader_get_sampler_binding(shader, "input_tx");
GPU_texture_bind(input_texture, input_unit);
const int image_unit = GPU_shader_get_sampler_binding(shader, "output_img");
GPU_texture_image_bind(preprocessed_texture, image_unit);
compute_dispatch_threads_at_least(shader, size);
GPU_shader_unbind();
GPU_texture_unbind(input_texture);
GPU_texture_image_unbind(preprocessed_texture);
GPU_texture_free(input_texture);
return preprocessed_texture;
}
/* Compositor images are expected to be always pre-multiplied, so identify if the GPU texture
* returned by the IMB module is straight and needs to be pre-multiplied. An exception is when
* the image has an alpha mode of channel packed or alpha ignore, in which case, we always ignore
* pre-multiplication. */
static bool should_premultiply_alpha(Image *image, ImBuf *image_buffer)
{
if (ELEM(image->alpha_mode, IMA_ALPHA_CHANNEL_PACKED, IMA_ALPHA_IGNORE)) {
return false;
}
return !BKE_image_has_gpu_texture_premultiplied_alpha(image, image_buffer);
}
/* Get a suitable texture format supported by the compositor given the format of the texture
* returned by the IMB module. See imb_gpu_get_format for the formats that needs to be handled. */
static eGPUTextureFormat get_compatible_texture_format(eGPUTextureFormat original_format)
{
switch (original_format) {
case GPU_R16F:
case GPU_R32F:
case GPU_RGBA16F:
case GPU_RGBA32F:
return original_format;
case GPU_R8:
return GPU_R16F;
case GPU_RGBA8:
case GPU_SRGB8_A8:
return GPU_RGBA16F;
default:
break;
}
BLI_assert_unreachable();
return original_format;
}
/* Get the selected render layer selected assuming the image is a multilayer image. */
static RenderLayer *get_render_layer(Image *image, ImageUser &image_user)
{
@ -205,6 +138,56 @@ static ImageUser compute_image_user_for_pass(Context &context,
return image_user_for_pass;
}
/* The image buffer might be stored as an sRGB 8-bit image, while the compositor expects linear
* float images, so compute a linear float buffer for the image buffer. This will also do linear
* space conversion and alpha pre-multiplication as needed. We could store those images in sRGB GPU
* textures and let the GPU do the linear space conversion, but the issues is that we don't control
* how the GPU does the conversion and so we get tiny differences across CPU and GPU compositing,
* and potentially even across GPUs/Drivers. Further, if alpha pre-multiplication is needed, we
* would need to do it ourself, which means alpha pre-multiplication will happen before linear
* space conversion, which would produce yet another difference. So we just do everything on the
* CPU, since this is already a cached resource.
*
* To avoid conflicts with other threads, create a new image buffer and assign all the necessary
* information to it, with IB_DO_NOT_TAKE_OWNERSHIP for buffers since a deep copy is not needed.
*
* The caller should free the returned image buffer. */
static ImBuf *compute_linear_buffer(ImBuf *image_buffer)
{
/* Do not pass the flags to the allocation function to avoid buffer allocation, but assign them
* after to retain important information like precision and alpha mode. */
ImBuf *linear_image_buffer = IMB_allocImBuf(
image_buffer->x, image_buffer->y, image_buffer->planes, 0);
linear_image_buffer->flags = image_buffer->flags;
/* Assign the float buffer if it exists, as well as its number of channels. */
IMB_assign_float_buffer(
linear_image_buffer, image_buffer->float_buffer, IB_DO_NOT_TAKE_OWNERSHIP);
linear_image_buffer->channels = image_buffer->channels;
/* If no float buffer exists, assign it then compute a float buffer from it. This is the main
* call of this function. */
OmarEmaraDev marked this conversation as resolved
Review

Should this become

if (image_buffer->ftype == IMB_FTYPE_DDS) {
  ...
} else if (!linear_image_buffer->float_buffer.data) {
  ...
}

Otherwise it is not very clear to me why do we need both float buffer and DDS.

Should this become ``` if (image_buffer->ftype == IMB_FTYPE_DDS) { ... } else if (!linear_image_buffer->float_buffer.data) { ... } ``` Otherwise it is not very clear to me why do we need both float buffer and DDS.
Review

@Sergey That's because some code exists that can fallback to the float buffer if the DDS data wasn't compatible for some reason. I asked about that in chat last week and Aras confirmed it.

@Sergey That's because some code exists that can fallback to the float buffer if the DDS data wasn't compatible for some reason. I asked about that in chat last week and Aras confirmed it.
Review

I see. But if we have have float buffer which we can just use, what is the point of using DDS? Is it handling mipmaps?
Also, is DDS guaranteed to be in scene linear space?

I see. But if we have have float buffer which we can just use, what is the point of using DDS? Is it handling mipmaps? Also, is DDS guaranteed to be in scene linear space?
Review

@Sergey I just assumed it would be faster to load and consume less memory, beside potentially having mipmaps.

Generally, DDS can be in sRGB it seems, but that doesn't seem to be handled by Blender, since it seems to be only supported in a specific DDS type.

However, it seems compressed textures are not yet supported in Metal, so it looks like we will have to intentionally disable it for now at least.

@Sergey I just assumed it would be faster to load and consume less memory, beside potentially having mipmaps. Generally, DDS can be in sRGB it seems, but that doesn't seem to be handled by Blender, since it seems to be only supported in a specific DDS type. However, it seems compressed textures are not yet supported in Metal, so it looks like we will have to intentionally disable it for now at least.
Review

@Sergey I added suitable conditions to keep DDS support when possible.

@Sergey I added suitable conditions to keep DDS support when possible.
Review

Do we really need to check for Metal here?
We can always assign shared DDS data to an imbuf, and if a backend does not use it, it is the backend's choice. Maybe it'll get supported later, and then you wouldn't need to hunt for all cases where check is done in the parent call.

Do we really need to check for Metal here? We can always assign shared DDS data to an imbuf, and if a backend does not use it, it is the backend's choice. Maybe it'll get supported later, and then you wouldn't need to hunt for all cases where check is done in the parent call.
Review

@Sergey The problem is that GPU texture creation is not a back-end specific thing. But I now realize this is not an issue in this patch, but a general issue in Blender. I talked to Clement and will submit a patch to fix this for the Metal back-end.

@Sergey The problem is that GPU texture creation is not a back-end specific thing. But I now realize this is not an issue in this patch, but a general issue in Blender. I talked to Clement and will submit a patch to fix this for the Metal back-end.
if (!linear_image_buffer->float_buffer.data) {
IMB_assign_byte_buffer(
linear_image_buffer, image_buffer->byte_buffer, IB_DO_NOT_TAKE_OWNERSHIP);
IMB_float_from_rect(linear_image_buffer);
}
/* If the image buffer contained compressed data, assign them as well, but only if the color
* space of the buffer is linear or data, since we need linear data and can't preprocess the
* compressed buffer. If not, we fallback to the float buffer already assigned, which is
* guaranteed to exist as a fallback for compressed textures. */
const bool is_suitable_compressed_color_space =
IMB_colormanagement_space_is_data(image_buffer->byte_buffer.colorspace) ||
IMB_colormanagement_space_is_scene_linear(image_buffer->byte_buffer.colorspace);
if (image_buffer->ftype == IMB_FTYPE_DDS && is_suitable_compressed_color_space) {
linear_image_buffer->ftype = IMB_FTYPE_DDS;
IMB_assign_dds_data(linear_image_buffer, image_buffer->dds_data, IB_DO_NOT_TAKE_OWNERSHIP);
}
return linear_image_buffer;
}
CachedImage::CachedImage(Context &context,
Image *image,
ImageUser *image_user,
@ -227,34 +210,12 @@ CachedImage::CachedImage(Context &context,
context, image, image_user, pass_name);
ImBuf *image_buffer = BKE_image_acquire_ibuf(image, &image_user_for_pass, nullptr);
const bool is_premultiplied = BKE_image_has_gpu_texture_premultiplied_alpha(image, image_buffer);
texture_ = IMB_create_gpu_texture("Image Texture", image_buffer, true, is_premultiplied);
ImBuf *linear_image_buffer = compute_linear_buffer(image_buffer);
texture_ = IMB_create_gpu_texture("Image Texture", linear_image_buffer, true, true);
GPU_texture_update_mipmap_chain(texture_);
const eGPUTextureFormat original_format = GPU_texture_format(texture_);
const eGPUTextureFormat target_format = get_compatible_texture_format(original_format);
const ResultType result_type = Result::type(target_format);
const ResultPrecision precision = Result::precision(target_format);
/* The GPU image returned by the IMB module can be in a format not supported by the compositor,
* or it might need pre-multiplication, so preprocess them first. */
if (result_type == ResultType::Color && should_premultiply_alpha(image, image_buffer)) {
texture_ = preprocess_texture(
context, texture_, target_format, precision, "compositor_premultiply_alpha");
}
else if (original_format != target_format) {
const char *conversion_shader_name = result_type == ResultType::Float ?
"compositor_convert_float_to_float" :
"compositor_convert_color_to_color";
texture_ = preprocess_texture(
context, texture_, target_format, precision, conversion_shader_name);
}
/* Set the alpha to 1 using swizzling if alpha is ignored. */
if (result_type == ResultType::Color && image->alpha_mode == IMA_ALPHA_IGNORE) {
GPU_texture_swizzle_set(texture_, "rgb1");
}
IMB_freeImBuf(linear_image_buffer);
BKE_image_release_ibuf(image, image_buffer, nullptr);
}

View File

@ -118,6 +118,21 @@ ImBuf *IMB_allocFromBuffer(const uint8_t *byte_buffer,
void IMB_assign_byte_buffer(ImBuf *ibuf, uint8_t *buffer_data, ImBufOwnership ownership);
void IMB_assign_float_buffer(ImBuf *ibuf, float *buffer_data, ImBufOwnership ownership);
OmarEmaraDev marked this conversation as resolved Outdated

I wouldn't make it public function, and instead have imb_assign_encoded_buffer next to the imb_addencodedbufferImBuf in the IMB_allocimbuf.h

I wouldn't make it public function, and instead have `imb_assign_encoded_buffer` next to the `imb_addencodedbufferImBuf` in the `IMB_allocimbuf.h`
/**
* Assign the content and the color space of the corresponding buffer the data from the given
* buffer.
*
* \note Does not modify the topology (width, height, number of channels)
* or the mipmaps in any way.
*
* \note The ownership of the data in the source buffer is ignored.
*/
void IMB_assign_byte_buffer(ImBuf *ibuf, const ImBufByteBuffer &buffer, ImBufOwnership ownership);
void IMB_assign_float_buffer(ImBuf *ibuf,
const ImBufFloatBuffer &buffer,
ImBufOwnership ownership);
void IMB_assign_dds_data(ImBuf *ibuf, const DDSData &data, ImBufOwnership ownership);
/**
* Make corresponding buffers available for modification.
* Is achieved by ensuring that the given ImBuf is the only owner of the underlying buffer data.

View File

@ -34,17 +34,6 @@ struct IDProperty;
#define IMB_MIPMAP_LEVELS 20
#define IMB_FILEPATH_SIZE 1024
struct DDSData {
/** DDS fourcc info */
unsigned int fourcc;
/** The number of mipmaps in the dds file */
unsigned int nummipmaps;
/** The compressed image data */
unsigned char *data;
/** The size of the compressed data */
unsigned int size;
};
/**
* \ingroup imbuf
* This is the abstraction of an image. ImBuf is the basic type used for all imbuf operations.
@ -143,6 +132,19 @@ enum ImBufOwnership {
IB_TAKE_OWNERSHIP = 1,
};
struct DDSData {
/** DDS fourcc info */
unsigned int fourcc;
/** The number of mipmaps in the dds file */
unsigned int nummipmaps;
/** The compressed image data */
unsigned char *data;
/** The size of the compressed data */
unsigned int size;
/** Who owns the data buffer. */
ImBufOwnership ownership;
};
/* Different storage specialization.
*
* NOTE: Avoid direct assignments and allocations, use the buffer utilities from the IMB_imbuf.hh

View File

@ -84,6 +84,27 @@ template<class BufferType> static void imb_free_buffer(BufferType &buffer)
buffer.ownership = IB_DO_NOT_TAKE_OWNERSHIP;
}
/* Free the specified DDS buffer storage, freeing memory when needed and restoring the state of the
* buffer to its defaults. */
static void imb_free_dds_buffer(DDSData &dds_data)
{
if (dds_data.data) {
switch (dds_data.ownership) {
case IB_DO_NOT_TAKE_OWNERSHIP:
break;
case IB_TAKE_OWNERSHIP:
/* dds_data.data is allocated by DirectDrawSurface::readData(), so don't use MEM_freeN! */
free(dds_data.data);
break;
}
}
/* Reset buffer to defaults. */
dds_data.data = nullptr;
dds_data.ownership = IB_DO_NOT_TAKE_OWNERSHIP;
}
/* Allocate pixel storage of the given buffer. The buffer owns the allocated memory.
* Returns true of allocation succeeded, false otherwise. */
template<class BufferType>
@ -249,11 +270,7 @@ void IMB_freeImBuf(ImBuf *ibuf)
IMB_free_gpu_textures(ibuf);
IMB_metadata_free(ibuf->metadata);
colormanage_cache_free(ibuf);
if (ibuf->dds_data.data != nullptr) {
/* dds_data.data is allocated by DirectDrawSurface::readData(), so don't use MEM_freeN! */
free(ibuf->dds_data.data);
}
imb_free_dds_buffer(ibuf->dds_data);
MEM_freeN(ibuf);
}
}
@ -472,6 +489,32 @@ void IMB_assign_float_buffer(ImBuf *ibuf, float *buffer_data, const ImBufOwnersh
}
}
void IMB_assign_byte_buffer(ImBuf *ibuf,
const ImBufByteBuffer &buffer,
const ImBufOwnership ownership)
{
IMB_assign_byte_buffer(ibuf, buffer.data, ownership);
ibuf->byte_buffer.colorspace = buffer.colorspace;
}
void IMB_assign_float_buffer(ImBuf *ibuf,
const ImBufFloatBuffer &buffer,
const ImBufOwnership ownership)
{
IMB_assign_float_buffer(ibuf, buffer.data, ownership);
ibuf->float_buffer.colorspace = buffer.colorspace;
}
void IMB_assign_dds_data(ImBuf *ibuf, const DDSData &data, const ImBufOwnership ownership)
{
BLI_assert(ibuf->ftype == IMB_FTYPE_DDS);
imb_free_dds_buffer(ibuf->dds_data);
ibuf->dds_data = data;
ibuf->dds_data.ownership = ownership;
}
ImBuf *IMB_allocFromBufferOwn(
uint8_t *byte_buffer, float *float_buffer, uint w, uint h, uint channels)
{

View File

@ -330,6 +330,7 @@ static void LoadDXTCImage(ImBuf *ibuf, Filesystem::IOMemReader &mem_reader)
ibuf->dds_data.size = mem_reader.size() - dds_header_size;
ibuf->dds_data.data = (uchar *)malloc(ibuf->dds_data.size);
mem_reader.pread(ibuf->dds_data.data, ibuf->dds_data.size, dds_header_size);
ibuf->dds_data.ownership = IB_TAKE_OWNERSHIP;
/* Flip compressed image data to match OpenGL convention. */
FlipDXTCImage(ibuf);