Some people in the comments pointed out some mistakes in my previous answer. This answer tries to fix all those issues. Thanks for the feedback!
Here is the new code.
I have chosen better images for the experiment this time.
This image has more high-frequency contrast changes.

And this is a extreme case: black and white checker 1-pixel grid.

Now the program uses more correct linear<->srgb conversion formula:
#define SIMPLIFIED_SRGB_FORMULA 0
// https://stackoverflow.com/questions/61138110/what-is-the-correct-gamma-correction-function
static float linearToGamma(float x) {
#if SIMPLIFIED_SRGB_FORMULA
return powf(x, 2.2f);
#else
return x <= 0.0031308f
? x * 12.92f
: powf(x, 1.0f / 2.4f) * 1.055f - 0.055f;
#endif
};
static float gammaToLinear(float x) {
#if SIMPLIFIED_SRGB_FORMULA
return powf(x, 1.f/2.2f);
#else
return x <= 0.04045f
? x / 12.92f
: powf((x + 0.055f) / 1.055f, 2.4f);
#endif
};
The new program generates 4 images and compares them:
- cpu_no_conversion.png: box filtering performed on the CPU, without any gamma conversions
- cpu_conversion.png: box filtering performed on the CPU, converting from gamma to linear, and back from linear to gamma
- gpu_no_conversion.png: let OpenGL create the mipmaps, RGB format
- gpu_conversion.png: let OpenGL create the mipmaps, SRGB format


As you can see in the previous images, there are some visual differences if we do the gamma conversion or not. This is very obvious for the checkers sample image. But noticeable for the tent sample image as well.
OpenGL seems to be doing something similar to our gamma<->linear conversion. So, at least in my system, OpenGL seems to be doing things right.
The program also prints a table with the difference between the generated images.
Meaning of the labels:
- CPU: downscale performed in the CPU
- GPU: downsample computed with OpenGL's glGenerateMipmap
- NG: doesn't do any gamma conversion
- G: converts from gamma space to linear when reading, and back to gamma when writing
For the tent image:
|
CPU, NG |
CPU, G |
GPU, NG |
GPU, G |
CPU, NG |
0 |
2102348 |
49224 |
2121146 |
CPU, G |
2102348 |
0 |
2276882 |
10106 |
GPU, NG |
49224 |
2276882 |
0 |
2295830 |
GPU, G |
2121146 |
10106 |
2295830 |
0 |
For the checker image:
|
CPU, NG |
CPU, G |
GPU, NG |
GPU, G |
CPU, NG |
0 |
176947200 |
0 |
176947200 |
CPU, G |
176947200 |
0 |
176947200 |
0 |
GPU, NG |
0 |
176947200 |
0 |
176947200 |
GPU, G |
176947200 |
0 |
176947200 |
0 |
For the checkers sample image, OpenGL perfectly matches the output of our CPU box-filtering in all cases (with conversion and without)!
For the tent sample image, it's doesn't 100% match but it's very close.
I performed the tests in Windows, and the RX6600 GPU.