Flutter Engine
The Flutter Engine
SkMatrixConvolutionImageFilter.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2012 The Android Open Source Project
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
7
9
17#include "include/core/SkM44.h"
19#include "include/core/SkRect.h"
24#include "include/core/SkSize.h"
33#include "src/base/SkSafeMath.h"
39#include "src/core/SkRectPriv.h"
41
42#include <cstdint>
43#include <cstring>
44#include <optional>
45#include <utility>
46
47using namespace skia_private;
48using namespace MatrixConvolutionImageFilter;
49
50namespace {
51
52static_assert(kLargeKernelSize % 4 == 0, "Must be a multiple of 4");
53static_assert(kSmallKernelSize <= kLargeKernelSize, "Small kernel size must be <= max size");
54// The uniform array storing the kernel is packed into half4's so that we don't waste space
55// forcing array elements out to 16-byte alignment when using std140.
56static_assert(kMaxUniformKernelSize % 4 == 0, "Must be a multiple of 4");
57
58SkBitmap create_kernel_bitmap(const SkISize& kernelSize, const float* kernel,
59 float* innerGain, float* innerBias);
60
61class SkMatrixConvolutionImageFilter final : public SkImageFilter_Base {
62public:
63 SkMatrixConvolutionImageFilter(const SkISize& kernelSize, const SkScalar* kernel,
64 SkScalar gain, SkScalar bias, const SkIPoint& kernelOffset,
65 bool convolveAlpha, sk_sp<SkImageFilter> const* input)
66 : SkImageFilter_Base(input, 1)
67 , fKernel(kernel, kernelSize.width() * kernelSize.height())
68 , fKernelSize(kernelSize)
69 , fKernelOffset({kernelOffset.fX, kernelOffset.fY})
70 , fGain(gain)
71 , fBias(bias)
72 , fConvolveAlpha(convolveAlpha) {
73 // The public factory should have ensured these before creating this object.
74 SkASSERT(SkSafeMath::Mul(kernelSize.fWidth, kernelSize.fHeight) <= kLargeKernelSize);
75 SkASSERT(kernelSize.fWidth >= 1 && kernelSize.fHeight >= 1);
76 SkASSERT(kernelOffset.fX >= 0 && kernelOffset.fX < kernelSize.fWidth);
77 SkASSERT(kernelOffset.fY >= 0 && kernelOffset.fY < kernelSize.fHeight);
78
79 // Does nothing for small kernels, otherwise encodes kernel into an A8 image.
80 fKernelBitmap = create_kernel_bitmap(kernelSize, kernel, &fInnerGain, &fInnerBias);
81 }
82
83 SkRect computeFastBounds(const SkRect& bounds) const override;
84
85protected:
86 void flatten(SkWriteBuffer&) const override;
87
88private:
90 SK_FLATTENABLE_HOOKS(SkMatrixConvolutionImageFilter)
91
92 bool onAffectsTransparentBlack() const override {
93 // affectsTransparentBlack() is conflated with "canComputeFastBounds" and MatrixConvolution
94 // is unique in that it might not produce unbounded output, but we can't calculate the
95 // fast bounds because the kernel is applied in device space and no transform is provided
96 // with that API.
97 // TODO(skbug.com/14617): Accept a matrix in computeFastBounds() so that we can handle the
98 // layer-space kernel case.
99
100 // That issue aside, a matrix convolution can affect transparent black when it has a
101 // non-zero bias and convolves alpha (if it doesn't convolve the alpha channel then the bias
102 // applied to RGB doesn't matter for transparent black pixels).
103 // NOTE: The crop image filters that wrap the matrix convolution to apply tile modes will
104 // reset this property when possible.
105 return true;
106 }
107
108 skif::FilterResult onFilterImage(const skif::Context& context) const override;
109
111 const skif::Mapping& mapping,
112 const skif::LayerSpace<SkIRect>& desiredOutput,
113 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
114
115 std::optional<skif::LayerSpace<SkIRect>> onGetOutputLayerBounds(
116 const skif::Mapping& mapping,
117 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
118
119 // Helper functions to adjust 'bounds' by the kernel size and offset, either for what would be
120 // sampled when covering 'bounds', or what could produce values when applied to 'bounds'.
121 skif::LayerSpace<SkIRect> boundsSampledByKernel(const skif::LayerSpace<SkIRect>& bounds) const;
122 skif::LayerSpace<SkIRect> boundsAffectedByKernel(const skif::LayerSpace<SkIRect>& bounds) const;
123
124 sk_sp<SkShader> createShader(const skif::Context& ctx, sk_sp<SkShader> input) const;
125
126 // Original kernel data, preserved for serialization even if it was encoded into fKernelBitmap
127 TArray<float> fKernel;
128
129 // Unlike the majority of image filters, the kernel is applied as-is to the layer-space pixels.
130 // This means that the kernel size and offset are always in the layer coordinate system.
131 skif::LayerSpace<SkISize> fKernelSize;
133
134 float fGain;
135 float fBias; // NOTE: This is assumed to be in [0-255] for historical reasons
136 bool fConvolveAlpha;
137
138 // Derived from fKernel when larger than what we will upload as uniforms; fInnerBias and
139 // fInnerGain reconstruct the original coefficient from unorm8 data as (a+innerBias)*innerGain
140 // Since these are derived, they are not serialized.
141 SkBitmap fKernelBitmap;
142 float fInnerBias;
143 float fInnerGain;
144};
145
146// LayerSpace doesn't have a clean type to represent 4 separate edge deltas, but the result
147// is a valid layer-space rectangle, so just go back to the underlying SkIRect temporarily.
149 int dl, int dt, int dr, int db) {
150 SkIRect adjusted = SkIRect(rect);
151 adjusted.adjust(dl, dt, dr, db);
152 return skif::LayerSpace<SkIRect>(adjusted);
153}
154
155std::pair<int, SkKnownRuntimeEffects::StableKey> quantize_by_kernel_size(int kernelSize) {
156 if (kernelSize < kMaxUniformKernelSize) {
157 return { kMaxUniformKernelSize, SkKnownRuntimeEffects::StableKey::kMatrixConvUniforms };
158 } else if (kernelSize <= kSmallKernelSize) {
159 return { kSmallKernelSize, SkKnownRuntimeEffects::StableKey::kMatrixConvTexSm };
160 }
161
162 return { kLargeKernelSize, SkKnownRuntimeEffects::StableKey::kMatrixConvTexLg };
163}
164
165SkBitmap create_kernel_bitmap(const SkISize& kernelSize, const float* kernel,
166 float* innerGain, float* innerBias) {
167 int length = kernelSize.fWidth * kernelSize.fHeight;
168 auto [quantizedKernelSize, key] = quantize_by_kernel_size(length);
169 if (key == SkKnownRuntimeEffects::StableKey::kMatrixConvUniforms) {
170 // No bitmap is needed to store the kernel on the GPU
171 *innerGain = 1.f;
172 *innerBias = 0.f;
173 return {};
174 }
175
176
177 // The convolution kernel is "big". The SVG spec has no upper limit on what's supported so
178 // store the kernel in a SkBitmap that will be uploaded to a data texture. We could
179 // implement a more straight forward evaluation loop for the CPU backend, but kernels of
180 // this size are already going to be very slow so we accept the extra indirection to
181 // keep the code paths consolidated.
182 //
183 // We store the data in A8 for universal support, but this requires normalizing the values
184 // and adding an extra inner bias operation to the shader. We could store values in A16 or
185 // A32 for improved accuracy but that would require querying GPU capabilities, which
186 // prevents creating the bitmap once during initialization. Even on the GPU, kernels larger
187 // than 5x5 quickly exceed realtime capabilities, so the loss of precision isn't a great
188 // concern either.
189 float min = kernel[0];
190 float max = kernel[0];
191 for (int i = 1; i < length; ++i) {
192 if (kernel[i] < min) {
193 min = kernel[i];
194 }
195 if (kernel[i] > max) {
196 max = kernel[i];
197 }
198 }
199
200 *innerGain = max - min;
201 *innerBias = min;
202 // Treat a near-0 gain (i.e. box blur) as 1 and let innerBias move everything to final value.
203 if (SkScalarNearlyZero(*innerGain)) {
204 *innerGain = 1.f;
205 }
206
207 SkBitmap kernelBM;
208 if (!kernelBM.tryAllocPixels(SkImageInfo::Make({ quantizedKernelSize, 1 },
211 // OOM so return an empty bitmap, which will be detected later on in onFilterImage().
212 return {};
213 }
214
215 for (int i = 0; i < length; ++i) {
216 *kernelBM.getAddr8(i, 0) = SkScalarRoundToInt(255 * (kernel[i] - min) / *innerGain);
217 }
218 for (int i = length; i < quantizedKernelSize; ++i) {
219 *kernelBM.getAddr8(i, 0) = 0;
220 }
221
222 kernelBM.setImmutable();
223 return kernelBM;
224}
225
226} // anonymous namespace
227
229 const SkScalar kernel[],
230 SkScalar gain,
231 SkScalar bias,
232 const SkIPoint& kernelOffset,
233 SkTileMode tileMode,
234 bool convolveAlpha,
236 const CropRect& cropRect) {
237 if (kernelSize.width() < 1 || kernelSize.height() < 1) {
238 return nullptr;
239 }
240 if (SkSafeMath::Mul(kernelSize.width(), kernelSize.height()) > kLargeKernelSize) {
241 return nullptr;
242 }
243 if (!kernel) {
244 return nullptr;
245 }
246 if ((kernelOffset.fX < 0) || (kernelOffset.fX >= kernelSize.fWidth) ||
247 (kernelOffset.fY < 0) || (kernelOffset.fY >= kernelSize.fHeight)) {
248 return nullptr;
249 }
250
251 // The 'tileMode' behavior is not well-defined if there is no crop, so we only apply it if
252 // there is a provided 'cropRect'.
253 sk_sp<SkImageFilter> filter = std::move(input);
254 if (cropRect && tileMode != SkTileMode::kDecal) {
255 // Historically the input image was restricted to the cropRect when tiling was not kDecal
256 // so that the kernel evaluated the tiled edge conditions, while a kDecal crop only affected
257 // the output.
258 filter = SkImageFilters::Crop(*cropRect, tileMode, std::move(filter));
259 }
260 filter = sk_sp<SkImageFilter>(new SkMatrixConvolutionImageFilter(
261 kernelSize, kernel, gain, bias, kernelOffset, convolveAlpha, &filter));
262 if (cropRect) {
263 // But regardless of the tileMode, the output is decal cropped.
264 filter = SkImageFilters::Crop(*cropRect, SkTileMode::kDecal, std::move(filter));
265 }
266 return filter;
267}
268
270 SK_REGISTER_FLATTENABLE(SkMatrixConvolutionImageFilter);
271 // TODO (michaelludwig) - Remove after grace period for SKPs to stop using old name
272 SkFlattenable::Register("SkMatrixConvolutionImageFilterImpl",
273 SkMatrixConvolutionImageFilter::CreateProc);
274}
275
276sk_sp<SkFlattenable> SkMatrixConvolutionImageFilter::CreateProc(SkReadBuffer& buffer) {
278
279 SkISize kernelSize;
280 kernelSize.fWidth = buffer.readInt();
281 kernelSize.fHeight = buffer.readInt();
282 const int count = buffer.getArrayCount();
283
284 const int64_t kernelArea = sk_64_mul(kernelSize.width(), kernelSize.height());
285 if (!buffer.validate(kernelArea == count)) {
286 return nullptr;
287 }
288 if (!buffer.validateCanReadN<SkScalar>(count)) {
289 return nullptr;
290 }
292 if (!buffer.readScalarArray(kernel.get(), count)) {
293 return nullptr;
294 }
295 SkScalar gain = buffer.readScalar();
296 SkScalar bias = buffer.readScalar();
297 SkIPoint kernelOffset;
298 kernelOffset.fX = buffer.readInt();
299 kernelOffset.fY = buffer.readInt();
300
303 tileMode = buffer.read32LE(SkTileMode::kLastTileMode);
304 } // else SkCropImageFilter handles the tile mode (if any)
305
306 bool convolveAlpha = buffer.readBool();
307
308 if (!buffer.isValid()) {
309 return nullptr;
310 }
311 // NOTE: For SKPs with version >= kConvolutionImageFilterTilingUpdate, tileMode will be kDecal
312 // and common.cropRect() will be null (so the factory also ignores tileMode). Any
313 // cropping/tiling will have been handled by the deserialized input/output Crop image filters.
315 kernelSize, kernel.get(), gain, bias, kernelOffset, tileMode,
316 convolveAlpha, common.getInput(0), common.cropRect());
317}
318
319void SkMatrixConvolutionImageFilter::flatten(SkWriteBuffer& buffer) const {
320 this->SkImageFilter_Base::flatten(buffer);
321 buffer.writeInt(fKernelSize.width());
322 buffer.writeInt(fKernelSize.height());
323 buffer.writeScalarArray(fKernel.data(), fKernel.size());
324 buffer.writeScalar(fGain);
325 buffer.writeScalar(fBias);
326 buffer.writeInt(fKernelOffset.x());
327 buffer.writeInt(fKernelOffset.y());
328 buffer.writeBool(fConvolveAlpha);
329}
330
331///////////////////////////////////////////////////////////////////////////////////////////////////
332
333skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::boundsSampledByKernel(
334 const skif::LayerSpace<SkIRect>& bounds) const {
335 return adjust(bounds,
336 -fKernelOffset.x(),
337 -fKernelOffset.y(),
338 fKernelSize.width() - fKernelOffset.x() - 1,
339 fKernelSize.height() - fKernelOffset.y() - 1);
340}
341
342skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::boundsAffectedByKernel(
343 const skif::LayerSpace<SkIRect>& bounds) const {
344 return adjust(bounds,
345 fKernelOffset.x() - fKernelSize.width() + 1,
346 fKernelOffset.y() - fKernelSize.height() + 1,
347 fKernelOffset.x(),
348 fKernelOffset.y());
349}
350
351
352
353sk_sp<SkShader> SkMatrixConvolutionImageFilter::createShader(const skif::Context& ctx,
354 sk_sp<SkShader> input) const {
355 const int kernelLength = fKernelSize.width() * fKernelSize.height();
356 auto [_, key] = quantize_by_kernel_size(kernelLength);
357 const bool useTextureShader = (key != SkKnownRuntimeEffects::StableKey::kMatrixConvUniforms);
358 if (useTextureShader && fKernelBitmap.empty()) {
359 return nullptr; // No actual kernel data to work with from a prior OOM
360 }
361
362 const SkRuntimeEffect* matrixConvEffect = GetKnownRuntimeEffect(key);
363
364 SkRuntimeShaderBuilder builder(sk_ref_sp(matrixConvEffect));
365 builder.child("child") = std::move(input);
366
367 if (useTextureShader) {
368 sk_sp<SkImage> cachedKernel = ctx.backend()->getCachedBitmap(fKernelBitmap);
369 if (!cachedKernel) {
370 return nullptr;
371 }
372 builder.child("kernel") = cachedKernel->makeRawShader(SkFilterMode::kNearest);
373 builder.uniform("innerGainAndBias") = SkV2{fInnerGain, fInnerBias};
374 } else {
375 float paddedKernel[kMaxUniformKernelSize];
376 memcpy(paddedKernel, fKernel.data(), kernelLength*sizeof(float));
377 memset(paddedKernel+kernelLength, 0, (kMaxUniformKernelSize - kernelLength)*sizeof(float));
378
379 builder.uniform("kernel").set(paddedKernel, kMaxUniformKernelSize);
380 }
381
382 builder.uniform("size") = SkISize(fKernelSize);
383 builder.uniform("offset") = skif::IVector(fKernelOffset);
384 // Scale the user-provided bias by 1/255 to match the [0,1] color channel range
385 builder.uniform("gainAndBias") = SkV2{fGain, fBias / 255.f};
386 builder.uniform("convolveAlpha") = fConvolveAlpha ? 1 : 0;
387
388 return builder.makeShader();
389}
390
391skif::FilterResult SkMatrixConvolutionImageFilter::onFilterImage(
392 const skif::Context& context) const {
393 using ShaderFlags = skif::FilterResult::ShaderFlags;
394
395 skif::LayerSpace<SkIRect> requiredInput = this->boundsSampledByKernel(context.desiredOutput());
396 skif::FilterResult childOutput =
397 this->getChildOutput(0, context.withNewDesiredOutput(requiredInput));
398
399 skif::LayerSpace<SkIRect> outputBounds;
400 if (fConvolveAlpha && fBias != 0.f) {
401 // The convolution will produce a non-trivial value for every pixel so fill desired output.
402 outputBounds = context.desiredOutput();
403 } else {
404 // Calculate the possible extent of the convolution given what was actually produced by the
405 // child filter and then intersect that with the desired output.
406 outputBounds = this->boundsAffectedByKernel(childOutput.layerBounds());
407 if (!outputBounds.intersect(context.desiredOutput())) {
408 return {};
409 }
410 }
411
413 builder.add(childOutput,
414 this->boundsSampledByKernel(outputBounds),
415 ShaderFlags::kSampledRepeatedly);
416 return builder.eval([&](SkSpan<sk_sp<SkShader>> inputs) {
417 return this->createShader(context, inputs[0]);
418 }, outputBounds);
419}
420
421skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::onGetInputLayerBounds(
422 const skif::Mapping& mapping,
423 const skif::LayerSpace<SkIRect>& desiredOutput,
424 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
425 // Adjust the desired output bounds by the kernel size to avoid evaluating edge conditions, and
426 // then recurse to the child filter.
427 skif::LayerSpace<SkIRect> requiredInput = this->boundsSampledByKernel(desiredOutput);
428 return this->getChildInputLayerBounds(0, mapping, requiredInput, contentBounds);
429}
430
431std::optional<skif::LayerSpace<SkIRect>> SkMatrixConvolutionImageFilter::onGetOutputLayerBounds(
432 const skif::Mapping& mapping,
433 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
434 if (fConvolveAlpha && fBias != 0.f) {
435 // Applying the kernel as a convolution to fully transparent black will result in 0 for
436 // each channel, unless the bias itself shifts this "zero-point". However, when the alpha
437 // channel is not convolved, the original a=0 is preserved and producing a premul color
438 // discards the non-zero bias. Convolving the alpha channel and a non-zero bias can mean
439 // the transparent black pixels outside of any input image become non-transparent black.
441 }
442
443 // Otherwise apply the kernel to the output bounds of the child filter.
444 auto outputBounds = this->getChildOutputLayerBounds(0, mapping, contentBounds);
445 if (outputBounds) {
446 return this->boundsAffectedByKernel(*outputBounds);
447 } else {
449 }
450}
451
452SkRect SkMatrixConvolutionImageFilter::computeFastBounds(const SkRect& bounds) const {
453 // See onAffectsTransparentBlack(), but without knowing the local-to-device transform, we don't
454 // know how many pixels will be sampled by the kernel. Return unbounded to match the
455 // expectations of an image filter that "affects" transparent black.
457}
int count
Definition: FontMgrTest.cpp:50
@ kPremul_SkAlphaType
pixel components are premultiplied by alpha
Definition: SkAlphaType.h:29
#define SkASSERT(cond)
Definition: SkAssert.h:116
@ kAlpha_8_SkColorType
pixel with alpha in 8-bit byte
Definition: SkColorType.h:21
#define SK_FLATTENABLE_HOOKS(type)
#define SK_REGISTER_FLATTENABLE(type)
#define SK_IMAGEFILTER_UNFLATTEN_COMMON(localVar, expectedCount)
static int64_t sk_64_mul(int64_t a, int64_t b)
Definition: SkMath.h:33
void SkRegisterMatrixConvolutionImageFilterFlattenable()
sk_sp< T > sk_ref_sp(T *obj)
Definition: SkRefCnt.h:381
static bool SkScalarNearlyZero(SkScalar x, SkScalar tolerance=SK_ScalarNearlyZero)
Definition: SkScalar.h:101
#define SkScalarRoundToInt(x)
Definition: SkScalar.h:37
SkTileMode
Definition: SkTileMode.h:13
void setImmutable()
Definition: SkBitmap.cpp:400
uint8_t * getAddr8(int x, int y) const
Definition: SkBitmap.h:1270
bool tryAllocPixels(const SkImageInfo &info, size_t rowBytes)
Definition: SkBitmap.cpp:271
static void Register(const char name[], Factory)
virtual skif::LayerSpace< SkIRect > onGetInputLayerBounds(const skif::Mapping &mapping, const skif::LayerSpace< SkIRect > &desiredOutput, std::optional< skif::LayerSpace< SkIRect > > contentBounds) const =0
virtual bool onAffectsTransparentBlack() const
void flatten(SkWriteBuffer &) const override
virtual std::optional< skif::LayerSpace< SkIRect > > onGetOutputLayerBounds(const skif::Mapping &mapping, std::optional< skif::LayerSpace< SkIRect > > contentBounds) const =0
virtual skif::FilterResult onFilterImage(const skif::Context &context) const =0
virtual SkRect computeFastBounds(const SkRect &bounds) const
static sk_sp< SkImageFilter > MatrixConvolution(const SkISize &kernelSize, const SkScalar kernel[], SkScalar gain, SkScalar bias, const SkIPoint &kernelOffset, SkTileMode tileMode, bool convolveAlpha, sk_sp< SkImageFilter > input, const CropRect &cropRect={})
static sk_sp< SkImageFilter > Crop(const SkRect &rect, SkTileMode tileMode, sk_sp< SkImageFilter > input)
sk_sp< SkShader > makeRawShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions &, const SkMatrix *localMatrix=nullptr) const
Definition: SkImage.cpp:207
@ kConvolutionImageFilterTilingUpdate
static SkRect MakeLargeS32()
Definition: SkRectPriv.h:33
static size_t Mul(size_t x, size_t y)
Definition: SkSafeMath.cpp:16
virtual sk_sp< SkImage > getCachedBitmap(const SkBitmap &data) const =0
const Backend * backend() const
const LayerSpace< SkIRect > & desiredOutput() const
Context withNewDesiredOutput(const LayerSpace< SkIRect > &desiredOutput) const
LayerSpace< SkIRect > layerBounds() const
bool intersect(const LayerSpace< SkIRect > &r)
float SkScalar
Definition: extension.cpp:12
static float max(float r, float g, float b)
Definition: hsl.cpp:49
static float min(float r, float g, float b)
Definition: hsl.cpp:48
size_t length
const SkRuntimeEffect * GetKnownRuntimeEffect(StableKey stableKey)
Optional< SkRect > bounds
Definition: SkRecords.h:189
sk_sp< SkBlender > blender SkRect rect
Definition: SkRecords.h:350
Definition: common.py:1
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified vm service A custom Dart VM Service port The default is to pick a randomly available open port disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode disable vm service Disable mDNS Dart VM Service publication Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set endless trace buffer
Definition: switches.h:126
int32_t height
int32_t width
int32_t fX
x-axis value
Definition: SkPoint_impl.h:29
int32_t fY
y-axis value
Definition: SkPoint_impl.h:30
Definition: SkRect.h:32
void adjust(int32_t dL, int32_t dT, int32_t dR, int32_t dB)
Definition: SkRect.h:446
Definition: SkSize.h:16
int32_t fHeight
Definition: SkSize.h:18
int32_t fWidth
Definition: SkSize.h:17
constexpr int32_t width() const
Definition: SkSize.h:36
constexpr int32_t height() const
Definition: SkSize.h:37
static SkImageInfo Make(int width, int height, SkColorType ct, SkAlphaType at)
Definition: SkM44.h:19