Loading libs/renderengine/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -102,6 +102,7 @@ filegroup { "skia/filters/GaussianBlurFilter.cpp", "skia/filters/KawaseBlurFilter.cpp", "skia/filters/LinearEffect.cpp", "skia/filters/MouriMap.cpp", "skia/filters/StretchShaderFactory.cpp", ], } Loading libs/renderengine/skia/SkiaRenderEngine.cpp +17 −3 Original line number Diff line number Diff line Loading @@ -79,6 +79,7 @@ #include "filters/GaussianBlurFilter.h" #include "filters/KawaseBlurFilter.h" #include "filters/LinearEffect.h" #include "filters/MouriMap.h" #include "log/log_main.h" #include "skia/compat/SkiaBackendTexture.h" #include "skia/debug/SkiaCapture.h" Loading Loading @@ -509,9 +510,9 @@ sk_sp<SkShader> SkiaRenderEngine::createRuntimeEffectShader( // Determine later on if we need to leverage the stertch shader within // surface flinger const auto& stretchEffect = parameters.layer.stretchEffect; const auto& targetBuffer = parameters.layer.source.buffer.buffer; auto shader = parameters.shader; if (stretchEffect.hasEffect()) { const auto targetBuffer = parameters.layer.source.buffer.buffer; const auto graphicBuffer = targetBuffer ? targetBuffer->getBuffer() : nullptr; if (graphicBuffer && parameters.shader) { shader = mStretchShaderFactory.createSkShader(shader, stretchEffect); Loading @@ -519,9 +520,22 @@ sk_sp<SkShader> SkiaRenderEngine::createRuntimeEffectShader( } if (parameters.requiresLinearEffect) { const auto format = targetBuffer != nullptr ? std::optional<ui::PixelFormat>( static_cast<ui::PixelFormat>(targetBuffer->getPixelFormat())) : std::nullopt; if (parameters.display.tonemapStrategy == DisplaySettings::TonemapStrategy::Local) { // TODO: Apply a local tonemap // fallthrough for now // TODO: Handle color matrix transforms in linear space. SkImage* image = parameters.shader->isAImage((SkMatrix*)nullptr, (SkTileMode*)nullptr); if (image) { static MouriMap kMapper; const float ratio = getHdrRenderType(parameters.layer.sourceDataspace, format) == HdrRenderType::GENERIC_HDR ? 1.0f : parameters.layerDimmingRatio; return kMapper.mouriMap(getActiveContext(), parameters.shader, ratio); } } auto effect = Loading libs/renderengine/skia/filters/MouriMap.cpp 0 → 100644 +183 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MouriMap.h" #include <SkCanvas.h> #include <SkColorType.h> #include <SkPaint.h> #include <SkTileMode.h> namespace android { namespace renderengine { namespace skia { namespace { sk_sp<SkRuntimeEffect> makeEffect(const SkString& sksl) { auto [effect, error] = SkRuntimeEffect::MakeForShader(sksl); LOG_ALWAYS_FATAL_IF(!effect, "RuntimeShader error: %s", error.c_str()); return effect; } const SkString kCrosstalkAndChunk16x16(R"( uniform shader bitmap; uniform float hdrSdrRatio; vec4 main(vec2 xy) { float maximum = 0.0; for (int y = 0; y < 16; y++) { for (int x = 0; x < 16; x++) { float3 linear = toLinearSrgb(bitmap.eval(xy * 16 + vec2(x, y)).rgb) * hdrSdrRatio; float maxRGB = max(linear.r, max(linear.g, linear.b)); maximum = max(maximum, log2(max(maxRGB, 1.0))); } } return float4(float3(maximum), 1.0); } )"); const SkString kChunk8x8(R"( uniform shader bitmap; vec4 main(vec2 xy) { float maximum = 0.0; for (int y = 0; y < 8; y++) { for (int x = 0; x < 8; x++) { maximum = max(maximum, bitmap.eval(xy * 8 + vec2(x, y)).r); } } return float4(float3(maximum), 1.0); } )"); const SkString kBlur(R"( uniform shader bitmap; vec4 main(vec2 xy) { float C[5]; C[0] = 1.0 / 16.0; C[1] = 4.0 / 16.0; C[2] = 6.0 / 16.0; C[3] = 4.0 / 16.0; C[4] = 1.0 / 16.0; float result = 0.0; for (int y = -2; y <= 2; y++) { for (int x = -2; x <= 2; x++) { result += C[y + 2] * C[x + 2] * bitmap.eval(xy + vec2(x, y)).r; } } return float4(float3(exp2(result)), 1.0); } )"); const SkString kTonemap(R"( uniform shader image; uniform shader lux; uniform float scaleFactor; uniform float hdrSdrRatio; vec4 main(vec2 xy) { float localMax = lux.eval(xy * scaleFactor).r; float4 rgba = image.eval(xy); float3 linear = toLinearSrgb(rgba.rgb) * hdrSdrRatio; if (localMax <= 1.0) { return float4(fromLinearSrgb(linear), 1.0); } float maxRGB = max(linear.r, max(linear.g, linear.b)); localMax = max(localMax, maxRGB); float gain = (1 + maxRGB / (localMax * localMax)) / (1 + maxRGB); return float4(fromLinearSrgb(linear * gain), 1.0); } )"); // Draws the given runtime shader on a GPU surface and returns the result as an SkImage. sk_sp<SkImage> makeImage(SkSurface* surface, const SkRuntimeShaderBuilder& builder) { sk_sp<SkShader> shader = builder.makeShader(nullptr); LOG_ALWAYS_FATAL_IF(!shader, "%s, Failed to make shader!", __func__); SkPaint paint; paint.setShader(std::move(shader)); paint.setBlendMode(SkBlendMode::kSrc); surface->getCanvas()->drawPaint(paint); return surface->makeImageSnapshot(); } } // namespace MouriMap::MouriMap() : mCrosstalkAndChunk16x16(makeEffect(kCrosstalkAndChunk16x16)), mChunk8x8(makeEffect(kChunk8x8)), mBlur(makeEffect(kBlur)), mTonemap(makeEffect(kTonemap)) {} sk_sp<SkShader> MouriMap::mouriMap(SkiaGpuContext* context, sk_sp<SkShader> input, float hdrSdrRatio) { auto downchunked = downchunk(context, input, hdrSdrRatio); auto localLux = blur(context, downchunked.get()); return tonemap(input, localLux.get(), hdrSdrRatio); } sk_sp<SkImage> MouriMap::downchunk(SkiaGpuContext* context, sk_sp<SkShader> input, float hdrSdrRatio) const { SkMatrix matrix; SkImage* image = input->isAImage(&matrix, (SkTileMode*)nullptr); SkRuntimeShaderBuilder crosstalkAndChunk16x16Builder(mCrosstalkAndChunk16x16); crosstalkAndChunk16x16Builder.child("bitmap") = input; crosstalkAndChunk16x16Builder.uniform("hdrSdrRatio") = hdrSdrRatio; // TODO: fp16 might be overkill. Most practical surfaces use 8-bit RGB for HDR UI and 10-bit YUV // for HDR video. These downsample operations compute log2(max(linear RGB, 1.0)). So we don't // care about LDR precision since they all resolve to LDR-max. For appropriately mastered HDR // content that follows BT. 2408, 25% of the bit range for HLG and 42% of the bit range for PQ // are reserved for HDR. This means that we can fit the entire HDR range for 10-bit HLG inside // of 8 bits. We can also fit about half of the range for PQ, but most content does not fill the // entire 10k nit range for PQ. Furthermore, we blur all of this later on anyways, so we might // not need to be so precise. So, it's possible that we could use A8 or R8 instead. If we want // to be really conservative we can try to use R16 or even RGBA1010102 to fake an R10 surface, // which would cut write bandwidth significantly. static constexpr auto kFirstDownscaleAmount = 16; sk_sp<SkSurface> firstDownsampledSurface = context->createRenderTarget( image->imageInfo() .makeWH(std::max(1, image->width() / kFirstDownscaleAmount), std::max(1, image->height() / kFirstDownscaleAmount)) .makeColorType(kRGBA_F16_SkColorType)); LOG_ALWAYS_FATAL_IF(!firstDownsampledSurface, "%s: Failed to create surface!", __func__); auto firstDownsampledImage = makeImage(firstDownsampledSurface.get(), crosstalkAndChunk16x16Builder); SkRuntimeShaderBuilder chunk8x8Builder(mChunk8x8); chunk8x8Builder.child("bitmap") = firstDownsampledImage->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions()); static constexpr auto kSecondDownscaleAmount = 8; sk_sp<SkSurface> secondDownsampledSurface = context->createRenderTarget( firstDownsampledImage->imageInfo() .makeWH(std::max(1, firstDownsampledImage->width() / kSecondDownscaleAmount), std::max(1, firstDownsampledImage->height() / kSecondDownscaleAmount))); LOG_ALWAYS_FATAL_IF(!secondDownsampledSurface, "%s: Failed to create surface!", __func__); return makeImage(secondDownsampledSurface.get(), chunk8x8Builder); } sk_sp<SkImage> MouriMap::blur(SkiaGpuContext* context, SkImage* input) const { SkRuntimeShaderBuilder blurBuilder(mBlur); blurBuilder.child("bitmap") = input->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions()); sk_sp<SkSurface> blurSurface = context->createRenderTarget(input->imageInfo()); LOG_ALWAYS_FATAL_IF(!blurSurface, "%s: Failed to create surface!", __func__); return makeImage(blurSurface.get(), blurBuilder); } sk_sp<SkShader> MouriMap::tonemap(sk_sp<SkShader> input, SkImage* localLux, float hdrSdrRatio) const { static constexpr float kScaleFactor = 1.0f / 128.0f; SkRuntimeShaderBuilder tonemapBuilder(mTonemap); tonemapBuilder.child("image") = input; tonemapBuilder.child("lux") = localLux->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone)); tonemapBuilder.uniform("scaleFactor") = kScaleFactor; tonemapBuilder.uniform("hdrSdrRatio") = hdrSdrRatio; return tonemapBuilder.makeShader(); } } // namespace skia } // namespace renderengine } // namespace android No newline at end of file libs/renderengine/skia/filters/MouriMap.h 0 → 100644 +81 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <SkImage.h> #include <SkRuntimeEffect.h> #include <SkShader.h> #include "../compat/SkiaGpuContext.h" namespace android { namespace renderengine { namespace skia { /** * MouriMap is a fast, albeit not realtime, tonemapping algorithm optimized for near-exact * preservation of SDR (or, equivalently, LDR) regions, while trying to do an acceptable job of * preserving HDR detail. * * MouriMap is a local tonemapping algorithm, meaning that nearby pixels are taken into * consideration when choosing a tonemapping curve. * * The algorithm conceptually is as follows: * 1. Partition the image into 128x128 chunks, computing the log2(maximum luminance) in each chunk *. a. Maximum luminance is computed as max(R, G, B), where the R, G, B values are in linear *. luminance on a scale defined by the destination color gamut. Max(R, G, B) has been found *. to minimize difference in hue while restricting to typical LDR color volumes. See: Burke, *. Adam & Smith, Michael & Zink, Michael. 2020. Color Volume and Hue-preservation in HDR *. Tone Mapping. SMPTE Motion Imaging Journal. *. b. Each computed luminance is lower-bounded by 1.0 in Skia's color *. management, or 203 nits. * 2. Blur the resulting chunks using a 5x5 gaussian kernel, to smooth out the local luminance map. * 3. Now, for each pixel in the original image: * a. Upsample from the blurred chunks of luminance computed in (2). Call this luminance value *. L: an estimate of the maximum luminance of surrounding pixels. *. b. If the luminance is less than 1.0 (203 nits), then do not modify the pixel value of the *. original image. *. c. Otherwise, *. parameterize a tone-mapping curve using a method described by Chrome: *. https://docs.google.com/document/d/17T2ek1i2R7tXdfHCnM-i5n6__RoYe0JyMfKmTEjoGR8/. *. i. Compute a gain G = (1 + max(linear R, linear G, linear B) / (L * L)) *. / (1 + max(linear R, linear G, linear B)). Note the similarity with the 1D curve *. described by Erik Reinhard, Michael Stark, Peter Shirley, and James Ferwerda. 2002. *. Photographic tone reproduction for digital images. ACM Trans. Graph. *. ii. Multiply G by the linear source colors to compute the final colors. * * Because it is a multi-renderpass algorithm requiring multiple off-screen textures, MouriMap is * typically not suitable to be ran "frequently", at high refresh rates (e.g., 120hz). However, * MouriMap is sufficiently fast enough for infrequent composition where preserving SDR detail is * most important, such as for screenshots. */ class MouriMap { public: MouriMap(); // Apply the MouriMap tonemmaping operator to the input. // The HDR/SDR ratio describes the luminace range of the input. 1.0 means SDR. Anything larger // then 1.0 means that there is headroom above the SDR region. sk_sp<SkShader> mouriMap(SkiaGpuContext* context, sk_sp<SkShader> input, float hdrSdrRatio); private: sk_sp<SkImage> downchunk(SkiaGpuContext* context, sk_sp<SkShader> input, float hdrSdrRatio) const; sk_sp<SkImage> blur(SkiaGpuContext* context, SkImage* input) const; sk_sp<SkShader> tonemap(sk_sp<SkShader> input, SkImage* localLux, float hdrSdrRatio) const; const sk_sp<SkRuntimeEffect> mCrosstalkAndChunk16x16; const sk_sp<SkRuntimeEffect> mChunk8x8; const sk_sp<SkRuntimeEffect> mBlur; const sk_sp<SkRuntimeEffect> mTonemap; }; } // namespace skia } // namespace renderengine } // namespace android No newline at end of file Loading
libs/renderengine/Android.bp +1 −0 Original line number Diff line number Diff line Loading @@ -102,6 +102,7 @@ filegroup { "skia/filters/GaussianBlurFilter.cpp", "skia/filters/KawaseBlurFilter.cpp", "skia/filters/LinearEffect.cpp", "skia/filters/MouriMap.cpp", "skia/filters/StretchShaderFactory.cpp", ], } Loading
libs/renderengine/skia/SkiaRenderEngine.cpp +17 −3 Original line number Diff line number Diff line Loading @@ -79,6 +79,7 @@ #include "filters/GaussianBlurFilter.h" #include "filters/KawaseBlurFilter.h" #include "filters/LinearEffect.h" #include "filters/MouriMap.h" #include "log/log_main.h" #include "skia/compat/SkiaBackendTexture.h" #include "skia/debug/SkiaCapture.h" Loading Loading @@ -509,9 +510,9 @@ sk_sp<SkShader> SkiaRenderEngine::createRuntimeEffectShader( // Determine later on if we need to leverage the stertch shader within // surface flinger const auto& stretchEffect = parameters.layer.stretchEffect; const auto& targetBuffer = parameters.layer.source.buffer.buffer; auto shader = parameters.shader; if (stretchEffect.hasEffect()) { const auto targetBuffer = parameters.layer.source.buffer.buffer; const auto graphicBuffer = targetBuffer ? targetBuffer->getBuffer() : nullptr; if (graphicBuffer && parameters.shader) { shader = mStretchShaderFactory.createSkShader(shader, stretchEffect); Loading @@ -519,9 +520,22 @@ sk_sp<SkShader> SkiaRenderEngine::createRuntimeEffectShader( } if (parameters.requiresLinearEffect) { const auto format = targetBuffer != nullptr ? std::optional<ui::PixelFormat>( static_cast<ui::PixelFormat>(targetBuffer->getPixelFormat())) : std::nullopt; if (parameters.display.tonemapStrategy == DisplaySettings::TonemapStrategy::Local) { // TODO: Apply a local tonemap // fallthrough for now // TODO: Handle color matrix transforms in linear space. SkImage* image = parameters.shader->isAImage((SkMatrix*)nullptr, (SkTileMode*)nullptr); if (image) { static MouriMap kMapper; const float ratio = getHdrRenderType(parameters.layer.sourceDataspace, format) == HdrRenderType::GENERIC_HDR ? 1.0f : parameters.layerDimmingRatio; return kMapper.mouriMap(getActiveContext(), parameters.shader, ratio); } } auto effect = Loading
libs/renderengine/skia/filters/MouriMap.cpp 0 → 100644 +183 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "MouriMap.h" #include <SkCanvas.h> #include <SkColorType.h> #include <SkPaint.h> #include <SkTileMode.h> namespace android { namespace renderengine { namespace skia { namespace { sk_sp<SkRuntimeEffect> makeEffect(const SkString& sksl) { auto [effect, error] = SkRuntimeEffect::MakeForShader(sksl); LOG_ALWAYS_FATAL_IF(!effect, "RuntimeShader error: %s", error.c_str()); return effect; } const SkString kCrosstalkAndChunk16x16(R"( uniform shader bitmap; uniform float hdrSdrRatio; vec4 main(vec2 xy) { float maximum = 0.0; for (int y = 0; y < 16; y++) { for (int x = 0; x < 16; x++) { float3 linear = toLinearSrgb(bitmap.eval(xy * 16 + vec2(x, y)).rgb) * hdrSdrRatio; float maxRGB = max(linear.r, max(linear.g, linear.b)); maximum = max(maximum, log2(max(maxRGB, 1.0))); } } return float4(float3(maximum), 1.0); } )"); const SkString kChunk8x8(R"( uniform shader bitmap; vec4 main(vec2 xy) { float maximum = 0.0; for (int y = 0; y < 8; y++) { for (int x = 0; x < 8; x++) { maximum = max(maximum, bitmap.eval(xy * 8 + vec2(x, y)).r); } } return float4(float3(maximum), 1.0); } )"); const SkString kBlur(R"( uniform shader bitmap; vec4 main(vec2 xy) { float C[5]; C[0] = 1.0 / 16.0; C[1] = 4.0 / 16.0; C[2] = 6.0 / 16.0; C[3] = 4.0 / 16.0; C[4] = 1.0 / 16.0; float result = 0.0; for (int y = -2; y <= 2; y++) { for (int x = -2; x <= 2; x++) { result += C[y + 2] * C[x + 2] * bitmap.eval(xy + vec2(x, y)).r; } } return float4(float3(exp2(result)), 1.0); } )"); const SkString kTonemap(R"( uniform shader image; uniform shader lux; uniform float scaleFactor; uniform float hdrSdrRatio; vec4 main(vec2 xy) { float localMax = lux.eval(xy * scaleFactor).r; float4 rgba = image.eval(xy); float3 linear = toLinearSrgb(rgba.rgb) * hdrSdrRatio; if (localMax <= 1.0) { return float4(fromLinearSrgb(linear), 1.0); } float maxRGB = max(linear.r, max(linear.g, linear.b)); localMax = max(localMax, maxRGB); float gain = (1 + maxRGB / (localMax * localMax)) / (1 + maxRGB); return float4(fromLinearSrgb(linear * gain), 1.0); } )"); // Draws the given runtime shader on a GPU surface and returns the result as an SkImage. sk_sp<SkImage> makeImage(SkSurface* surface, const SkRuntimeShaderBuilder& builder) { sk_sp<SkShader> shader = builder.makeShader(nullptr); LOG_ALWAYS_FATAL_IF(!shader, "%s, Failed to make shader!", __func__); SkPaint paint; paint.setShader(std::move(shader)); paint.setBlendMode(SkBlendMode::kSrc); surface->getCanvas()->drawPaint(paint); return surface->makeImageSnapshot(); } } // namespace MouriMap::MouriMap() : mCrosstalkAndChunk16x16(makeEffect(kCrosstalkAndChunk16x16)), mChunk8x8(makeEffect(kChunk8x8)), mBlur(makeEffect(kBlur)), mTonemap(makeEffect(kTonemap)) {} sk_sp<SkShader> MouriMap::mouriMap(SkiaGpuContext* context, sk_sp<SkShader> input, float hdrSdrRatio) { auto downchunked = downchunk(context, input, hdrSdrRatio); auto localLux = blur(context, downchunked.get()); return tonemap(input, localLux.get(), hdrSdrRatio); } sk_sp<SkImage> MouriMap::downchunk(SkiaGpuContext* context, sk_sp<SkShader> input, float hdrSdrRatio) const { SkMatrix matrix; SkImage* image = input->isAImage(&matrix, (SkTileMode*)nullptr); SkRuntimeShaderBuilder crosstalkAndChunk16x16Builder(mCrosstalkAndChunk16x16); crosstalkAndChunk16x16Builder.child("bitmap") = input; crosstalkAndChunk16x16Builder.uniform("hdrSdrRatio") = hdrSdrRatio; // TODO: fp16 might be overkill. Most practical surfaces use 8-bit RGB for HDR UI and 10-bit YUV // for HDR video. These downsample operations compute log2(max(linear RGB, 1.0)). So we don't // care about LDR precision since they all resolve to LDR-max. For appropriately mastered HDR // content that follows BT. 2408, 25% of the bit range for HLG and 42% of the bit range for PQ // are reserved for HDR. This means that we can fit the entire HDR range for 10-bit HLG inside // of 8 bits. We can also fit about half of the range for PQ, but most content does not fill the // entire 10k nit range for PQ. Furthermore, we blur all of this later on anyways, so we might // not need to be so precise. So, it's possible that we could use A8 or R8 instead. If we want // to be really conservative we can try to use R16 or even RGBA1010102 to fake an R10 surface, // which would cut write bandwidth significantly. static constexpr auto kFirstDownscaleAmount = 16; sk_sp<SkSurface> firstDownsampledSurface = context->createRenderTarget( image->imageInfo() .makeWH(std::max(1, image->width() / kFirstDownscaleAmount), std::max(1, image->height() / kFirstDownscaleAmount)) .makeColorType(kRGBA_F16_SkColorType)); LOG_ALWAYS_FATAL_IF(!firstDownsampledSurface, "%s: Failed to create surface!", __func__); auto firstDownsampledImage = makeImage(firstDownsampledSurface.get(), crosstalkAndChunk16x16Builder); SkRuntimeShaderBuilder chunk8x8Builder(mChunk8x8); chunk8x8Builder.child("bitmap") = firstDownsampledImage->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions()); static constexpr auto kSecondDownscaleAmount = 8; sk_sp<SkSurface> secondDownsampledSurface = context->createRenderTarget( firstDownsampledImage->imageInfo() .makeWH(std::max(1, firstDownsampledImage->width() / kSecondDownscaleAmount), std::max(1, firstDownsampledImage->height() / kSecondDownscaleAmount))); LOG_ALWAYS_FATAL_IF(!secondDownsampledSurface, "%s: Failed to create surface!", __func__); return makeImage(secondDownsampledSurface.get(), chunk8x8Builder); } sk_sp<SkImage> MouriMap::blur(SkiaGpuContext* context, SkImage* input) const { SkRuntimeShaderBuilder blurBuilder(mBlur); blurBuilder.child("bitmap") = input->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions()); sk_sp<SkSurface> blurSurface = context->createRenderTarget(input->imageInfo()); LOG_ALWAYS_FATAL_IF(!blurSurface, "%s: Failed to create surface!", __func__); return makeImage(blurSurface.get(), blurBuilder); } sk_sp<SkShader> MouriMap::tonemap(sk_sp<SkShader> input, SkImage* localLux, float hdrSdrRatio) const { static constexpr float kScaleFactor = 1.0f / 128.0f; SkRuntimeShaderBuilder tonemapBuilder(mTonemap); tonemapBuilder.child("image") = input; tonemapBuilder.child("lux") = localLux->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone)); tonemapBuilder.uniform("scaleFactor") = kScaleFactor; tonemapBuilder.uniform("hdrSdrRatio") = hdrSdrRatio; return tonemapBuilder.makeShader(); } } // namespace skia } // namespace renderengine } // namespace android No newline at end of file
libs/renderengine/skia/filters/MouriMap.h 0 → 100644 +81 −0 Original line number Diff line number Diff line /* * Copyright 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <SkImage.h> #include <SkRuntimeEffect.h> #include <SkShader.h> #include "../compat/SkiaGpuContext.h" namespace android { namespace renderengine { namespace skia { /** * MouriMap is a fast, albeit not realtime, tonemapping algorithm optimized for near-exact * preservation of SDR (or, equivalently, LDR) regions, while trying to do an acceptable job of * preserving HDR detail. * * MouriMap is a local tonemapping algorithm, meaning that nearby pixels are taken into * consideration when choosing a tonemapping curve. * * The algorithm conceptually is as follows: * 1. Partition the image into 128x128 chunks, computing the log2(maximum luminance) in each chunk *. a. Maximum luminance is computed as max(R, G, B), where the R, G, B values are in linear *. luminance on a scale defined by the destination color gamut. Max(R, G, B) has been found *. to minimize difference in hue while restricting to typical LDR color volumes. See: Burke, *. Adam & Smith, Michael & Zink, Michael. 2020. Color Volume and Hue-preservation in HDR *. Tone Mapping. SMPTE Motion Imaging Journal. *. b. Each computed luminance is lower-bounded by 1.0 in Skia's color *. management, or 203 nits. * 2. Blur the resulting chunks using a 5x5 gaussian kernel, to smooth out the local luminance map. * 3. Now, for each pixel in the original image: * a. Upsample from the blurred chunks of luminance computed in (2). Call this luminance value *. L: an estimate of the maximum luminance of surrounding pixels. *. b. If the luminance is less than 1.0 (203 nits), then do not modify the pixel value of the *. original image. *. c. Otherwise, *. parameterize a tone-mapping curve using a method described by Chrome: *. https://docs.google.com/document/d/17T2ek1i2R7tXdfHCnM-i5n6__RoYe0JyMfKmTEjoGR8/. *. i. Compute a gain G = (1 + max(linear R, linear G, linear B) / (L * L)) *. / (1 + max(linear R, linear G, linear B)). Note the similarity with the 1D curve *. described by Erik Reinhard, Michael Stark, Peter Shirley, and James Ferwerda. 2002. *. Photographic tone reproduction for digital images. ACM Trans. Graph. *. ii. Multiply G by the linear source colors to compute the final colors. * * Because it is a multi-renderpass algorithm requiring multiple off-screen textures, MouriMap is * typically not suitable to be ran "frequently", at high refresh rates (e.g., 120hz). However, * MouriMap is sufficiently fast enough for infrequent composition where preserving SDR detail is * most important, such as for screenshots. */ class MouriMap { public: MouriMap(); // Apply the MouriMap tonemmaping operator to the input. // The HDR/SDR ratio describes the luminace range of the input. 1.0 means SDR. Anything larger // then 1.0 means that there is headroom above the SDR region. sk_sp<SkShader> mouriMap(SkiaGpuContext* context, sk_sp<SkShader> input, float hdrSdrRatio); private: sk_sp<SkImage> downchunk(SkiaGpuContext* context, sk_sp<SkShader> input, float hdrSdrRatio) const; sk_sp<SkImage> blur(SkiaGpuContext* context, SkImage* input) const; sk_sp<SkShader> tonemap(sk_sp<SkShader> input, SkImage* localLux, float hdrSdrRatio) const; const sk_sp<SkRuntimeEffect> mCrosstalkAndChunk16x16; const sk_sp<SkRuntimeEffect> mChunk8x8; const sk_sp<SkRuntimeEffect> mBlur; const sk_sp<SkRuntimeEffect> mTonemap; }; } // namespace skia } // namespace renderengine } // namespace android No newline at end of file