diff --git a/client/renderers/EGL/CMakeLists.txt b/client/renderers/EGL/CMakeLists.txt index 4a8ce270..8b88346f 100644 --- a/client/renderers/EGL/CMakeLists.txt +++ b/client/renderers/EGL/CMakeLists.txt @@ -60,6 +60,7 @@ build_shaders( shader/hdr_compose.vert shader/hdr_compose.frag shader/basic.vert + shader/hdr_decode.frag shader/convert_24bit.frag shader/ffx_cas.frag shader/ffx_fsr1_easu.frag @@ -97,6 +98,7 @@ add_library(renderer_EGL STATIC ffx.c filter_glsl.c filter_24bit.c + filter_hdr.c filter_ffx_cas.c filter_ffx_fsr1.c filter_downscale.c diff --git a/client/renderers/EGL/desktop.c b/client/renderers/EGL/desktop.c index 6fddbffe..2a9baa7d 100644 --- a/client/renderers/EGL/desktop.c +++ b/client/renderers/EGL/desktop.c @@ -497,14 +497,16 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, } unsigned int finalSizeX, finalSizeY; + bool outputHDRPQ = desktop->hdrPQ; EGL_Texture * texture = egl_postProcessGetOutput(desktop->pp, - &finalSizeX, &finalSizeY); + &finalSizeX, &finalSizeY, &outputHDRPQ); if (unlikely(!texture)) { - texture = tex; - finalSizeX = width; - finalSizeY = height; + texture = tex; + finalSizeX = width; + finalSizeY = height; + outputHDRPQ = desktop->hdrPQ; } if (target) @@ -546,7 +548,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, /* PQ is normalized to 10000 nits, while scRGB defines 1.0 as 80 nits. * Convert either encoding into values relative to the selected SDR peak. */ - const float mapHDRGain = (desktop->hdrPQ ? 10000.0f : 80.0f) / + const float mapHDRGain = (outputHDRPQ ? 10000.0f : 80.0f) / desktop->peakLuminance; const float mapHDRContentPeak = (float)desktop->maxCLL / desktop->peakLuminance; @@ -562,7 +564,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, desktop->mapHDRtoSDR && !desktop->nativeHDR); egl_uniform1f (shader->uMapHDRGain , mapHDRGain); egl_uniform1f (shader->uMapHDRContentPeak, mapHDRContentPeak); - egl_uniform1i (shader->uMapHDRPQ , desktop->hdrPQ); + egl_uniform1i (shader->uMapHDRPQ , outputHDRPQ); egl_uniform1i (shader->uOutputHDRLinear , desktop->linearComposition); egl_shaderUse(shader->shader); diff --git a/client/renderers/EGL/filter_hdr.c b/client/renderers/EGL/filter_hdr.c new file mode 100644 index 00000000..338a7437 --- /dev/null +++ b/client/renderers/EGL/filter_hdr.c @@ -0,0 +1,171 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "filter.h" +#include "effect.h" + +#include "common/debug.h" + +#include "basic.vert.h" +#include "hdr_decode.frag.h" + +#include + +typedef struct EGL_FilterHDR +{ + EGL_Filter base; + + int useDMA; + unsigned int width; + unsigned int height; + bool prepared; + + EGL_Shader * shader; + EGL_Effect * effect; + EGL_EffectPass * pass; +} +EGL_FilterHDR; + +static bool egl_filterHDRInit(EGL_Filter ** filter) +{ + EGL_FilterHDR * this = calloc(1, sizeof(*this)); + if (!this) + { + DEBUG_ERROR("Failed to allocate memory"); + return false; + } + + this->useDMA = -1; + + if (!egl_shaderInit(&this->shader)) + { + DEBUG_ERROR("Failed to initialize the shader"); + goto error_this; + } + + if (!egl_effectInit(&this->effect)) + { + DEBUG_ERROR("Failed to initialize the effect"); + goto error_shader; + } + + if (!egl_effectAddPass(this->effect, this->shader, &this->pass)) + { + DEBUG_ERROR("Failed to initialize the effect pass"); + goto error_effect; + } + + egl_effectPassSetFilter(this->pass, GL_NEAREST, GL_NEAREST); + *filter = &this->base; + return true; + +error_effect: + egl_effectFree(&this->effect); + +error_shader: + egl_shaderFree(&this->shader); + +error_this: + free(this); + return false; +} + +static void egl_filterHDRFree(EGL_Filter * filter) +{ + EGL_FilterHDR * this = UPCAST(EGL_FilterHDR, filter); + + egl_effectFree(&this->effect); + egl_shaderFree(&this->shader); + free(this); +} + +static bool egl_filterHDRSetup(EGL_Filter * filter, + enum EGL_PixelFormat pixFmt, unsigned int width, unsigned int height, + unsigned int desktopWidth, unsigned int desktopHeight, bool useDMA) +{ + EGL_FilterHDR * this = UPCAST(EGL_FilterHDR, filter); + (void)desktopWidth; + (void)desktopHeight; + + if (pixFmt != EGL_PF_RGBA10) + return false; + + if (this->useDMA != useDMA) + { + if (!egl_shaderCompile(this->shader, + b_shader_basic_vert , b_shader_basic_vert_size, + b_shader_hdr_decode_frag, b_shader_hdr_decode_frag_size, + useDMA, NULL)) + { + DEBUG_ERROR("Failed to compile the HDR decode shader"); + return false; + } + + this->useDMA = useDMA; + this->prepared = false; + } + + if (this->prepared && this->width == width && this->height == height) + return true; + + if (!egl_effectPassSetup(this->pass, EGL_PF_RGBA16F, width, height)) + return false; + + this->width = width; + this->height = height; + this->prepared = false; + return true; +} + +static void egl_filterHDRGetOutputRes(EGL_Filter * filter, + unsigned int * width, unsigned int * height, EGL_PixelFormat * pixFmt) +{ + EGL_FilterHDR * this = UPCAST(EGL_FilterHDR, filter); + *width = this->width; + *height = this->height; + *pixFmt = EGL_PF_RGBA16F; +} + +static bool egl_filterHDRPrepare(EGL_Filter * filter) +{ + EGL_FilterHDR * this = UPCAST(EGL_FilterHDR, filter); + this->prepared = true; + return true; +} + +static EGL_Texture * egl_filterHDRRun(EGL_Filter * filter, + EGL_FilterRects * rects, EGL_Texture * texture) +{ + EGL_FilterHDR * this = UPCAST(EGL_FilterHDR, filter); + return egl_effectRun(this->effect, rects, texture); +} + +EGL_FilterOps egl_filterHDRDecodeOps = +{ + .id = "hdrDecode", + .name = "HDR Decode", + .type = EGL_FILTER_TYPE_INTERNAL, + .init = egl_filterHDRInit, + .free = egl_filterHDRFree, + .setup = egl_filterHDRSetup, + .getOutputRes = egl_filterHDRGetOutputRes, + .prepare = egl_filterHDRPrepare, + .run = egl_filterHDRRun +}; diff --git a/client/renderers/EGL/filters.h b/client/renderers/EGL/filters.h index 2a65b0af..e49408ab 100644 --- a/client/renderers/EGL/filters.h +++ b/client/renderers/EGL/filters.h @@ -21,6 +21,7 @@ #pragma once extern EGL_FilterOps egl_filter24bitOps; +extern EGL_FilterOps egl_filterHDRDecodeOps; extern EGL_FilterOps egl_filterDownscaleOps; extern EGL_FilterOps egl_filterFFXCASOps; extern EGL_FilterOps egl_filterFFXFSR1Ops; diff --git a/client/renderers/EGL/postprocess.c b/client/renderers/EGL/postprocess.c index ff2de624..4052eb60 100644 --- a/client/renderers/EGL/postprocess.c +++ b/client/renderers/EGL/postprocess.c @@ -49,7 +49,11 @@ static const EGL_FilterOps * EGL_Filters[] = struct EGL_PostProcess { - Vector filters, internalFilters, activeFilters; + Vector filters; + Vector internalFilters; + Vector activeFilters; + Vector effectFilters; + EGL_Filter * hdrDecode; EGL_Texture * output; unsigned int outputX, outputY; _Atomic(bool) modified; @@ -64,6 +68,7 @@ struct EGL_PostProcess bool useDMA; bool hdrPQ; unsigned int outputX, outputY; + bool outputHDRPQ; bool fullFrame; } config; @@ -588,16 +593,29 @@ bool egl_postProcessInit(EGL_PostProcess ** pp) } if (!vector_create(&this->activeFilters, - sizeof(EGL_Filter *), ARRAY_LENGTH(EGL_Filters) + 1)) + sizeof(EGL_Filter *), ARRAY_LENGTH(EGL_Filters) + 2)) { DEBUG_ERROR("Failed to allocate the active filter list"); goto error_internal; } + if (!vector_create(&this->effectFilters, + sizeof(EGL_Filter *), ARRAY_LENGTH(EGL_Filters))) + { + DEBUG_ERROR("Failed to allocate the effect filter list"); + goto error_active; + } + + if (!egl_filterInit(&egl_filterHDRDecodeOps, &this->hdrDecode)) + { + DEBUG_ERROR("Failed to initialize the HDR decode filter"); + goto error_effect; + } + if (!egl_desktopRectsInit(&this->rects, 1)) { DEBUG_ERROR("Failed to initialize the desktop rects"); - goto error_active; + goto error_hdr; } loadPresetList(this); @@ -607,6 +625,12 @@ bool egl_postProcessInit(EGL_PostProcess ** pp) *pp = this; return true; +error_hdr: + egl_filterFree(&this->hdrDecode); + +error_effect: + vector_destroy(&this->effectFilters); + error_active: vector_destroy(&this->activeFilters); @@ -628,6 +652,8 @@ void egl_postProcessFree(EGL_PostProcess ** pp) EGL_PostProcess * this = *pp; + egl_filterFree(&this->hdrDecode); + vector_destroy(&this->effectFilters); vector_destroy(&this->activeFilters); EGL_Filter ** filter; @@ -728,6 +754,7 @@ static bool configureChain(EGL_PostProcess * this, EGL_PixelFormat pixFmt, { this->config.valid = false; vector_clear(&this->activeFilters); + vector_clear(&this->effectFilters); unsigned int outputX = inputX; unsigned int outputY = inputY; @@ -736,45 +763,97 @@ static bool configureChain(EGL_PostProcess * this, EGL_PixelFormat pixFmt, bool fullFrame = false; EGL_Filter * filter; - const Vector * lists[] = + vector_forEach(filter, &this->internalFilters) { - &this->internalFilters, - &this->filters, - NULL - }; + egl_filterSetOutputResHint(filter, targetX, targetY); - for(const Vector ** filters = lists; *filters; ++filters) - vector_forEach(filter, *filters) + if (!egl_filterSetup(filter, outputFormat, outputX, outputY, + desktopWidth, desktopHeight, inputDMA) || + !egl_filterPrepare(filter)) + continue; + + if (!vector_push(&this->activeFilters, &filter)) { - // These filters operate on their input signal directly. PQ must be - // decoded to linear light before resampling or enhancement, and none of - // the current external filters declares such support. Keep only the - // mandatory format-conversion filters in a PQ chain. - if (hdrPQ && filter->ops.type != EGL_FILTER_TYPE_INTERNAL) - continue; - - egl_filterSetOutputResHint(filter, targetX, targetY); - - if (!egl_filterSetup(filter, outputFormat, outputX, outputY, - desktopWidth, desktopHeight, inputDMA) || - !egl_filterPrepare(filter)) - continue; - - if (!vector_push(&this->activeFilters, &filter)) - { - vector_clear(&this->activeFilters); - return false; - } - - fullFrame |= filter->ops.fullFrame; - - egl_filterGetOutputRes(filter, &outputX, &outputY, &outputFormat); - - /* The first active filter converts external DMA textures into a normal - * texture for every subsequent filter. */ - inputDMA = false; + vector_clear(&this->activeFilters); + return false; } + fullFrame |= filter->ops.fullFrame; + egl_filterGetOutputRes(filter, &outputX, &outputY, &outputFormat); + inputDMA = false; + } + + const unsigned int baseOutputX = outputX; + const unsigned int baseOutputY = outputY; + const EGL_PixelFormat baseOutputFormat = outputFormat; + const bool baseInputDMA = inputDMA; + const bool baseFullFrame = fullFrame; + + // Effects operate in linear scRGB. Probe them against the format produced + // by the decoder first so an inactive chain does not compile or execute an + // otherwise unnecessary conversion pass. + if (hdrPQ) + { + outputFormat = EGL_PF_RGBA16F; + inputDMA = false; + } + + vector_forEach(filter, &this->filters) + { + egl_filterSetOutputResHint(filter, targetX, targetY); + + if (!egl_filterSetup(filter, outputFormat, outputX, outputY, + desktopWidth, desktopHeight, inputDMA) || + !egl_filterPrepare(filter)) + continue; + + if (!vector_push(&this->effectFilters, &filter)) + { + vector_clear(&this->activeFilters); + vector_clear(&this->effectFilters); + return false; + } + + fullFrame |= filter->ops.fullFrame; + egl_filterGetOutputRes(filter, &outputX, &outputY, &outputFormat); + inputDMA = false; + } + + bool effectsActive = vector_size(&this->effectFilters) > 0; + if (effectsActive && hdrPQ) + { + if (!egl_filterSetup(this->hdrDecode, + baseOutputFormat, baseOutputX, baseOutputY, + desktopWidth, desktopHeight, baseInputDMA) || + !egl_filterPrepare(this->hdrDecode)) + { + DEBUG_ERROR("Failed to prepare HDR input for the EGL effect chain"); + vector_clear(&this->effectFilters); + effectsActive = false; + fullFrame = baseFullFrame; + } + else if (!vector_push(&this->activeFilters, &this->hdrDecode)) + { + vector_clear(&this->activeFilters); + vector_clear(&this->effectFilters); + return false; + } + } + + vector_forEach(filter, &this->effectFilters) + if (!vector_push(&this->activeFilters, &filter)) + { + vector_clear(&this->activeFilters); + vector_clear(&this->effectFilters); + return false; + } + + if (!effectsActive) + { + outputX = baseOutputX; + outputY = baseOutputY; + } + egl_desktopRectsMatrix(this->matrix, desktopWidth, desktopHeight, 0.0f, 0.0f, 1.0f, 1.0f, LG_ROTATE_0); @@ -789,6 +868,7 @@ static bool configureChain(EGL_PostProcess * this, EGL_PixelFormat pixFmt, this->config.hdrPQ = hdrPQ; this->config.outputX = outputX; this->config.outputY = outputY; + this->config.outputHDRPQ = hdrPQ && !effectsActive; this->config.fullFrame = fullFrame; this->config.valid = true; return true; @@ -871,9 +951,10 @@ bool egl_postProcessRun(EGL_PostProcess * this, EGL_Texture * tex, } EGL_Texture * egl_postProcessGetOutput(EGL_PostProcess * this, - unsigned int * outputX, unsigned int * outputY) + unsigned int * outputX, unsigned int * outputY, bool * hdrPQ) { *outputX = this->outputX; *outputY = this->outputY; + *hdrPQ = this->config.outputHDRPQ; return this->output; } diff --git a/client/renderers/EGL/postprocess.h b/client/renderers/EGL/postprocess.h index 27bc1baa..063bc738 100644 --- a/client/renderers/EGL/postprocess.h +++ b/client/renderers/EGL/postprocess.h @@ -49,5 +49,6 @@ bool egl_postProcessRun(EGL_PostProcess * this, EGL_Texture * tex, EGL_DesktopRects * rects, int desktopWidth, int desktopHeight, unsigned int targetX, unsigned int targetY, bool useDMA, bool hdrPQ); +/* Return the current output texture and whether it remains PQ encoded. */ EGL_Texture * egl_postProcessGetOutput(EGL_PostProcess * this, - unsigned int * outputX, unsigned int * outputY); + unsigned int * outputX, unsigned int * outputY, bool * hdrPQ); diff --git a/client/renderers/EGL/shader/hdr_decode.frag b/client/renderers/EGL/shader/hdr_decode.frag new file mode 100644 index 00000000..df982ab6 --- /dev/null +++ b/client/renderers/EGL/shader/hdr_decode.frag @@ -0,0 +1,37 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#version 300 es +#extension GL_OES_EGL_image_external_essl3 : enable + +precision highp float; + +#include "hdr.h" + +in vec2 fragCoord; +out vec4 fragColor; + +uniform sampler2D sampler1; + +void main() +{ + vec4 pq = texture(sampler1, fragCoord); + fragColor = vec4(bt2020to709(pq2lin(pq.rgb, 1.0)) * 125.0, pq.a); +}