Rakashazi / emu-ex-plus-alpha

Multi-platform computer & game console emulation system including supporting code (EmuFramework) and core engine (Imagine)
GNU General Public License v3.0
600 stars 145 forks source link

Linear interpolation is not gamma corrected #208

Closed M-a-r-k closed 2 years ago

M-a-r-k commented 3 years ago

Currently (at least for my testing on several Android phones), when linear interpolation is enabled parts of the resulting image appear too dark. Intermediate shades between two colors are darker than they should be.

The reason is that (for example) the grey with brightness half-way between black and white actually has value ~187 not 128 in SRGB images.

For example run 240p Test Suite in Snes9x EX and select Checkerboard. Enable & disable linear interpolation and notice the change in overall screen brightness.

The SRGB source image is probably not being linearized before filtering.

It may possible for that to be done in hardware when the texture is sampled and back again when the framebuffer is written. (Probably varies by OpenGL and driver version.)

But if not a simple shader could perform both gamma correction and bilinear filtering (and also prescaling).

The shader would take each RGB component value (float between 0 and 1.0) and raise to power 2.2. Then bilinear filter and convert back to SRGB by raising each component value in the destination texture by power 1/2.2. (If you tell OpenGL the framebuffer is SRGB it might do that automatically).

A faster approximation could use power 2, or especially if doing it on the CPU, a pre-generated lookup table.

Some info about SRGB in OpenGL at https://www.g-truc.net/post-0263.html

Rakashazi commented 3 years ago

Thanks for the report and link. The post is mostly referencing desktop OpenGL but it looks like it's possible to request an SRGB framebuffer in OpenGL ES when using EGL and creating the window surface if the device supports the KHR_gl_colorspace extension. Assuming this extension works properly on my test devices I'll try to fit this in for the next update.

M-a-r-k commented 3 years ago

I suspect that hardware/driver support will be a bit of a lottery.

To work around that you could add code to the shader to linearize before interpolating (and back afterwards if hardware doesn't support that either). Something like https://github.com/Themaister/slang-shaders/tree/master/linear (That shader uses gamma 2.0 for performance reasons. But that's probably good enough anyway.)

Some related links:

Friends don’t let friends do resampling in gamma space: https://petewarden.com/2008/08/02/why-fonts-need/

http://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/

https://developer.nvidia.com/gpugems/gpugems3/part-iv-image-effects/chapter-24-importance-being-linear

Computer Color is Broken: https://www.youtube.com/watch?v=LKnqECcg6Gw

Rakashazi commented 3 years ago

Thanks for the additional links. I also found the extension EXT_sRGB_write_control gives the equivalent glEnable(GL_FRAMEBUFFER_SRGB) functionality in OpenGL ES and should be supported on recent devices. I tested it on a Pixel phone and it works as expected, so I'm starting with this approach rather than shaders since it will be virtually zero overhead.

In the last commit I was updating the OpenGL support code so I also hooked up support for SRGB framebuffers, textures, and the above extension. There's also a new video option in the emulators to enable these SRGB extensions and provide the gamma corrected bilinear filtering you mentioned. The last piece is to update all the emulators to render to a RGBA8888 texture needed for SRGB use since most of them only render RGB565. Currently GBC.emu and Snes9x EX do use RGBA8888 so this option will have an effect when enabled on them. Give it a try when the builds are out and let me know how it works on your devices.

M-a-r-k commented 3 years ago

Great, will do. For testing purposes, the ability to map a controller button to toggle gamma-correct filtering would be handy.

I was reading about EXT_texture_sRGB. Maybe what follows only applies to old hardware but anyway...

For Nvidia, GeForce 8 was the first generation to support sRGB conversion pre-filtering. EXT_texture_sRGB does not mandate that which is quite annoying.

From the Nvidia link above: "On NVIDIA GeForce 8-class (and future) hardware, all samples used in a texture filtering operation are linearized before filtering to ensure the proper filtering is performed (older GPUs apply the gamma post-filtering)."

From the OpenGL EXT_texture_sRGB spec (https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_sRGB.txt):

"Core pixel maps and ARB_imaging provides sufficient color tables so that applications interested in managing color space conversions within the pixel path can do so themselves. A 256 entry table outputting floating-point values is sufficient to convert sRGB to linear RGB."

[I guess that would be the pre-shader-capable hardware way of doing it? Perhaps lower-end hardware could benefit from that method?]

"Should this extension mandate that sRGB conversion be performed pre-filtering?

RESOLVED: Post-filtering sRGB color conversion is allowed though pre-filtering conversion is the preferred approach.

Ideally, sRGB conversion moves from the non-linear sRGB to the linear RGB color space. However, implementations should be provided leeway as to whether sRGB conversion occurs before or after texture filtering of RGB components. ... NVIDIA Implementation Details GeForce FX, Quadro FX, and GeForce 6 and 7 Series GPUs store sRGB texels at 8 bits per component. sRGB conversion occurs post-filtering."

Rakashazi commented 3 years ago

Thanks for the additional info, my main focus for desktop OpenGL is 3.3-capable hardware and Geforce 8 is actually the minimum for that on the Nvidia side. The last commit adds general RGBA8888 texture support so all emus work with the SRGB mode now. There's isn't a button toggle atm but I'll look into adding some additional bindings for video options next time I work on the core input code.

M-a-r-k commented 2 years ago

I tested the new sRGB support on four Android phones. One is modern(ish), the others all older. Device specs below.

Only the P Smart 2019 allows SRGB to be selected. With the others, it's not a case of being able to choose SRGB and there being no visual difference. There's just no SRGB option in the GUI.

SRGB seems to work fine on the P Smart phone. Easily noticeable on games which outline sprites & text in black, e.g. Super Mario World. Outlines no longer appear thicker/bolder when linear filtering is used with SRGB.

I wonder, is there any alternative way to support SRGB on the older devices, other than using a shader? Might some hardware actually support it, but whatever method you use to check for support doesn't work there?

Links which may or may not be relevant:

https://community.khronos.org/t/srgb-default-framebuffers-unreliable-feature/106024

https://stackoverflow.com/questions/20396523/android-egl-srgb-default-renderbuffer

https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt

Specs of the phones I tested:

Huawei P Smart 2019 (Android 10, Kirin 710, Mali-G51, OpenGL ES 3.2): yes supports SRGB.

Alcatel Pixi 3 4.5 (Android 5.1, MT6735, Mali-T720, OpenGL ES 3.0): no SRGB

BUSH Spira D5 dual camera (Android 7.0, MT6735, Mali-T720, OpenGL ES 3.1): no SRGB

Huawei G620S (Android 4.4.4, Snapdragon 400/410, Adreno 306, OpenGL ES 3.0): no SRGB

M-a-r-k commented 2 years ago

One more point. On the P Smart 2019 phone there is a significant performance decrease when SRGB is enabled.

Using Snes9x-EX+ and 240p Test Suite I selected Grid Scroll Test. Smooth scrolling without SRGB, juddery scrolling with.

Surely the ARM driver developers aren't doing it in software? Or perhaps their SRGB code path involves extra copies?

github-actions[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity.