Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f2ea10cb authored by Alec Mouri's avatar Alec Mouri
Browse files

Introduce MouriMap

MouriMap is a local-tonemapping algorithm optimized for near-exact
preservation of SDR/LDR regions, while trying to do a good job of
rendering HDR. MouriMap was designed to run well on mobile hardware.

On a Pixel 8 Pro, MouriMap is able to tonemap screen-sized images
between 20 and 25 milliseconds. This is not fast enough for real-time
rendering at the panel refresh rate. But, this is sufficient for
screenshots, which is the use-case that MouriMap is intended to be
deployed for.

Tests will follow after this patch.

Bug: 329464641
Test: builds, boots
Test: Swipe apps into Recents
Test: adb screenshot
Change-Id: I0ded29b65ccf41940de74cff26d36275bfa46e78
parent a7e752e2
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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",
    ],
}
+17 −3
Original line number Diff line number Diff line
@@ -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"
@@ -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);
@@ -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 =
+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
+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