blob: 1e1c477cd5bf6da947a6b4022be99404e054c860 [file] [log] [blame]
tomhudson@google.comd8f856c2012-05-10 12:13:36 +00001/*
2 * Copyright 2012 Google Inc.
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
8#include "GrConvolutionEffect.h"
joshualittb0a8a372014-09-23 09:50:21 -07009#include "gl/GrGLProcessor.h"
tomhudson@google.comd8f856c2012-05-10 12:13:36 +000010#include "gl/GrGLTexture.h"
joshualitteb2a6762014-12-04 11:35:33 -080011#include "gl/builders/GrGLProgramBuilder.h"
tomhudson@google.comd8f856c2012-05-10 12:13:36 +000012
bsalomon@google.comdbbc4e22012-07-25 17:48:39 +000013// For brevity
kkinnunen7510b222014-07-30 00:04:16 -070014typedef GrGLProgramDataManager::UniformHandle UniformHandle;
bsalomon@google.com032b2212012-07-16 13:36:18 +000015
ericrk0f386122015-07-21 13:15:47 -070016/**
17 * Base class with shared functionality for GrGLBoundedConvolutionEffect and
18 * GrGLLerpConvolutionEffect.
19 */
joshualittb0a8a372014-09-23 09:50:21 -070020class GrGLConvolutionEffect : public GrGLFragmentProcessor {
tomhudson@google.comd8f856c2012-05-10 12:13:36 +000021public:
joshualitteb2a6762014-12-04 11:35:33 -080022 GrGLConvolutionEffect(const GrProcessor&);
jvanverthcfc18862015-04-28 08:48:20 -070023 static inline void GenKey(const GrProcessor&, const GrGLSLCaps&, GrProcessorKeyBuilder*);
tomhudson@google.comd8f856c2012-05-10 12:13:36 +000024
ericrk0f386122015-07-21 13:15:47 -070025protected:
26 int radius() const { return fRadius; }
scroggo39a24f22015-07-13 07:06:45 -070027 int width() const { return Gr1DKernelEffect::WidthFromRadius(fRadius); }
scroggo39a24f22015-07-13 07:06:45 -070028 Gr1DKernelEffect::Direction direction() const { return fDirection; }
ericrk0f386122015-07-21 13:15:47 -070029 void getImageIncrement(const GrConvolutionEffect&, float (*)[2]) const;
scroggo39a24f22015-07-13 07:06:45 -070030
ericrk0f386122015-07-21 13:15:47 -070031private:
32 int fRadius;
33 Gr1DKernelEffect::Direction fDirection;
tomhudson@google.comd8f856c2012-05-10 12:13:36 +000034
joshualittb0a8a372014-09-23 09:50:21 -070035 typedef GrGLFragmentProcessor INHERITED;
tomhudson@google.comd8f856c2012-05-10 12:13:36 +000036};
37
joshualitteb2a6762014-12-04 11:35:33 -080038GrGLConvolutionEffect::GrGLConvolutionEffect(const GrProcessor& processor) {
joshualittb0a8a372014-09-23 09:50:21 -070039 const GrConvolutionEffect& c = processor.cast<GrConvolutionEffect>();
bsalomon@google.comb505a122012-05-31 18:40:36 +000040 fRadius = c.radius();
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +000041 fDirection = c.direction();
tomhudson@google.comd8f856c2012-05-10 12:13:36 +000042}
43
ericrk0f386122015-07-21 13:15:47 -070044void GrGLConvolutionEffect::GenKey(const GrProcessor& processor,
45 const GrGLSLCaps&,
joshualittb0a8a372014-09-23 09:50:21 -070046 GrProcessorKeyBuilder* b) {
47 const GrConvolutionEffect& conv = processor.cast<GrConvolutionEffect>();
bsalomon63e99f72014-07-21 08:03:14 -070048 uint32_t key = conv.radius();
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +000049 key <<= 2;
50 if (conv.useBounds()) {
51 key |= 0x2;
52 key |= GrConvolutionEffect::kY_Direction == conv.direction() ? 0x1 : 0x0;
53 }
bsalomon63e99f72014-07-21 08:03:14 -070054 b->add32(key);
tomhudson@google.comd8f856c2012-05-10 12:13:36 +000055}
56
ericrk0f386122015-07-21 13:15:47 -070057void GrGLConvolutionEffect::getImageIncrement(const GrConvolutionEffect& conv,
58 float (*imageIncrement)[2]) const {
59 GrTexture& texture = *conv.texture(0);
60 (*imageIncrement)[0] = (*imageIncrement)[1] = 0;
61 float ySign = texture.origin() != kTopLeft_GrSurfaceOrigin ? 1.0f : -1.0f;
62 switch (conv.direction()) {
63 case Gr1DKernelEffect::kX_Direction:
64 (*imageIncrement)[0] = 1.0f / texture.width();
65 break;
66 case Gr1DKernelEffect::kY_Direction:
67 (*imageIncrement)[1] = ySign / texture.height();
68 break;
69 default:
70 SkFAIL("Unknown filter direction.");
71 }
72}
73
74///////////////////////////////////////////////////////////////////////////////
75
76/**
77 * Applies a convolution effect which restricts samples to the provided bounds
78 * using shader logic.
79 */
80class GrGLBoundedConvolutionEffect : public GrGLConvolutionEffect {
81public:
82 GrGLBoundedConvolutionEffect(const GrProcessor& processor) : INHERITED(processor) {}
83
84 virtual void emitCode(GrGLFPBuilder*,
85 const GrFragmentProcessor&,
86 const char* outputColor,
87 const char* inputColor,
88 const TransformedCoordsArray&,
89 const TextureSamplerArray&) override;
90
91 void setData(const GrGLProgramDataManager& pdman, const GrProcessor&) override;
92
93private:
94 UniformHandle fKernelUni;
95 UniformHandle fImageIncrementUni;
96 UniformHandle fBoundsUni;
97
98 typedef GrGLConvolutionEffect INHERITED;
99};
100
101void GrGLBoundedConvolutionEffect::emitCode(GrGLFPBuilder* builder,
102 const GrFragmentProcessor& processor,
103 const char* outputColor,
104 const char* inputColor,
105 const TransformedCoordsArray& coords,
106 const TextureSamplerArray& samplers) {
107 fImageIncrementUni =
108 builder->addUniform(GrGLProgramBuilder::kFragment_Visibility, kVec2f_GrSLType,
109 kDefault_GrSLPrecision, "ImageIncrement");
110
111 fBoundsUni = builder->addUniform(GrGLProgramBuilder::kFragment_Visibility, kVec2f_GrSLType,
112 kDefault_GrSLPrecision, "Bounds");
113
114 fKernelUni = builder->addUniformArray(GrGLProgramBuilder::kFragment_Visibility, kFloat_GrSLType,
115 kDefault_GrSLPrecision, "Kernel", this->width());
116
117 GrGLFragmentBuilder* fsBuilder = builder->getFragmentShaderBuilder();
118 SkString coords2D = fsBuilder->ensureFSCoords2D(coords, 0);
119
120 fsBuilder->codeAppendf("%s = vec4(0, 0, 0, 0);\n", outputColor);
121
122 int width = this->width();
123 const GrGLShaderVar& kernel = builder->getUniformVariable(fKernelUni);
124 const char* imgInc = builder->getUniformCStr(fImageIncrementUni);
125
126 fsBuilder->codeAppendf("vec2 coord = %s - %d.0 * %s;\n", coords2D.c_str(), this->radius(),
127 imgInc);
128
129 // Manually unroll loop because some drivers don't; yields 20-30% speedup.
130 for (int i = 0; i < width; i++) {
131 SkString index;
132 SkString kernelIndex;
133 index.appendS32(i);
134 kernel.appendArrayAccess(index.c_str(), &kernelIndex);
135 // We used to compute a bool indicating whether we're in bounds or not, cast it to a
136 // float, and then mul weight*texture_sample by the float. However, the Adreno 430 seems
137 // to have a bug that caused corruption.
138 const char* bounds = builder->getUniformCStr(fBoundsUni);
139 const char* component = this->direction() == Gr1DKernelEffect::kY_Direction ? "y" : "x";
140 fsBuilder->codeAppendf("if (coord.%s >= %s.x && coord.%s <= %s.y) {",
141 component, bounds, component, bounds);
142 fsBuilder->codeAppendf("%s += ", outputColor);
143 fsBuilder->appendTextureLookup(samplers[0], "coord");
144 fsBuilder->codeAppendf(" * %s;\n", kernelIndex.c_str());
145 fsBuilder->codeAppend("}");
146 fsBuilder->codeAppendf("coord += %s;\n", imgInc);
147 }
148
149 SkString modulate;
150 GrGLSLMulVarBy4f(&modulate, outputColor, inputColor);
151 fsBuilder->codeAppend(modulate.c_str());
152}
153
154void GrGLBoundedConvolutionEffect::setData(const GrGLProgramDataManager& pdman,
155 const GrProcessor& processor) {
156 const GrConvolutionEffect& conv = processor.cast<GrConvolutionEffect>();
157
158 // the code we generated was for a specific kernel radius
159 SkASSERT(conv.radius() == this->radius());
160
161 // the code we generated was for a specific bounding mode.
162 SkASSERT(conv.useBounds());
163
164 GrTexture& texture = *conv.texture(0);
165 float imageIncrement[2];
166 getImageIncrement(conv, &imageIncrement);
167 pdman.set2fv(fImageIncrementUni, 1, imageIncrement);
168 const float* bounds = conv.bounds();
169 if (Gr1DKernelEffect::kY_Direction == conv.direction() &&
170 texture.origin() != kTopLeft_GrSurfaceOrigin) {
171 pdman.set2f(fBoundsUni, 1.0f - bounds[1], 1.0f - bounds[0]);
172 } else {
173 pdman.set2f(fBoundsUni, bounds[0], bounds[1]);
174 }
175 pdman.set1fv(fKernelUni, this->width(), conv.kernel());
176}
177
178///////////////////////////////////////////////////////////////////////////////
179
180/**
181 * Applies a convolution effect which applies the convolution using a linear
182 * interpolation optimization to use half as many samples.
183 */
184class GrGLLerpConvolutionEffect : public GrGLConvolutionEffect {
185public:
186 GrGLLerpConvolutionEffect(const GrProcessor& processor) : INHERITED(processor) {}
187
188 virtual void emitCode(GrGLFPBuilder*,
189 const GrFragmentProcessor&,
190 const char* outputColor,
191 const char* inputColor,
192 const TransformedCoordsArray&,
193 const TextureSamplerArray&) override;
194
195 void setData(const GrGLProgramDataManager& pdman, const GrProcessor&) override;
196
197private:
198 int bilerpSampleCount() const;
199
200 // Bounded uniforms
201 UniformHandle fSampleWeightUni;
202 UniformHandle fSampleOffsetUni;
203
204 typedef GrGLConvolutionEffect INHERITED;
205};
206
207void GrGLLerpConvolutionEffect::emitCode(GrGLFPBuilder* builder,
208 const GrFragmentProcessor& processor,
209 const char* outputColor,
210 const char* inputColor,
211 const TransformedCoordsArray& coords,
212 const TextureSamplerArray& samplers) {
213 int sampleCount = bilerpSampleCount();
214
215 // We use 2 * sampleCount uniforms. The maximum allowed by PS2.0 is 32, so
216 // ensure we don't exceed this. Note that it is currently impossible to
217 // exceed this as bilerpSampleCount = (kernelWidth + 1) / 2, and kernelWidth
218 // maxes out at 25, resulting in a max sampleCount of 26.
219 SkASSERT(sampleCount < 16);
220
221 fSampleOffsetUni =
222 builder->addUniformArray(GrGLProgramBuilder::kFragment_Visibility, kVec2f_GrSLType,
223 kDefault_GrSLPrecision, "SampleOffset", sampleCount);
224 fSampleWeightUni =
225 builder->addUniformArray(GrGLProgramBuilder::kFragment_Visibility, kFloat_GrSLType,
226 kDefault_GrSLPrecision, "SampleWeight", sampleCount);
227
228 GrGLFragmentBuilder* fsBuilder = builder->getFragmentShaderBuilder();
229 SkString coords2D = fsBuilder->ensureFSCoords2D(coords, 0);
230
231 fsBuilder->codeAppendf("%s = vec4(0, 0, 0, 0);\n", outputColor);
232
233 const GrGLShaderVar& kernel = builder->getUniformVariable(fSampleWeightUni);
234 const GrGLShaderVar& imgInc = builder->getUniformVariable(fSampleOffsetUni);
235
236 fsBuilder->codeAppendf("vec2 coord; \n");
237
238 // Manually unroll loop because some drivers don't; yields 20-30% speedup.
239 for (int i = 0; i < sampleCount; i++) {
240 SkString index;
241 SkString weightIndex;
242 SkString offsetIndex;
243 index.appendS32(i);
244 kernel.appendArrayAccess(index.c_str(), &weightIndex);
245 imgInc.appendArrayAccess(index.c_str(), &offsetIndex);
246 fsBuilder->codeAppendf("coord = %s + %s;\n", coords2D.c_str(), offsetIndex.c_str());
247 fsBuilder->codeAppendf("%s += ", outputColor);
248 fsBuilder->appendTextureLookup(samplers[0], "coord");
249 fsBuilder->codeAppendf(" * %s;\n", weightIndex.c_str());
250 }
251
252 SkString modulate;
253 GrGLSLMulVarBy4f(&modulate, outputColor, inputColor);
254 fsBuilder->codeAppend(modulate.c_str());
255}
256
257void GrGLLerpConvolutionEffect::setData(const GrGLProgramDataManager& pdman,
258 const GrProcessor& processor) {
259 const GrConvolutionEffect& conv = processor.cast<GrConvolutionEffect>();
260
261 // the code we generated was for a specific kernel radius
262 SkASSERT(conv.radius() == this->radius());
263
264 // the code we generated was for a specific bounding mode.
265 SkASSERT(!conv.useBounds());
266
267 int sampleCount = bilerpSampleCount();
268 SkAutoTArray<float> imageIncrements(sampleCount * 2); // X and Y floats per sample.
269 SkAutoTArray<float> kernel(sampleCount);
270
271 float baseImageIncrement[2];
272 getImageIncrement(conv, &baseImageIncrement);
273
274 for (int i = 0; i < sampleCount; i++) {
275 int sampleIndex1 = i * 2;
276 int sampleIndex2 = sampleIndex1 + 1;
277
278 // If we have an odd number of samples in our filter, the last sample won't use
279 // the linear interpolation optimization (it will be pixel aligned).
280 if (sampleIndex2 >= this->width()) {
281 sampleIndex2 = sampleIndex1;
282 }
283
284 float kernelWeight1 = conv.kernel()[sampleIndex1];
285 float kernelWeight2 = conv.kernel()[sampleIndex2];
286
287 float totalKernelWeight =
288 (sampleIndex1 == sampleIndex2) ? kernelWeight1 : (kernelWeight1 + kernelWeight2);
289
290 float sampleRatio =
291 (sampleIndex1 == sampleIndex2) ? 0 : kernelWeight2 / (kernelWeight1 + kernelWeight2);
292
293 imageIncrements[i * 2] = (-this->radius() + i * 2 + sampleRatio) * baseImageIncrement[0];
294 imageIncrements[i * 2 + 1] =
295 (-this->radius() + i * 2 + sampleRatio) * baseImageIncrement[1];
296
297 kernel[i] = totalKernelWeight;
298 }
299 pdman.set2fv(fSampleOffsetUni, sampleCount, imageIncrements.get());
300 pdman.set1fv(fSampleWeightUni, sampleCount, kernel.get());
301}
302
303int GrGLLerpConvolutionEffect::bilerpSampleCount() const {
304 // We use a linear interpolation optimization to only sample once for each
305 // two pixel aligned samples in the kernel. If we have an odd number of
306 // samples, we will have to skip this optimization for the last sample.
307 // Because of this we always round up our sample count (by adding 1 before
308 // dividing).
309 return (this->width() + 1) / 2;
310}
311
bsalomon@google.comb505a122012-05-31 18:40:36 +0000312///////////////////////////////////////////////////////////////////////////////
tomhudson@google.comd8f856c2012-05-10 12:13:36 +0000313
joshualitt5f10b5c2015-07-09 10:24:35 -0700314GrConvolutionEffect::GrConvolutionEffect(GrProcessorDataManager* procDataManager,
315 GrTexture* texture,
tomhudson@google.comd0c1a062012-07-12 17:23:52 +0000316 Direction direction,
bsalomon@google.comb505a122012-05-31 18:40:36 +0000317 int radius,
senorblanco@chromium.org194d7752013-07-24 22:19:24 +0000318 const float* kernel,
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +0000319 bool useBounds,
320 float bounds[2])
ericrk0f386122015-07-21 13:15:47 -0700321 : INHERITED(procDataManager,
322 texture,
323 direction,
324 radius,
325 useBounds ? GrTextureParams::FilterMode::kNone_FilterMode
326 : GrTextureParams::FilterMode::kBilerp_FilterMode)
327 , fUseBounds(useBounds) {
joshualitteb2a6762014-12-04 11:35:33 -0800328 this->initClassID<GrConvolutionEffect>();
tfarina@chromium.orgf6de4752013-08-17 00:02:59 +0000329 SkASSERT(radius <= kMaxKernelRadius);
bsalomon49f085d2014-09-05 13:34:00 -0700330 SkASSERT(kernel);
bsalomon@google.comb505a122012-05-31 18:40:36 +0000331 int width = this->width();
bsalomon@google.comb4a55b72012-11-02 20:45:37 +0000332 for (int i = 0; i < width; i++) {
333 fKernel[i] = kernel[i];
tomhudson@google.comd8f856c2012-05-10 12:13:36 +0000334 }
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +0000335 memcpy(fBounds, bounds, sizeof(fBounds));
tomhudson@google.comd8f856c2012-05-10 12:13:36 +0000336}
337
joshualitt5f10b5c2015-07-09 10:24:35 -0700338GrConvolutionEffect::GrConvolutionEffect(GrProcessorDataManager* procDataManager,
339 GrTexture* texture,
tomhudson@google.comfde2c0a2012-07-16 12:23:32 +0000340 Direction direction,
341 int radius,
senorblanco@chromium.org194d7752013-07-24 22:19:24 +0000342 float gaussianSigma,
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +0000343 bool useBounds,
344 float bounds[2])
ericrk0f386122015-07-21 13:15:47 -0700345 : INHERITED(procDataManager,
346 texture,
347 direction,
348 radius,
349 useBounds ? GrTextureParams::FilterMode::kNone_FilterMode
350 : GrTextureParams::FilterMode::kBilerp_FilterMode)
351 , fUseBounds(useBounds) {
joshualitteb2a6762014-12-04 11:35:33 -0800352 this->initClassID<GrConvolutionEffect>();
tfarina@chromium.orgf6de4752013-08-17 00:02:59 +0000353 SkASSERT(radius <= kMaxKernelRadius);
tomhudson@google.comfde2c0a2012-07-16 12:23:32 +0000354 int width = this->width();
355
356 float sum = 0.0f;
357 float denom = 1.0f / (2.0f * gaussianSigma * gaussianSigma);
358 for (int i = 0; i < width; ++i) {
359 float x = static_cast<float>(i - this->radius());
360 // Note that the constant term (1/(sqrt(2*pi*sigma^2)) of the Gaussian
361 // is dropped here, since we renormalize the kernel below.
362 fKernel[i] = sk_float_exp(- x * x * denom);
363 sum += fKernel[i];
364 }
365 // Normalize the kernel
366 float scale = 1.0f / sum;
367 for (int i = 0; i < width; ++i) {
368 fKernel[i] *= scale;
369 }
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +0000370 memcpy(fBounds, bounds, sizeof(fBounds));
tomhudson@google.comfde2c0a2012-07-16 12:23:32 +0000371}
372
tomhudson@google.comd8f856c2012-05-10 12:13:36 +0000373GrConvolutionEffect::~GrConvolutionEffect() {
tomhudson@google.comd8f856c2012-05-10 12:13:36 +0000374}
375
jvanverthcfc18862015-04-28 08:48:20 -0700376void GrConvolutionEffect::getGLProcessorKey(const GrGLSLCaps& caps,
joshualitteb2a6762014-12-04 11:35:33 -0800377 GrProcessorKeyBuilder* b) const {
378 GrGLConvolutionEffect::GenKey(*this, caps, b);
379}
380
381GrGLFragmentProcessor* GrConvolutionEffect::createGLInstance() const {
ericrk0f386122015-07-21 13:15:47 -0700382 // We support a linear interpolation optimization which (when feasible) uses
383 // half the number of samples to apply the kernel. This is not always
384 // applicable, as the linear interpolation optimization does not support
385 // bounded sampling.
386 if (this->useBounds()) {
387 return SkNEW_ARGS(GrGLBoundedConvolutionEffect, (*this));
388 } else {
389 return SkNEW_ARGS(GrGLLerpConvolutionEffect, (*this));
390 }
tomhudson@google.comd8f856c2012-05-10 12:13:36 +0000391}
392
bsalomon0e08fc12014-10-15 08:19:04 -0700393bool GrConvolutionEffect::onIsEqual(const GrFragmentProcessor& sBase) const {
joshualitt49586be2014-09-16 08:21:41 -0700394 const GrConvolutionEffect& s = sBase.cast<GrConvolutionEffect>();
bsalomon420d7e92014-10-16 09:18:09 -0700395 return (this->radius() == s.radius() &&
tomhudson@google.comd0c1a062012-07-12 17:23:52 +0000396 this->direction() == s.direction() &&
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +0000397 this->useBounds() == s.useBounds() &&
398 0 == memcmp(fBounds, s.fBounds, sizeof(fBounds)) &&
tomhudson@google.comd0c1a062012-07-12 17:23:52 +0000399 0 == memcmp(fKernel, s.fKernel, this->width() * sizeof(float)));
tomhudson@google.comd8f856c2012-05-10 12:13:36 +0000400}
bsalomon@google.com0a7672f2012-08-03 18:12:20 +0000401
402///////////////////////////////////////////////////////////////////////////////
403
joshualittb0a8a372014-09-23 09:50:21 -0700404GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrConvolutionEffect);
bsalomon@google.com0a7672f2012-08-03 18:12:20 +0000405
joshualitt0067ff52015-07-08 14:26:19 -0700406GrFragmentProcessor* GrConvolutionEffect::TestCreate(GrProcessorTestData* d) {
407 int texIdx = d->fRandom->nextBool() ? GrProcessorUnitTest::kSkiaPMTextureIdx :
408 GrProcessorUnitTest::kAlphaTextureIdx;
409 Direction dir = d->fRandom->nextBool() ? kX_Direction : kY_Direction;
410 int radius = d->fRandom->nextRangeU(1, kMaxKernelRadius);
bungeman@google.com43486632013-08-20 15:20:34 +0000411 float kernel[kMaxKernelWidth];
412 for (size_t i = 0; i < SK_ARRAY_COUNT(kernel); ++i) {
joshualitt0067ff52015-07-08 14:26:19 -0700413 kernel[i] = d->fRandom->nextSScalar1();
bsalomon@google.com0a7672f2012-08-03 18:12:20 +0000414 }
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +0000415 float bounds[2];
bungeman@google.com43486632013-08-20 15:20:34 +0000416 for (size_t i = 0; i < SK_ARRAY_COUNT(bounds); ++i) {
joshualitt0067ff52015-07-08 14:26:19 -0700417 bounds[i] = d->fRandom->nextF();
senorblanco@chromium.org194d7752013-07-24 22:19:24 +0000418 }
bsalomon@google.com0a7672f2012-08-03 18:12:20 +0000419
joshualitt0067ff52015-07-08 14:26:19 -0700420 bool useBounds = d->fRandom->nextBool();
joshualitt5f10b5c2015-07-09 10:24:35 -0700421 return GrConvolutionEffect::Create(d->fProcDataManager,
422 d->fTextures[texIdx],
senorblanco@chromium.org194d7752013-07-24 22:19:24 +0000423 dir,
424 radius,
425 kernel,
senorblanco@chromium.orge8232bc2013-07-29 18:45:44 +0000426 useBounds,
427 bounds);
bsalomon@google.com0a7672f2012-08-03 18:12:20 +0000428}