1 /*
2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 #include "MouriMap.h"
17 #include <SkCanvas.h>
18 #include <SkColorType.h>
19 #include <SkPaint.h>
20 #include <SkTileMode.h>
21
22 namespace android {
23 namespace renderengine {
24 namespace skia {
25 namespace {
makeEffect(const SkString & sksl)26 sk_sp<SkRuntimeEffect> makeEffect(const SkString& sksl) {
27 auto [effect, error] = SkRuntimeEffect::MakeForShader(sksl);
28 LOG_ALWAYS_FATAL_IF(!effect, "RuntimeShader error: %s", error.c_str());
29 return effect;
30 }
31 const SkString kCrosstalkAndChunk16x16(R"(
32 uniform shader bitmap;
33 uniform float hdrSdrRatio;
34 vec4 main(vec2 xy) {
35 float maximum = 0.0;
36 for (int y = 0; y < 16; y++) {
37 for (int x = 0; x < 16; x++) {
38 float3 linear = toLinearSrgb(bitmap.eval((xy - 0.5) * 16 + 0.5 + vec2(x, y)).rgb) * hdrSdrRatio;
39 float maxRGB = max(linear.r, max(linear.g, linear.b));
40 maximum = max(maximum, log2(max(maxRGB, 1.0)));
41 }
42 }
43 return float4(float3(maximum), 1.0);
44 }
45 )");
46 const SkString kChunk8x8(R"(
47 uniform shader bitmap;
48 vec4 main(vec2 xy) {
49 float maximum = 0.0;
50 for (int y = 0; y < 8; y++) {
51 for (int x = 0; x < 8; x++) {
52 maximum = max(maximum, bitmap.eval((xy - 0.5) * 8 + 0.5 + vec2(x, y)).r);
53 }
54 }
55 return float4(float3(maximum), 1.0);
56 }
57 )");
58 const SkString kBlur(R"(
59 uniform shader bitmap;
60 vec4 main(vec2 xy) {
61 float C[5];
62 C[0] = 1.0 / 16.0;
63 C[1] = 4.0 / 16.0;
64 C[2] = 6.0 / 16.0;
65 C[3] = 4.0 / 16.0;
66 C[4] = 1.0 / 16.0;
67 float result = 0.0;
68 for (int y = -2; y <= 2; y++) {
69 for (int x = -2; x <= 2; x++) {
70 result += C[y + 2] * C[x + 2] * bitmap.eval(xy + vec2(x, y)).r;
71 }
72 }
73 return float4(float3(exp2(result)), 1.0);
74 }
75 )");
76 const SkString kTonemap(R"(
77 uniform shader image;
78 uniform shader lux;
79 uniform float scaleFactor;
80 uniform float hdrSdrRatio;
81 uniform float targetHdrSdrRatio;
82 vec4 main(vec2 xy) {
83 float localMax = lux.eval(xy * scaleFactor).r;
84 float4 rgba = image.eval(xy);
85 float3 linear = toLinearSrgb(rgba.rgb) * hdrSdrRatio;
86
87 if (localMax <= targetHdrSdrRatio) {
88 return float4(fromLinearSrgb(linear), rgba.a);
89 }
90
91 float maxRGB = max(linear.r, max(linear.g, linear.b));
92 localMax = max(localMax, maxRGB);
93 float gain = (1 + maxRGB * (targetHdrSdrRatio / (localMax * localMax)))
94 / (1 + maxRGB / targetHdrSdrRatio);
95 return float4(fromLinearSrgb(linear * gain), rgba.a);
96 }
97 )");
98
99 // Draws the given runtime shader on a GPU surface and returns the result as an SkImage.
makeImage(SkSurface * surface,const SkRuntimeShaderBuilder & builder)100 sk_sp<SkImage> makeImage(SkSurface* surface, const SkRuntimeShaderBuilder& builder) {
101 sk_sp<SkShader> shader = builder.makeShader(nullptr);
102 LOG_ALWAYS_FATAL_IF(!shader, "%s, Failed to make shader!", __func__);
103 SkPaint paint;
104 paint.setShader(std::move(shader));
105 paint.setBlendMode(SkBlendMode::kSrc);
106 surface->getCanvas()->drawPaint(paint);
107 return surface->makeImageSnapshot();
108 }
109
110 } // namespace
111
MouriMap()112 MouriMap::MouriMap()
113 : mCrosstalkAndChunk16x16(makeEffect(kCrosstalkAndChunk16x16)),
114 mChunk8x8(makeEffect(kChunk8x8)),
115 mBlur(makeEffect(kBlur)),
116 mTonemap(makeEffect(kTonemap)) {}
117
mouriMap(SkiaGpuContext * context,sk_sp<SkShader> input,float hdrSdrRatio,float targetHdrSdrRatio)118 sk_sp<SkShader> MouriMap::mouriMap(SkiaGpuContext* context, sk_sp<SkShader> input,
119 float hdrSdrRatio, float targetHdrSdrRatio) {
120 auto downchunked = downchunk(context, input, hdrSdrRatio);
121 auto localLux = blur(context, downchunked.get());
122 return tonemap(input, localLux.get(), hdrSdrRatio, targetHdrSdrRatio);
123 }
124
downchunk(SkiaGpuContext * context,sk_sp<SkShader> input,float hdrSdrRatio) const125 sk_sp<SkImage> MouriMap::downchunk(SkiaGpuContext* context, sk_sp<SkShader> input,
126 float hdrSdrRatio) const {
127 SkMatrix matrix;
128 SkImage* image = input->isAImage(&matrix, (SkTileMode*)nullptr);
129 SkRuntimeShaderBuilder crosstalkAndChunk16x16Builder(mCrosstalkAndChunk16x16);
130 crosstalkAndChunk16x16Builder.child("bitmap") = input;
131 crosstalkAndChunk16x16Builder.uniform("hdrSdrRatio") = hdrSdrRatio;
132 // TODO: fp16 might be overkill. Most practical surfaces use 8-bit RGB for HDR UI and 10-bit YUV
133 // for HDR video. These downsample operations compute log2(max(linear RGB, 1.0)). So we don't
134 // care about LDR precision since they all resolve to LDR-max. For appropriately mastered HDR
135 // content that follows BT. 2408, 25% of the bit range for HLG and 42% of the bit range for PQ
136 // are reserved for HDR. This means that we can fit the entire HDR range for 10-bit HLG inside
137 // of 8 bits. We can also fit about half of the range for PQ, but most content does not fill the
138 // entire 10k nit range for PQ. Furthermore, we blur all of this later on anyways, so we might
139 // not need to be so precise. So, it's possible that we could use A8 or R8 instead. If we want
140 // to be really conservative we can try to use R16 or even RGBA1010102 to fake an R10 surface,
141 // which would cut write bandwidth significantly.
142 static constexpr auto kFirstDownscaleAmount = 16;
143 sk_sp<SkSurface> firstDownsampledSurface = context->createRenderTarget(
144 image->imageInfo()
145 .makeWH(std::max(1, image->width() / kFirstDownscaleAmount),
146 std::max(1, image->height() / kFirstDownscaleAmount))
147 .makeColorType(kRGBA_F16_SkColorType));
148 LOG_ALWAYS_FATAL_IF(!firstDownsampledSurface, "%s: Failed to create surface!", __func__);
149 auto firstDownsampledImage =
150 makeImage(firstDownsampledSurface.get(), crosstalkAndChunk16x16Builder);
151 SkRuntimeShaderBuilder chunk8x8Builder(mChunk8x8);
152 chunk8x8Builder.child("bitmap") =
153 firstDownsampledImage->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp,
154 SkSamplingOptions());
155 static constexpr auto kSecondDownscaleAmount = 8;
156 sk_sp<SkSurface> secondDownsampledSurface = context->createRenderTarget(
157 firstDownsampledImage->imageInfo()
158 .makeWH(std::max(1, firstDownsampledImage->width() / kSecondDownscaleAmount),
159 std::max(1, firstDownsampledImage->height() / kSecondDownscaleAmount)));
160 LOG_ALWAYS_FATAL_IF(!secondDownsampledSurface, "%s: Failed to create surface!", __func__);
161 return makeImage(secondDownsampledSurface.get(), chunk8x8Builder);
162 }
blur(SkiaGpuContext * context,SkImage * input) const163 sk_sp<SkImage> MouriMap::blur(SkiaGpuContext* context, SkImage* input) const {
164 SkRuntimeShaderBuilder blurBuilder(mBlur);
165 blurBuilder.child("bitmap") =
166 input->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions());
167 sk_sp<SkSurface> blurSurface = context->createRenderTarget(input->imageInfo());
168 LOG_ALWAYS_FATAL_IF(!blurSurface, "%s: Failed to create surface!", __func__);
169 return makeImage(blurSurface.get(), blurBuilder);
170 }
tonemap(sk_sp<SkShader> input,SkImage * localLux,float hdrSdrRatio,float targetHdrSdrRatio) const171 sk_sp<SkShader> MouriMap::tonemap(sk_sp<SkShader> input, SkImage* localLux, float hdrSdrRatio,
172 float targetHdrSdrRatio) const {
173 static constexpr float kScaleFactor = 1.0f / 128.0f;
174 SkRuntimeShaderBuilder tonemapBuilder(mTonemap);
175 tonemapBuilder.child("image") = input;
176 tonemapBuilder.child("lux") =
177 localLux->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp,
178 SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone));
179 tonemapBuilder.uniform("scaleFactor") = kScaleFactor;
180 tonemapBuilder.uniform("hdrSdrRatio") = hdrSdrRatio;
181 tonemapBuilder.uniform("targetHdrSdrRatio") = targetHdrSdrRatio;
182 return tonemapBuilder.makeShader();
183 }
184 } // namespace skia
185 } // namespace renderengine
186 } // namespace android
187