Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 /* | 1 /* |
| 2 * Copyright 2015 Google Inc. | 2 * Copyright 2015 Google Inc. |
| 3 * | 3 * |
| 4 * Use of this source code is governed by a BSD-style license that can be | 4 * Use of this source code is governed by a BSD-style license that can be |
| 5 * found in the LICENSE file. | 5 * found in the LICENSE file. |
| 6 */ | 6 */ |
| 7 | 7 |
| 8 #include "GrCircleBlurFragmentProcessor.h" | 8 #include "GrCircleBlurFragmentProcessor.h" |
| 9 | 9 |
| 10 #if SK_SUPPORT_GPU | 10 #if SK_SUPPORT_GPU |
| 11 | 11 |
| 12 #include "GrContext.h" | 12 #include "GrContext.h" |
| 13 #include "GrInvariantOutput.h" | 13 #include "GrInvariantOutput.h" |
| 14 #include "GrTextureProvider.h" | 14 #include "GrTextureProvider.h" |
| 15 | 15 |
| 16 #include "glsl/GrGLSLFragmentProcessor.h" | 16 #include "glsl/GrGLSLFragmentProcessor.h" |
| 17 #include "glsl/GrGLSLFragmentShaderBuilder.h" | 17 #include "glsl/GrGLSLFragmentShaderBuilder.h" |
| 18 #include "glsl/GrGLSLProgramDataManager.h" | 18 #include "glsl/GrGLSLProgramDataManager.h" |
| 19 #include "glsl/GrGLSLUniformHandler.h" | 19 #include "glsl/GrGLSLUniformHandler.h" |
| 20 | 20 |
| 21 #include "SkFixed.h" | 21 #include "SkFixed.h" |
| 22 | 22 |
| 23 class GrGLCircleBlurFragmentProcessor : public GrGLSLFragmentProcessor { | 23 class GrCircleBlurFragmentProcessor::GLSLProcessor : public GrGLSLFragmentProces sor { |
| 24 public: | 24 public: |
| 25 void emitCode(EmitArgs&) override; | 25 void emitCode(EmitArgs&) override; |
| 26 | 26 |
| 27 protected: | 27 protected: |
| 28 void onSetData(const GrGLSLProgramDataManager&, const GrProcessor&) override ; | 28 void onSetData(const GrGLSLProgramDataManager&, const GrProcessor&) override ; |
| 29 | 29 |
| 30 private: | 30 private: |
| 31 GrGLSLProgramDataManager::UniformHandle fDataUniform; | 31 GrGLSLProgramDataManager::UniformHandle fDataUniform; |
| 32 | 32 |
| 33 typedef GrGLSLFragmentProcessor INHERITED; | 33 typedef GrGLSLFragmentProcessor INHERITED; |
| 34 }; | 34 }; |
| 35 | 35 |
| 36 void GrGLCircleBlurFragmentProcessor::emitCode(EmitArgs& args) { | 36 void GrCircleBlurFragmentProcessor::GLSLProcessor::emitCode(EmitArgs& args) { |
| 37 | 37 |
| 38 const char *dataName; | 38 const char *dataName; |
| 39 | 39 |
| 40 // The data is formatted as: | 40 // The data is formatted as: |
| 41 // x,y - the center of the circle | 41 // x,y - the center of the circle |
| 42 // z - the distance at which the intensity starts falling off (e.g., the start of the table) | 42 // z - the distance at which the intensity starts falling off (e.g., the start of the table) |
| 43 // w - the inverse of the profile texture size | 43 // w - the inverse of the distance over which the texture is stretched. |
| 44 fDataUniform = args.fUniformHandler->addUniform(kFragment_GrShaderFlag, | 44 fDataUniform = args.fUniformHandler->addUniform(kFragment_GrShaderFlag, |
| 45 kVec4f_GrSLType, | 45 kVec4f_GrSLType, |
| 46 kDefault_GrSLPrecision, | 46 kDefault_GrSLPrecision, |
| 47 "data", | 47 "data", |
| 48 &dataName); | 48 &dataName); |
| 49 | 49 |
| 50 GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder; | 50 GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder; |
| 51 const char *fragmentPos = fragBuilder->fragmentPosition(); | 51 const char *fragmentPos = fragBuilder->fragmentPosition(); |
| 52 | 52 |
| 53 if (args.fInputColor) { | 53 if (args.fInputColor) { |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 64 fragBuilder->codeAppendf("float dist = length(vec) + ( 0.5 - %s.z ) * %s.w;" , | 64 fragBuilder->codeAppendf("float dist = length(vec) + ( 0.5 - %s.z ) * %s.w;" , |
| 65 dataName, dataName); | 65 dataName, dataName); |
| 66 | 66 |
| 67 fragBuilder->codeAppendf("float intensity = "); | 67 fragBuilder->codeAppendf("float intensity = "); |
| 68 fragBuilder->appendTextureLookup(args.fTexSamplers[0], "vec2(dist, 0.5)"); | 68 fragBuilder->appendTextureLookup(args.fTexSamplers[0], "vec2(dist, 0.5)"); |
| 69 fragBuilder->codeAppend(".a;"); | 69 fragBuilder->codeAppend(".a;"); |
| 70 | 70 |
| 71 fragBuilder->codeAppendf("%s = src * intensity;\n", args.fOutputColor ); | 71 fragBuilder->codeAppendf("%s = src * intensity;\n", args.fOutputColor ); |
| 72 } | 72 } |
| 73 | 73 |
| 74 void GrGLCircleBlurFragmentProcessor::onSetData(const GrGLSLProgramDataManager& pdman, | 74 void GrCircleBlurFragmentProcessor::GLSLProcessor::onSetData(const GrGLSLProgram DataManager& pdman, |
| 75 const GrProcessor& proc) { | 75 const GrProcessor& proc) { |
| 76 const GrCircleBlurFragmentProcessor& cbfp = proc.cast<GrCircleBlurFragmentPr ocessor>(); | 76 const GrCircleBlurFragmentProcessor& cbfp = proc.cast<GrCircleBlurFragmentPr ocessor>(); |
| 77 const SkRect& circle = cbfp.circle(); | 77 const SkRect& circle = cbfp.fCircle; |
| 78 | 78 |
| 79 // The data is formatted as: | 79 // The data is formatted as: |
| 80 // x,y - the center of the circle | 80 // x,y - the center of the circle |
| 81 // z - the distance at which the intensity starts falling off (e.g., the start of the table) | 81 // z - the distance at which the intensity starts falling off (e.g., the start of the table) |
| 82 // w - the inverse of the profile texture size | 82 // w - the inverse of the distance over which the profile texture is stre tched. |
| 83 pdman.set4f(fDataUniform, circle.centerX(), circle.centerY(), cbfp.offset(), | 83 pdman.set4f(fDataUniform, circle.centerX(), circle.centerY(), cbfp.fSolidRad ius, |
| 84 1.0f / cbfp.profileSize()); | 84 1.f / cbfp.fTextureRadius); |
| 85 } | 85 } |
| 86 | 86 |
| 87 /////////////////////////////////////////////////////////////////////////////// | 87 /////////////////////////////////////////////////////////////////////////////// |
| 88 | 88 |
| 89 GrCircleBlurFragmentProcessor::GrCircleBlurFragmentProcessor(const SkRect& circl e, | 89 GrCircleBlurFragmentProcessor::GrCircleBlurFragmentProcessor(const SkRect& circl e, |
| 90 float sigma, | 90 float sigma, |
| 91 float offset, | 91 float solidRadius, |
| 92 float textureRadius , | |
| 92 GrTexture* blurProf ile) | 93 GrTexture* blurProf ile) |
| 93 : fCircle(circle) | 94 : fCircle(circle) |
| 94 , fSigma(sigma) | 95 , fSigma(sigma) |
| 95 , fOffset(offset) | 96 , fSolidRadius(solidRadius) |
| 97 , fTextureRadius(textureRadius) | |
| 96 , fBlurProfileAccess(blurProfile, GrTextureParams::kBilerp_FilterMode) { | 98 , fBlurProfileAccess(blurProfile, GrTextureParams::kBilerp_FilterMode) { |
| 97 this->initClassID<GrCircleBlurFragmentProcessor>(); | 99 this->initClassID<GrCircleBlurFragmentProcessor>(); |
| 98 this->addTextureAccess(&fBlurProfileAccess); | 100 this->addTextureAccess(&fBlurProfileAccess); |
| 99 this->setWillReadFragmentPosition(); | 101 this->setWillReadFragmentPosition(); |
| 100 } | 102 } |
| 101 | 103 |
| 102 GrGLSLFragmentProcessor* GrCircleBlurFragmentProcessor::onCreateGLSLInstance() c onst { | 104 GrGLSLFragmentProcessor* GrCircleBlurFragmentProcessor::onCreateGLSLInstance() c onst { |
| 103 return new GrGLCircleBlurFragmentProcessor; | 105 return new GLSLProcessor; |
| 104 } | 106 } |
| 105 | 107 |
| 106 void GrCircleBlurFragmentProcessor::onGetGLSLProcessorKey(const GrGLSLCaps& caps , | 108 void GrCircleBlurFragmentProcessor::onGetGLSLProcessorKey(const GrGLSLCaps& caps , |
| 107 GrProcessorKeyBuilder* b) const { | 109 GrProcessorKeyBuilder* b) const { |
| 108 GrGLCircleBlurFragmentProcessor::GenKey(*this, caps, b); | 110 return; |
|
jvanverth1
2016/06/14 18:51:15
Won't this return garbage (whatever was there befo
bsalomon
2016/06/14 19:16:51
This just leaves the key empty (since this process
| |
| 109 } | 111 } |
| 110 | 112 |
| 111 void GrCircleBlurFragmentProcessor::onComputeInvariantOutput(GrInvariantOutput* inout) const { | 113 void GrCircleBlurFragmentProcessor::onComputeInvariantOutput(GrInvariantOutput* inout) const { |
| 112 inout->mulByUnknownSingleComponent(); | 114 inout->mulByUnknownSingleComponent(); |
| 113 } | 115 } |
| 114 | 116 |
| 115 // Create a Gaussian half-kernel and a summed area table given a sigma and numbe r of discrete | 117 // Create a Gaussian half-kernel and a summed area table given a sigma and numbe r of discrete |
| 116 // steps. The half kernel is normalized to sum to 0.5. | 118 // steps. The half kernel is normalized to sum to 0.5. |
| 117 static void make_half_kernel_and_summed_table(float* halfKernel, float* summedHa lfKernel, | 119 static void make_half_kernel_and_summed_table(float* halfKernel, float* summedHa lfKernel, |
| 118 int halfKernelSize, float sigma) { | 120 int halfKernelSize, float sigma) { |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 186 continue; | 188 continue; |
| 187 } | 189 } |
| 188 float verticalEval = yKernelEvaluations[i + halfKernelSize]; | 190 float verticalEval = yKernelEvaluations[i + halfKernelSize]; |
| 189 acc += verticalEval * halfKernel[i]; | 191 acc += verticalEval * halfKernel[i]; |
| 190 } | 192 } |
| 191 // Since we applied a half kernel in y we multiply acc by 2 (the circle is s ymmetric about the | 193 // Since we applied a half kernel in y we multiply acc by 2 (the circle is s ymmetric about the |
| 192 // x axis). | 194 // x axis). |
| 193 return SkUnitScalarClampToByte(2.f * acc); | 195 return SkUnitScalarClampToByte(2.f * acc); |
| 194 } | 196 } |
| 195 | 197 |
| 196 static inline void compute_profile_offset_and_size(float circleR, float sigma, | |
| 197 float* offset, int* size) { | |
| 198 if (3*sigma <= circleR) { | |
| 199 // The circle is bigger than the Gaussian. In this case we know the inte rior of the | |
| 200 // blurred circle is solid. | |
| 201 *offset = circleR - 3 * sigma; // This location maps to 0.5f in the weig hts texture. | |
| 202 // It should always be 255. | |
| 203 *size = SkScalarCeilToInt(6*sigma); | |
| 204 } else { | |
| 205 // The Gaussian is bigger than the circle. | |
| 206 *offset = 0.0f; | |
| 207 *size = SkScalarCeilToInt(circleR + 3*sigma); | |
| 208 } | |
| 209 } | |
| 210 | |
| 211 // This function creates a profile of a blurred circle. It does this by computin g a kernel for | 198 // This function creates a profile of a blurred circle. It does this by computin g a kernel for |
| 212 // half the Gaussian and a matching summed area table. The summed area table is used to compute | 199 // half the Gaussian and a matching summed area table. The summed area table is used to compute |
| 213 // an array of vertical applications of the half kernel to the circle along the x axis. The table | 200 // an array of vertical applications of the half kernel to the circle along the x axis. The table |
| 214 // of y evaluations has 2 * k + n entries where k is the size of the half kernel and n is the size | 201 // of y evaluations has 2 * k + n entries where k is the size of the half kernel and n is the size |
| 215 // of the profile being computed. Then for each of the n profile entries we walk out k steps in each | 202 // of the profile being computed. Then for each of the n profile entries we walk out k steps in each |
| 216 // horizontal direction multiplying the corresponding y evaluation by the half k ernel entry and | 203 // horizontal direction multiplying the corresponding y evaluation by the half k ernel entry and |
| 217 // sum these values to compute the profile entry. | 204 // sum these values to compute the profile entry. |
| 218 static uint8_t* create_profile(float circleR, float sigma) { | 205 static uint8_t* create_profile(float sigma, float circleR, float offset, int pro fileTextureWidth) { |
| 219 float offset; | 206 int numSteps = profileTextureWidth; |
| 220 int numSteps; | 207 // compute_profile_offset_and_size(circleR, sigma, &offset, &numSteps); |
|
jvanverth1
2016/06/14 18:51:15
Delete this line?
bsalomon
2016/06/14 19:16:51
oops, done.
| |
| 221 compute_profile_offset_and_size(circleR, sigma, &offset, &numSteps); | |
| 222 | 208 |
| 223 uint8_t* weights = new uint8_t[numSteps]; | 209 uint8_t* weights = new uint8_t[numSteps]; |
| 224 | 210 |
| 225 // The full kernel is 6 sigmas wide. | 211 // The full kernel is 6 sigmas wide. |
| 226 int halfKernelSize = SkScalarCeilToInt(6.0f*sigma); | 212 int halfKernelSize = SkScalarCeilToInt(6.0f*sigma); |
| 227 // round up to next multiple of 2 and then divide by 2 | 213 // round up to next multiple of 2 and then divide by 2 |
| 228 halfKernelSize = ((halfKernelSize + 1) & ~1) >> 1; | 214 halfKernelSize = ((halfKernelSize + 1) & ~1) >> 1; |
| 229 | 215 |
| 230 // Number of x steps at which to apply kernel in y to cover all the profile samples in x. | 216 // Number of x steps at which to apply kernel in y to cover all the profile samples in x. |
| 231 int numYSteps = numSteps + 2 * halfKernelSize; | 217 int numYSteps = numSteps + 2 * halfKernelSize; |
| 232 | 218 |
| 233 SkAutoTArray<float> bulkAlloc(halfKernelSize + halfKernelSize + numYSteps); | 219 SkAutoTArray<float> bulkAlloc(halfKernelSize + halfKernelSize + numYSteps); |
| 234 float* halfKernel = bulkAlloc.get(); | 220 float* halfKernel = bulkAlloc.get(); |
| 235 float* summedKernel = bulkAlloc.get() + halfKernelSize; | 221 float* summedKernel = bulkAlloc.get() + halfKernelSize; |
| 236 float* yEvals = bulkAlloc.get() + 2 * halfKernelSize; | 222 float* yEvals = bulkAlloc.get() + 2 * halfKernelSize; |
| 237 make_half_kernel_and_summed_table(halfKernel, summedKernel, halfKernelSize, sigma); | 223 make_half_kernel_and_summed_table(halfKernel, summedKernel, halfKernelSize, sigma); |
| 238 | 224 |
| 239 float firstX = offset - halfKernelSize + 0.5f; | 225 float firstX = offset - halfKernelSize + 0.5f; |
| 240 apply_kernel_in_y(yEvals, numYSteps, firstX, circleR, halfKernelSize, summed Kernel); | 226 apply_kernel_in_y(yEvals, numYSteps, firstX, circleR, halfKernelSize, summed Kernel); |
| 241 | 227 |
| 242 for (int i = 0; i < numSteps - 1; ++i) { | 228 for (int i = 0; i < numSteps - 1; ++i) { |
| 243 float evalX = offset + i + 0.5f; | 229 float evalX = offset + i + 0.5f; |
| 244 weights[i] = eval_at(evalX, circleR, halfKernel, halfKernelSize, yEvals + i); | 230 weights[i] = eval_at(evalX, circleR, halfKernel, halfKernelSize, yEvals + i); |
| 245 } | 231 } |
| 246 // Ensure the tail of the Gaussian goes to zero. | 232 // Ensure the tail of the Gaussian goes to zero. |
| 247 weights[numSteps - 1] = 0; | 233 weights[numSteps - 1] = 0; |
| 248 return weights; | 234 return weights; |
| 249 } | 235 } |
| 250 | 236 |
| 237 static int next_pow2_16bits(int x) { | |
| 238 SkASSERT(x > 0); | |
| 239 SkASSERT(x <= SK_MaxS16); | |
| 240 x--; | |
| 241 x |= x >> 1; | |
| 242 x |= x >> 2; | |
| 243 x |= x >> 4; | |
| 244 x |= x >> 8; | |
| 245 return x + 1; | |
| 246 } | |
| 247 | |
| 251 GrTexture* GrCircleBlurFragmentProcessor::CreateCircleBlurProfileTexture( | 248 GrTexture* GrCircleBlurFragmentProcessor::CreateCircleBlurProfileTexture( |
| 252 GrTextureProvide r* textureProvider, | 249 GrTextureProvide r* textureProvider, |
| 253 const SkRect& ci rcle, | 250 const SkRect& ci rcle, |
| 254 float sigma, | 251 float sigma, |
| 255 float* offset) { | 252 float* solidRadi us, |
| 253 float* textureRa dius) { | |
| 256 float circleR = circle.width() / 2.0f; | 254 float circleR = circle.width() / 2.0f; |
| 255 // Profile textures are cached by the ratio of sigma to circle radius and by the size of the | |
| 256 // profile texture (binned by powers of 2). | |
| 257 SkScalar sigmaToCircleRRatio = sigma / circleR; | |
| 258 // When sigma is really small this becomes a equivalent to convolving a Gaus sian with a half- | |
| 259 // plane. We could do that simpler computation. However, right now we're jus t using a lower | |
| 260 // bound off the ratio. Similarly, in the extreme high ratio cases circle be comes a point WRT to | |
| 261 // the Guassian and the profile texture is a just a Gaussian evaluation. | |
| 262 sigmaToCircleRRatio = SkTPin(sigmaToCircleRRatio, 0.05f, 8.f); | |
| 263 // Convert to fixed point for the key. | |
| 264 SkFixed sigmaToCircleRRatioFixed = SkScalarToFixed(sigmaToCircleRRatio); | |
| 265 // We shave off some bits to reduce the number of unique entries. We could p robably shave off | |
| 266 // more than we do. | |
| 267 sigmaToCircleRRatioFixed &= ~0xff; | |
| 268 // From the circle center to solidRadius is all 1s and represented by the le ftmost pixel (with | |
| 269 // value 255) in the profile texture. If it is zero then there is no solid c enter to the | |
| 270 // blurred circle. | |
| 271 if (3*sigma <= circleR) { | |
| 272 // The circle is bigger than the Gaussian. In this case we know the inte rior of the | |
| 273 // blurred circle is solid. | |
| 274 *solidRadius = circleR - 3 * sigma; // This location maps to 0.5f in the weights texture. | |
| 275 // It should always be 255. | |
| 276 *textureRadius = SkScalarCeilToInt(6*sigma); | |
| 277 } else { | |
| 278 // The Gaussian is bigger than the circle. | |
| 279 *solidRadius = 0.0f; | |
| 280 *textureRadius = SkScalarCeilToInt(circleR + 3*sigma); | |
| 281 } | |
| 282 int profileTextureWidth = SkScalarCeilToInt(*textureRadius); | |
| 283 profileTextureWidth = (profileTextureWidth >= 1024) ? 1024 : | |
| 284 next_pow2_16bits(profileTextureWidth); | |
| 257 | 285 |
| 258 static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain(); | 286 static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain(); |
| 259 GrUniqueKey key; | 287 GrUniqueKey key; |
| 260 GrUniqueKey::Builder builder(&key, kDomain, 2); | 288 GrUniqueKey::Builder builder(&key, kDomain, 2); |
| 261 // The profile curve varies with both the sigma of the Gaussian and the size of the | 289 builder[0] = sigmaToCircleRRatioFixed; |
| 262 // disk. Quantizing to 16.16 should be close enough though. | 290 builder[1] = profileTextureWidth; |
| 263 builder[0] = SkScalarToFixed(sigma); | |
| 264 builder[1] = SkScalarToFixed(circleR); | |
| 265 builder.finish(); | 291 builder.finish(); |
| 266 | 292 |
| 267 GrTexture *blurProfile = textureProvider->findAndRefTextureByUniqueKey(key); | 293 GrTexture *blurProfile = textureProvider->findAndRefTextureByUniqueKey(key); |
| 268 | 294 |
| 269 int profileSize; | |
| 270 compute_profile_offset_and_size(circleR, sigma, offset, &profileSize); | |
| 271 | |
| 272 if (!blurProfile) { | 295 if (!blurProfile) { |
| 273 | |
| 274 GrSurfaceDesc texDesc; | 296 GrSurfaceDesc texDesc; |
| 275 texDesc.fWidth = profileSize; | 297 texDesc.fWidth = profileTextureWidth; |
| 276 texDesc.fHeight = 1; | 298 texDesc.fHeight = 1; |
| 277 texDesc.fConfig = kAlpha_8_GrPixelConfig; | 299 texDesc.fConfig = kAlpha_8_GrPixelConfig; |
| 278 | 300 |
| 279 SkAutoTDeleteArray<uint8_t> profile(create_profile(circleR, sigma)); | 301 // Rescale params to the size of the texture we're creating. |
| 302 SkScalar scale = profileTextureWidth / *textureRadius; | |
| 303 SkAutoTDeleteArray<uint8_t> profile(create_profile(sigma * scale, circle R * scale, | |
| 304 *solidRadius * scale, | |
| 305 profileTextureWidth)) ; | |
| 280 | 306 |
| 281 blurProfile = textureProvider->createTexture(texDesc, SkBudgeted::kYes, profile.get(), 0); | 307 blurProfile = textureProvider->createTexture(texDesc, SkBudgeted::kYes, profile.get(), 0); |
| 282 if (blurProfile) { | 308 if (blurProfile) { |
| 283 textureProvider->assignUniqueKeyToTexture(key, blurProfile); | 309 textureProvider->assignUniqueKeyToTexture(key, blurProfile); |
| 284 } | 310 } |
| 285 } | 311 } |
| 286 | 312 |
| 287 return blurProfile; | 313 return blurProfile; |
| 288 } | 314 } |
| 289 | 315 |
| 290 GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrCircleBlurFragmentProcessor); | 316 GR_DEFINE_FRAGMENT_PROCESSOR_TEST(GrCircleBlurFragmentProcessor); |
| 291 | 317 |
| 292 sk_sp<GrFragmentProcessor> GrCircleBlurFragmentProcessor::TestCreate(GrProcessor TestData* d) { | 318 sk_sp<GrFragmentProcessor> GrCircleBlurFragmentProcessor::TestCreate(GrProcessor TestData* d) { |
| 293 SkScalar wh = d->fRandom->nextRangeScalar(100.f, 1000.f); | 319 SkScalar wh = d->fRandom->nextRangeScalar(100.f, 1000.f); |
| 294 SkScalar sigma = d->fRandom->nextRangeF(1.f,10.f); | 320 SkScalar sigma = d->fRandom->nextRangeF(1.f,10.f); |
| 295 SkRect circle = SkRect::MakeWH(wh, wh); | 321 SkRect circle = SkRect::MakeWH(wh, wh); |
| 296 return GrCircleBlurFragmentProcessor::Make(d->fContext->textureProvider(), c ircle, sigma); | 322 return GrCircleBlurFragmentProcessor::Make(d->fContext->textureProvider(), c ircle, sigma); |
| 297 } | 323 } |
| 298 | 324 |
| 299 #endif | 325 #endif |
| OLD | NEW |