Gamma workflow

I was observing some shader presets and some of them linearize at the start. Is it good habit for libretro shaders to linearize everything and reencode gamma to output? Or is it better to deal this separately in a single linearization shader pass?

Also I observed most if not all use a pure power curve, I researched a bit and learned that CRTs used a pure power curve while LCDs use a sRGB piece wise power function.

Shouldn’t the output gamma be sRGB gamma? I found some shaders that did this like “misc\ntsc-colors.glsl”. I might rework some of my shaders to be gamma aware.

1 Like

Doing it at the start and then reencoding later is purely for performance and convenience. That is, you linearize once and then use an srgb/float framebuffer instead of doing it at the start and end of each shader pass.

using the pow(image, 2.2) is just an approximation. Some people will get even more approximate and use image*image and then sqrt(image). There are more exact linearizations, like the one used in crt-lottes, but it doesn’t usually make that much of a difference vs performance impact.

3 Likes

woow, so great, didn’t know that trick, the helps check what’s going on. Yes the curve approximates better to sRGB but still crushes some blacks. I always use:

color <= 0.04045 ? (pow(color, 1.0) / 12.92) : pow((color + 0.055) / (1.0 + 0.055), gamma)

So as I see there’s no actual libretro recommendation towards the gamma pipeline. I was inspecting lut.glsl, normally first in the stack, which didn’t have any gamma operation that I could see. Is that related to the preset parameter “SamplerLUT1_linear = true”, I’m not sure if it’s correct, Hald CLUTs shouldn’t be linear?

That’s referring to the sample mode, either nearest neighbor or bilinear.

As i read the sRGB framebuffer consists of a format, which uses 8 bits per component. So a proper conversion is a must (as described above). But this can lead to a lack of dynamics when using different values for input/output gamma. It can also mean the gamma conversion is not a continuons function with a non-standard gamma value.

With most of the CRT shaders some sort of brightboost is applied to compensate for scanlines and mask brightness loss, resulting in better visible dark pixels, or parts of it. :wink:

1 Like

Yes, that’s right, a float framebuffer should be preferred to overcome quantization, specially on heavy filtering like the grade ubershader I put together.

I’m not aware of the shortcomings gamma has for scanlines but for color typically that is done in linear light, mixing colors in gamma encoded space results in change of tones.

I know many optimization techniques are applied in shaders for real time performance, but I want to experiment with more accuracy in initial color passes.

EDIT: By the way, is there a consensus on what gamma CRTs operated? I often see 2.50 or 2.25 in the shaders.

2 Likes

I think I will stick with a gamma of 2.4, apparently this is the mean gamma value of a consumer reference CRT. https://floyd.lbl.gov/radiance/refer/Notes/gamma.html

2 Likes

Ok, so it took me a while (confusing terms). I will post here as a reminder. float_framebuffer is for float precision, while sRGB_framebuffer is just the gamma transfer function. Is this right? I was fooled by “sRGB” because to me it generally means a color space with its primaries and gamut and not only a transfer function, some people use the term for the byte format. Does it use the piece-wise function?

It’s just a texture format. Scroll down to #pragma format in the readme here to see the available formats: https://github.com/libretro/slang-shaders

Default is R8G8B8A8_UNORM and srgb_framebuffer gives you R8G8B8A8_SRGB. I believe float_framebuffer gives you R16G16B16A16_SFLOAT. Khronos decided that sRGB conversion can happen in hardware for 8-bit values but takes too much calculation for higher depth, which is why the _SRGB format only exists for 8-bit textures.

2 Likes

@hunterk I’m not following all of this super well but I have a question semi-related to the topic.

Is there a reason ntsc-composite uses R8G8B8A8_SRGB, but ntsc-svideo doesn’t? (Sorry if there’s a super obvious reason for this, this is so far above my head it could be a plane, lol.)

Also would it be an issue if I didn’t use the R8G8B8A8_SRGB in ntsc-composite or if I used the R8G8B8A8_SRGB in ntsc-svideo?

EDIT: This only seems to be the case with the glsl version of this it seems (maybe overlooked it, don’t think I did though), I’m not seeing it in the slang version… Love to hear your thoughts.

Can you link the relevant bits on github? I’m not seeing what you’re talking about.

However, I did notice that the ntsc-svideo.glslp preset used (and always had, apparently) the composite pass instead of the svideo one :open_mouth:

I just pushed a fix for that.

1 Like

I’m not sure how to do that exactly but here’s a code block from ntsc-pass1-composite-2phase. (This is the only one that does it, neither of the 3phase pass1 do this.)

// vertex compatibility #defines
#define vTexCoord TEX0.xy
#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
#define outsize vec4(OutputSize, 1.0 / OutputSize)

void main()
{
    gl_Position = MVPMatrix * VertexCoord;
    COL0 = COLOR;
    TEX0.xy = TexCoord.xy;
   pix_no = vTexCoord * SourceSize.xy * (outsize.xy / InputSize.xy);
}

#elif defined(FRAGMENT)
#pragma format R8G8B8A8_SRGB

#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

And here’s the same section but from ntsc-pass1-svideo-2phase


// vertex compatibility #defines
#define vTexCoord TEX0.xy
#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
#define outsize vec4(OutputSize, 1.0 / OutputSize)

void main()
{
    gl_Position = MVPMatrix * VertexCoord;
    COL0 = COLOR;
    TEX0.xy = TexCoord.xy;
   pix_no = vTexCoord * SourceSize.xy * (outsize.xy / InputSize.xy);
}

#elif defined(FRAGMENT)

#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

This is for the glsl versions, in the slang versions neither of these shaders use this: (At least from what I can see.

#pragma format R8G8B8A8_SRGB

Ah, GLSL shaders don’t even know what to do with the #pragma format statements, so it just gets ignored anyway. You could replace it with #pragma dickbutt and it would act the same :stuck_out_tongue:

1 Like

Ahh, that’s interesting. So basically it’s a pointless line in this case.

I have a code related question do you want me to ask you here or PM you?