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

Commit fe0aa3b5 authored by Alec Mouri's avatar Alec Mouri Committed by Android (Google) Code Review
Browse files

Merge "Introduce MouriMap" into main

parents 9ad203c8 f2ea10cb
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