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 * 16 + 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 * 8 + 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 vec4 main(vec2 xy) {
82 float localMax = lux.eval(xy * scaleFactor).r;
83 float4 rgba = image.eval(xy);
84 float3 linear = toLinearSrgb(rgba.rgb) * hdrSdrRatio;
85
86 if (localMax <= 1.0) {
87 return float4(fromLinearSrgb(linear), 1.0);
88 }
89
90 float maxRGB = max(linear.r, max(linear.g, linear.b));
91 localMax = max(localMax, maxRGB);
92 float gain = (1 + maxRGB / (localMax * localMax)) / (1 + maxRGB);
93 return float4(fromLinearSrgb(linear * gain), 1.0);
94 }
95 )");
96
97 // Draws the given runtime shader on a GPU surface and returns the result as an SkImage.
makeImage(SkSurface * surface,const SkRuntimeShaderBuilder & builder)98 sk_sp<SkImage> makeImage(SkSurface* surface, const SkRuntimeShaderBuilder& builder) {
99 sk_sp<SkShader> shader = builder.makeShader(nullptr);
100 LOG_ALWAYS_FATAL_IF(!shader, "%s, Failed to make shader!", __func__);
101 SkPaint paint;
102 paint.setShader(std::move(shader));
103 paint.setBlendMode(SkBlendMode::kSrc);
104 surface->getCanvas()->drawPaint(paint);
105 return surface->makeImageSnapshot();
106 }
107
108 } // namespace
109
MouriMap()110 MouriMap::MouriMap()
111 : mCrosstalkAndChunk16x16(makeEffect(kCrosstalkAndChunk16x16)),
112 mChunk8x8(makeEffect(kChunk8x8)),
113 mBlur(makeEffect(kBlur)),
114 mTonemap(makeEffect(kTonemap)) {}
115
mouriMap(SkiaGpuContext * context,sk_sp<SkShader> input,float hdrSdrRatio)116 sk_sp<SkShader> MouriMap::mouriMap(SkiaGpuContext* context, sk_sp<SkShader> input,
117 float hdrSdrRatio) {
118 auto downchunked = downchunk(context, input, hdrSdrRatio);
119 auto localLux = blur(context, downchunked.get());
120 return tonemap(input, localLux.get(), hdrSdrRatio);
121 }
122
downchunk(SkiaGpuContext * context,sk_sp<SkShader> input,float hdrSdrRatio) const123 sk_sp<SkImage> MouriMap::downchunk(SkiaGpuContext* context, sk_sp<SkShader> input,
124 float hdrSdrRatio) const {
125 SkMatrix matrix;
126 SkImage* image = input->isAImage(&matrix, (SkTileMode*)nullptr);
127 SkRuntimeShaderBuilder crosstalkAndChunk16x16Builder(mCrosstalkAndChunk16x16);
128 crosstalkAndChunk16x16Builder.child("bitmap") = input;
129 crosstalkAndChunk16x16Builder.uniform("hdrSdrRatio") = hdrSdrRatio;
130 // TODO: fp16 might be overkill. Most practical surfaces use 8-bit RGB for HDR UI and 10-bit YUV
131 // for HDR video. These downsample operations compute log2(max(linear RGB, 1.0)). So we don't
132 // care about LDR precision since they all resolve to LDR-max. For appropriately mastered HDR
133 // content that follows BT. 2408, 25% of the bit range for HLG and 42% of the bit range for PQ
134 // are reserved for HDR. This means that we can fit the entire HDR range for 10-bit HLG inside
135 // of 8 bits. We can also fit about half of the range for PQ, but most content does not fill the
136 // entire 10k nit range for PQ. Furthermore, we blur all of this later on anyways, so we might
137 // not need to be so precise. So, it's possible that we could use A8 or R8 instead. If we want
138 // to be really conservative we can try to use R16 or even RGBA1010102 to fake an R10 surface,
139 // which would cut write bandwidth significantly.
140 static constexpr auto kFirstDownscaleAmount = 16;
141 sk_sp<SkSurface> firstDownsampledSurface = context->createRenderTarget(
142 image->imageInfo()
143 .makeWH(std::max(1, image->width() / kFirstDownscaleAmount),
144 std::max(1, image->height() / kFirstDownscaleAmount))
145 .makeColorType(kRGBA_F16_SkColorType));
146 LOG_ALWAYS_FATAL_IF(!firstDownsampledSurface, "%s: Failed to create surface!", __func__);
147 auto firstDownsampledImage =
148 makeImage(firstDownsampledSurface.get(), crosstalkAndChunk16x16Builder);
149 SkRuntimeShaderBuilder chunk8x8Builder(mChunk8x8);
150 chunk8x8Builder.child("bitmap") =
151 firstDownsampledImage->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp,
152 SkSamplingOptions());
153 static constexpr auto kSecondDownscaleAmount = 8;
154 sk_sp<SkSurface> secondDownsampledSurface = context->createRenderTarget(
155 firstDownsampledImage->imageInfo()
156 .makeWH(std::max(1, firstDownsampledImage->width() / kSecondDownscaleAmount),
157 std::max(1, firstDownsampledImage->height() / kSecondDownscaleAmount)));
158 LOG_ALWAYS_FATAL_IF(!secondDownsampledSurface, "%s: Failed to create surface!", __func__);
159 return makeImage(secondDownsampledSurface.get(), chunk8x8Builder);
160 }
blur(SkiaGpuContext * context,SkImage * input) const161 sk_sp<SkImage> MouriMap::blur(SkiaGpuContext* context, SkImage* input) const {
162 SkRuntimeShaderBuilder blurBuilder(mBlur);
163 blurBuilder.child("bitmap") =
164 input->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions());
165 sk_sp<SkSurface> blurSurface = context->createRenderTarget(input->imageInfo());
166 LOG_ALWAYS_FATAL_IF(!blurSurface, "%s: Failed to create surface!", __func__);
167 return makeImage(blurSurface.get(), blurBuilder);
168 }
tonemap(sk_sp<SkShader> input,SkImage * localLux,float hdrSdrRatio) const169 sk_sp<SkShader> MouriMap::tonemap(sk_sp<SkShader> input, SkImage* localLux,
170 float hdrSdrRatio) const {
171 static constexpr float kScaleFactor = 1.0f / 128.0f;
172 SkRuntimeShaderBuilder tonemapBuilder(mTonemap);
173 tonemapBuilder.child("image") = input;
174 tonemapBuilder.child("lux") =
175 localLux->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp,
176 SkSamplingOptions(SkFilterMode::kLinear, SkMipmapMode::kNone));
177 tonemapBuilder.uniform("scaleFactor") = kScaleFactor;
178 tonemapBuilder.uniform("hdrSdrRatio") = hdrSdrRatio;
179 return tonemapBuilder.makeShader();
180 }
181 } // namespace skia
182 } // namespace renderengine
183 } // namespace android