From 2ab6a819aa62c2c6a5ebba325be30a0918c94566 Mon Sep 17 00:00:00 2001 From: Matthieu Bouron Date: Tue, 15 Jul 2025 14:33:23 +0200 Subject: [PATCH 1/5] ngpu: add more sRGB formats Adds more sRGB formats for internal use. --- libnopegl/src/ngpu/format.h | 3 +++ libnopegl/src/ngpu/opengl/format_gl.c | 3 +++ libnopegl/src/ngpu/opengl/glincludes.h | 4 ++++ libnopegl/src/ngpu/vulkan/format_vk.c | 3 +++ 4 files changed, 13 insertions(+) diff --git a/libnopegl/src/ngpu/format.h b/libnopegl/src/ngpu/format.h index c2a76e8cc..bb3156211 100644 --- a/libnopegl/src/ngpu/format.h +++ b/libnopegl/src/ngpu/format.h @@ -31,10 +31,12 @@ enum ngpu_format { NGPU_FORMAT_R8_SNORM, NGPU_FORMAT_R8_UINT, NGPU_FORMAT_R8_SINT, + NGPU_FORMAT_R8_SRGB, NGPU_FORMAT_R8G8_UNORM, NGPU_FORMAT_R8G8_SNORM, NGPU_FORMAT_R8G8_UINT, NGPU_FORMAT_R8G8_SINT, + NGPU_FORMAT_R8G8_SRGB, NGPU_FORMAT_R8G8B8_UNORM, NGPU_FORMAT_R8G8B8_SNORM, NGPU_FORMAT_R8G8B8_UINT, @@ -49,6 +51,7 @@ enum ngpu_format { NGPU_FORMAT_B8G8R8A8_SNORM, NGPU_FORMAT_B8G8R8A8_UINT, NGPU_FORMAT_B8G8R8A8_SINT, + NGPU_FORMAT_B8G8R8A8_SRGB, NGPU_FORMAT_R16_UNORM, NGPU_FORMAT_R16_SNORM, NGPU_FORMAT_R16_UINT, diff --git a/libnopegl/src/ngpu/opengl/format_gl.c b/libnopegl/src/ngpu/opengl/format_gl.c index be0b53c9e..b9af9a2e3 100644 --- a/libnopegl/src/ngpu/opengl/format_gl.c +++ b/libnopegl/src/ngpu/opengl/format_gl.c @@ -41,11 +41,13 @@ static const struct ngpu_format_gl formats[NGPU_FORMAT_NB] = { [NGPU_FORMAT_R8_SNORM] = {GL_RED, GL_R8_SNORM, GL_BYTE, S|SL|B}, [NGPU_FORMAT_R8_UINT] = {GL_RED_INTEGER, GL_R8UI, GL_UNSIGNED_BYTE, S|C}, [NGPU_FORMAT_R8_SINT] = {GL_RED_INTEGER, GL_R8I, GL_BYTE, S|C}, + [NGPU_FORMAT_R8_SRGB] = {GL_RED, GL_SR8, GL_BYTE, S|SL}, [NGPU_FORMAT_R8G8_UNORM] = {GL_RG, GL_RG8, GL_UNSIGNED_BYTE, S|SL|C|B}, [NGPU_FORMAT_R8G8_SNORM] = {GL_RG, GL_RG8_SNORM, GL_BYTE, S|SL|B}, [NGPU_FORMAT_R8G8_UINT] = {GL_RG_INTEGER, GL_RG8UI, GL_UNSIGNED_BYTE, S|C}, [NGPU_FORMAT_R8G8_SINT] = {GL_RG_INTEGER, GL_RG8I, GL_BYTE, S|C}, + [NGPU_FORMAT_R8G8_SRGB] = {GL_RG, GL_SRG8, GL_BYTE, S|SL}, [NGPU_FORMAT_R8G8B8_UNORM] = {GL_RGB, GL_RGB8, GL_UNSIGNED_BYTE, S|SL|C|B}, [NGPU_FORMAT_R8G8B8_SNORM] = {GL_RGB, GL_RGB8_SNORM, GL_BYTE, S|SL}, @@ -63,6 +65,7 @@ static const struct ngpu_format_gl formats[NGPU_FORMAT_NB] = { [NGPU_FORMAT_B8G8R8A8_SNORM] = {GL_BGRA, GL_RGBA8_SNORM, GL_BYTE, 0}, [NGPU_FORMAT_B8G8R8A8_UINT] = {GL_BGRA_INTEGER, GL_RGBA8UI, GL_UNSIGNED_BYTE, 0}, [NGPU_FORMAT_B8G8R8A8_SINT] = {GL_BGRA_INTEGER, GL_RGBA8I, GL_BYTE, 0}, + [NGPU_FORMAT_B8G8R8A8_SRGB] = {GL_BGRA, GL_SRGB8_ALPHA8,GL_BYTE, 0}, [NGPU_FORMAT_R16_UNORM] = {GL_RED, GL_R16, GL_UNSIGNED_SHORT, 0}, [NGPU_FORMAT_R16_SNORM] = {GL_RED, GL_R16_SNORM, GL_SHORT, 0}, diff --git a/libnopegl/src/ngpu/opengl/glincludes.h b/libnopegl/src/ngpu/opengl/glincludes.h index 3ab5c9043..babf550ad 100644 --- a/libnopegl/src/ngpu/opengl/glincludes.h +++ b/libnopegl/src/ngpu/opengl/glincludes.h @@ -154,4 +154,8 @@ typedef void (NGLI_GL_APIENTRY *GLDEBUGPROC)(GLenum source, GLenum type, GLuint # define GL_DYNAMIC_STORAGE_BIT 0x0100 # define GL_CLIENT_STORAGE_BIT 0x0200 +/* sRGB */ +# define GL_SR8 0x8FBD +# define GL_SRG8 0x8FBE + #endif /* GLINCLUDES_H */ diff --git a/libnopegl/src/ngpu/vulkan/format_vk.c b/libnopegl/src/ngpu/vulkan/format_vk.c index 8650fed64..5e34f8c76 100644 --- a/libnopegl/src/ngpu/vulkan/format_vk.c +++ b/libnopegl/src/ngpu/vulkan/format_vk.c @@ -32,10 +32,12 @@ VkFormat ngpu_format_ngl_to_vk(enum ngpu_format format) [NGPU_FORMAT_R8_SNORM] = VK_FORMAT_R8_SNORM, [NGPU_FORMAT_R8_UINT] = VK_FORMAT_R8_UINT, [NGPU_FORMAT_R8_SINT] = VK_FORMAT_R8_SINT, + [NGPU_FORMAT_R8_SRGB] = VK_FORMAT_R8_SRGB, [NGPU_FORMAT_R8G8_UNORM] = VK_FORMAT_R8G8_UNORM, [NGPU_FORMAT_R8G8_SNORM] = VK_FORMAT_R8G8_SNORM, [NGPU_FORMAT_R8G8_UINT] = VK_FORMAT_R8G8_UINT, [NGPU_FORMAT_R8G8_SINT] = VK_FORMAT_R8G8_SINT, + [NGPU_FORMAT_R8G8_SRGB] = VK_FORMAT_R8G8_SRGB, [NGPU_FORMAT_R8G8B8_UNORM] = VK_FORMAT_R8G8B8_UNORM, [NGPU_FORMAT_R8G8B8_SNORM] = VK_FORMAT_R8G8B8_SNORM, [NGPU_FORMAT_R8G8B8_UINT] = VK_FORMAT_R8G8B8_UINT, @@ -50,6 +52,7 @@ VkFormat ngpu_format_ngl_to_vk(enum ngpu_format format) [NGPU_FORMAT_B8G8R8A8_SNORM] = VK_FORMAT_B8G8R8A8_SNORM, [NGPU_FORMAT_B8G8R8A8_UINT] = VK_FORMAT_B8G8R8A8_UINT, [NGPU_FORMAT_B8G8R8A8_SINT] = VK_FORMAT_B8G8R8A8_SINT, + [NGPU_FORMAT_B8G8R8A8_SRGB] = VK_FORMAT_B8G8R8A8_SRGB, [NGPU_FORMAT_R16_UNORM] = VK_FORMAT_R16_UNORM, [NGPU_FORMAT_R16_SNORM] = VK_FORMAT_R16_SNORM, [NGPU_FORMAT_R16_UINT] = VK_FORMAT_R16_UINT, From 2f0f193c51ea6d4ae4962d277f00ff975faa045e Mon Sep 17 00:00:00 2001 From: Matthieu Bouron Date: Wed, 16 Jul 2025 10:50:09 +0200 Subject: [PATCH 2/5] ngpu/gl: enable sRGB framebuffer conversions by default on OpenGL sRGB framebuffer conversions are automatically enabled on OpenGLES. --- libnopegl/src/ngpu/opengl/glcontext.c | 4 +++- libnopegl/src/ngpu/opengl/glincludes.h | 1 + libnopegl/src/ngpu/opengl/glstate.c | 4 ++++ libnopegl/src/ngpu/opengl/rendertarget_gl.c | 17 +++++++++++++++++ .../pynopegl_utils/qml/ngl_widget.py | 6 ++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/libnopegl/src/ngpu/opengl/glcontext.c b/libnopegl/src/ngpu/opengl/glcontext.c index c3946074d..bf10d8f76 100644 --- a/libnopegl/src/ngpu/opengl/glcontext.c +++ b/libnopegl/src/ngpu/opengl/glcontext.c @@ -510,8 +510,10 @@ struct glcontext *ngli_glcontext_create(const struct glcontext_params *params) if (ret < 0) goto fail; - if (glcontext->backend == NGL_BACKEND_OPENGL) + if (glcontext->backend == NGL_BACKEND_OPENGL) { glcontext->funcs.Enable(GL_TEXTURE_CUBE_MAP_SEAMLESS); + glcontext->funcs.Enable(GL_FRAMEBUFFER_SRGB); + } if (!glcontext->external && !glcontext->offscreen) { ret = ngli_glcontext_resize(glcontext, glcontext->width, glcontext->height); diff --git a/libnopegl/src/ngpu/opengl/glincludes.h b/libnopegl/src/ngpu/opengl/glincludes.h index babf550ad..b467386de 100644 --- a/libnopegl/src/ngpu/opengl/glincludes.h +++ b/libnopegl/src/ngpu/opengl/glincludes.h @@ -155,6 +155,7 @@ typedef void (NGLI_GL_APIENTRY *GLDEBUGPROC)(GLenum source, GLenum type, GLuint # define GL_CLIENT_STORAGE_BIT 0x0200 /* sRGB */ +# define GL_FRAMEBUFFER_SRGB 0x8DB9 # define GL_SR8 0x8FBD # define GL_SRG8 0x8FBE diff --git a/libnopegl/src/ngpu/opengl/glstate.c b/libnopegl/src/ngpu/opengl/glstate.c index e1fc90aeb..dd61af140 100644 --- a/libnopegl/src/ngpu/opengl/glstate.c +++ b/libnopegl/src/ngpu/opengl/glstate.c @@ -120,6 +120,10 @@ void ngpu_glstate_reset(const struct glcontext *gl, struct ngpu_glstate *glstate { memset(glstate, 0, sizeof(*glstate)); + /* sRGB */ + if (gl->backend == NGL_BACKEND_OPENGL) + gl->funcs.Enable(GL_FRAMEBUFFER_SRGB); + /* Blending */ gl->funcs.Disable(GL_BLEND); glstate->blend = 0; diff --git a/libnopegl/src/ngpu/opengl/rendertarget_gl.c b/libnopegl/src/ngpu/opengl/rendertarget_gl.c index 0372e8e32..5145e9d07 100644 --- a/libnopegl/src/ngpu/opengl/rendertarget_gl.c +++ b/libnopegl/src/ngpu/opengl/rendertarget_gl.c @@ -320,6 +320,17 @@ void ngpu_rendertarget_gl_begin_pass(struct ngpu_rendertarget *s) ngpu_glstate_enable_scissor_test(gl, glstate, GL_FALSE); +#if defined(TARGET_DARWIN) + /* + * The default framebuffer on macOS is sRGB compatible and there is no way + * to request a non-sRGB format, thus we need to disable the linear -> sRGB + * conversion (that is enabled by GL_FRAMEBUFFER_SRGB). + */ + if (s_priv->id == ngli_glcontext_get_default_framebuffer(gl)) { + gl->funcs.Disable(GL_FRAMEBUFFER_SRGB); + } +#endif + gl->funcs.BindFramebuffer(GL_FRAMEBUFFER, s_priv->id); const struct ngpu_viewport viewport = { @@ -364,6 +375,12 @@ void ngpu_rendertarget_gl_end_pass(struct ngpu_rendertarget *s) gl->funcs.BindFramebuffer(GL_FRAMEBUFFER, s_priv->id); } +#if defined(TARGET_DARWIN) + if (s_priv->id == ngli_glcontext_get_default_framebuffer(gl)) { + gl->funcs.Enable(GL_FRAMEBUFFER_SRGB); + } +#endif + s_priv->invalidate(s); } diff --git a/python/pynopegl-utils/pynopegl_utils/qml/ngl_widget.py b/python/pynopegl-utils/pynopegl_utils/qml/ngl_widget.py index d3d7f30d5..d9368d66e 100644 --- a/python/pynopegl-utils/pynopegl_utils/qml/ngl_widget.py +++ b/python/pynopegl-utils/pynopegl_utils/qml/ngl_widget.py @@ -31,6 +31,8 @@ QML_IMPORT_MAJOR_VERSION = 1 QML_IMPORT_MINOR_VERSION = 0 +_GL_FRAMEBUFFER_SRGB = 0x8DB9 + @QmlElement class NopeGLWidget(QQuickFramebufferObject): @@ -142,6 +144,10 @@ def render(self): self._context = None self._request_stop = False + # endExternalCommands() does not reset GL_FRAMEBUFFER_SRGB state + gl_funcs = QOpenGLFunctions(QOpenGLContext.currentContext()) + gl_funcs.glDisable(_GL_FRAMEBUFFER_SRGB) + self._window.endExternalCommands() def synchronize(self, ngl_widget): From 0ee284003620e2e4f3898d8ba35039e92b839185 Mon Sep 17 00:00:00 2001 From: Matthieu Bouron Date: Wed, 16 Jul 2025 11:03:47 +0200 Subject: [PATCH 3/5] ngpu/glcontext_egl: add EGL_KHR_gl_colorspace support Swapchain framebuffer colorspace is left to the default (linear) and is not configurable yet. --- libnopegl/src/ngpu/opengl/glcontext_egl.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/libnopegl/src/ngpu/opengl/glcontext_egl.c b/libnopegl/src/ngpu/opengl/glcontext_egl.c index da2fcd7a9..f12d1f4bb 100644 --- a/libnopegl/src/ngpu/opengl/glcontext_egl.c +++ b/libnopegl/src/ngpu/opengl/glcontext_egl.c @@ -51,6 +51,9 @@ #define EGL_PLATFORM_X11 0x31D5 #define EGL_PLATFORM_WAYLAND 0x31D8 #define EGL_PLATFORM_SURFACELESS_MESA 0x31DD +#define EGL_GL_COLORSPACE_KHR 0x309D +#define EGL_GL_COLORSPACE_SRGB_KHR 0x3089 +#define EGL_GL_COLORSPACE_LINEAR_KHR 0x308A struct egl_priv { EGLNativeDisplayType native_display; @@ -77,6 +80,7 @@ struct egl_priv { int has_device_base_ext; int has_create_context_ext; int has_image_dma_buf_import_modifiers_ext; + int has_gl_colorspace_ext; #if defined(HAVE_WAYLAND) struct wl_egl_window *wl_egl_window; #endif @@ -196,6 +200,10 @@ static int egl_probe_extensions(struct glcontext *ctx) egl->has_image_dma_buf_import_modifiers_ext = 1; } + if (ngli_glcontext_check_extension("EGL_KHR_gl_colorspace", egl->extensions)) { + egl->has_gl_colorspace_ext = 1; + } + return 0; } @@ -507,7 +515,12 @@ try_again:; LOG(ERROR, "could not retrieve EGL native window"); return NGL_ERROR_EXTERNAL; } - egl->surface = eglCreateWindowSurface(egl->display, egl->config, egl->native_window, NULL); + const EGLint surface_attribs[] = { + EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_LINEAR_KHR, + EGL_NONE, + }; + egl->surface = eglCreateWindowSurface(egl->display, egl->config, egl->native_window, + egl->has_surfaceless_context_ext ? surface_attribs : NULL); if (!egl->surface) { LOG(ERROR, "could not create EGL window surface: %s", ngli_eglGetErrorStr()); return NGL_ERROR_EXTERNAL; From 23dfb345ee8293aa65b9cb230781a8bb103867c7 Mon Sep 17 00:00:00 2001 From: Matthieu Bouron Date: Wed, 16 Jul 2025 14:12:32 +0200 Subject: [PATCH 4/5] ngpu/glcontext_wgl: add WGL_EXT_colorspace support Swapchain framebuffer colorspace is left to the default (linear) and is not configurable yet. --- libnopegl/src/ngpu/opengl/glcontext_wgl.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libnopegl/src/ngpu/opengl/glcontext_wgl.c b/libnopegl/src/ngpu/opengl/glcontext_wgl.c index 40b17753e..fc3b01b88 100644 --- a/libnopegl/src/ngpu/opengl/glcontext_wgl.c +++ b/libnopegl/src/ngpu/opengl/glcontext_wgl.c @@ -33,6 +33,10 @@ #include "nopegl.h" #include "log.h" +#define WGL_COLORSPACE_EXT 0x309D +#define WGL_COLORSPACE_SRGB_EXT 0x3089 +#define WGL_COLORSPACE_LINEAR_EXT 0x308A + struct wgl_priv { HWND window; HDC device_context; @@ -42,6 +46,7 @@ struct wgl_priv { PFNWGLCREATECONTEXTATTRIBSARBPROC CreateContextAttribsARB; PFNWGLGETEXTENSIONSSTRINGARBPROC GetExtensionsStringARB; PFNWGLSWAPINTERVALEXTPROC SwapIntervalEXT; + int has_colorspace_ext; }; static int wgl_init_extensions(struct glcontext *ctx) @@ -122,6 +127,11 @@ static int wgl_init_extensions(struct glcontext *ctx) if (!wgl->SwapIntervalEXT) LOG(WARNING, "context does not support any swap interval extension (%lu)", GetLastError()); + const char *extensions_string = wgl->GetExtensionsStringARB(device_context); + if (ngli_glcontext_check_extension("WGL_EXT_colorspace", extensions_string)) { + wgl->has_colorspace_ext = 1; + } + ret = 0; done: @@ -166,6 +176,9 @@ static int wgl_init(struct glcontext *ctx, uintptr_t display, uintptr_t window, return NGL_ERROR_EXTERNAL; } + const int colorspace_attr = wgl->has_colorspace_ext ? WGL_COLORSPACE_EXT : 0; + const int colorspace_value = wgl->has_colorspace_ext ? WGL_COLORSPACE_LINEAR_EXT : 0; + const int pixel_format_attributes[] = { WGL_DRAW_TO_WINDOW_ARB, GL_TRUE, WGL_SUPPORT_OPENGL_ARB, GL_TRUE, @@ -181,6 +194,7 @@ static int wgl_init(struct glcontext *ctx, uintptr_t display, uintptr_t window, WGL_DOUBLE_BUFFER_ARB, GL_TRUE, WGL_SAMPLE_BUFFERS_ARB, ctx->offscreen ? 0 : (ctx->samples > 0), WGL_SAMPLES_ARB, ctx->offscreen ? 0 : (int)ctx->samples, + colorspace_attr, colorspace_value, 0 }; From b3a3f16f7e456eac8586d0588072166a34c1ee8f Mon Sep 17 00:00:00 2001 From: Matthieu Bouron Date: Tue, 23 Sep 2025 14:37:36 +0200 Subject: [PATCH 5/5] tests: add rtt_srgb test Ensures linear->sRGB conversions are performed when using sRGB color formats. This test targets the OpenGL backend which needs to explicitly enable GL_FRAMEBUFFER_SRGB to behave like Vulkan and OpenGLES. --- tests/meson.build | 1 + tests/refs/rtt_srgb.ref | 1 + tests/rtt.py | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 tests/refs/rtt_srgb.ref diff --git a/tests/meson.build b/tests/meson.build index dfea3ee7f..506c05056 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -316,6 +316,7 @@ foreach backend : backends 'clear_attachment_with_timeranges', 'resizable_with_timeranges', 'resizable_with_timeranges_implicit', + 'srgb', ] if max_samples >= 4 diff --git a/tests/refs/rtt_srgb.ref b/tests/refs/rtt_srgb.ref new file mode 100644 index 000000000..7c52ebba1 --- /dev/null +++ b/tests/refs/rtt_srgb.ref @@ -0,0 +1 @@ +0:4F4F4FFF 1:7C7C7CFF 2:9B9B9BFF 3:B2B2B2FF 4:C8C8C8FF 5:DADADAFF 6:EBEBEBFF 7:FAFAFAFF diff --git a/tests/rtt.py b/tests/rtt.py index 86972fbf4..68b782f17 100644 --- a/tests/rtt.py +++ b/tests/rtt.py @@ -26,6 +26,7 @@ from pynopegl_utils.misc import get_shader from pynopegl_utils.tests.cmp_cuepoints import test_cuepoints from pynopegl_utils.tests.cmp_fingerprint import test_fingerprint +from pynopegl_utils.tests.cuepoints_utils import get_grid_points, get_points_nodes from pynopegl_utils.toolbox.colors import COLORS @@ -427,3 +428,28 @@ def rtt_resizable_with_timeranges_implicit(cfg: ngl.SceneCfg): group = ngl.Group(children=[time_range_filter1, time_range_filter2]) return group + + +@test_cuepoints(width=32, height=32, points=get_grid_points(8, 1), tolerance=1) +@ngl.scene(controls=dict(show_dbg_points=ngl.scene.Bool())) +def rtt_srgb(cfg: ngl.SceneCfg, show_dbg_points=False): + cfg.aspect_ratio = (1, 1) + cfg.duration = 1 + + gradient = ngl.DrawGradient( + color0=(0, 0, 0), + color1=(1, 1, 1), + ) + texture = ngl.Texture2D( + format="r8g8b8a8_srgb", + width=32, + height=32, + data_src=gradient, + ) + group = ngl.Group(children=[ngl.DrawTexture(texture)]) + + if show_dbg_points: + cuepoints = get_grid_points(8, 1) + group.add_children(get_points_nodes(cfg, cuepoints)) + + return group