gfx/skia/trunk/src/core/SkConvolver.cpp

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
michael@0 2 // Use of this source code is governed by a BSD-style license that can be
michael@0 3 // found in the LICENSE file.
michael@0 4
michael@0 5 #include "SkConvolver.h"
michael@0 6 #include "SkSize.h"
michael@0 7 #include "SkTypes.h"
michael@0 8
michael@0 9 namespace {
michael@0 10
michael@0 11 // Converts the argument to an 8-bit unsigned value by clamping to the range
michael@0 12 // 0-255.
michael@0 13 inline unsigned char ClampTo8(int a) {
michael@0 14 if (static_cast<unsigned>(a) < 256) {
michael@0 15 return a; // Avoid the extra check in the common case.
michael@0 16 }
michael@0 17 if (a < 0) {
michael@0 18 return 0;
michael@0 19 }
michael@0 20 return 255;
michael@0 21 }
michael@0 22
michael@0 23 // Stores a list of rows in a circular buffer. The usage is you write into it
michael@0 24 // by calling AdvanceRow. It will keep track of which row in the buffer it
michael@0 25 // should use next, and the total number of rows added.
michael@0 26 class CircularRowBuffer {
michael@0 27 public:
michael@0 28 // The number of pixels in each row is given in |sourceRowPixelWidth|.
michael@0 29 // The maximum number of rows needed in the buffer is |maxYFilterSize|
michael@0 30 // (we only need to store enough rows for the biggest filter).
michael@0 31 //
michael@0 32 // We use the |firstInputRow| to compute the coordinates of all of the
michael@0 33 // following rows returned by Advance().
michael@0 34 CircularRowBuffer(int destRowPixelWidth, int maxYFilterSize,
michael@0 35 int firstInputRow)
michael@0 36 : fRowByteWidth(destRowPixelWidth * 4),
michael@0 37 fNumRows(maxYFilterSize),
michael@0 38 fNextRow(0),
michael@0 39 fNextRowCoordinate(firstInputRow) {
michael@0 40 fBuffer.reset(fRowByteWidth * maxYFilterSize);
michael@0 41 fRowAddresses.reset(fNumRows);
michael@0 42 }
michael@0 43
michael@0 44 // Moves to the next row in the buffer, returning a pointer to the beginning
michael@0 45 // of it.
michael@0 46 unsigned char* advanceRow() {
michael@0 47 unsigned char* row = &fBuffer[fNextRow * fRowByteWidth];
michael@0 48 fNextRowCoordinate++;
michael@0 49
michael@0 50 // Set the pointer to the next row to use, wrapping around if necessary.
michael@0 51 fNextRow++;
michael@0 52 if (fNextRow == fNumRows) {
michael@0 53 fNextRow = 0;
michael@0 54 }
michael@0 55 return row;
michael@0 56 }
michael@0 57
michael@0 58 // Returns a pointer to an "unrolled" array of rows. These rows will start
michael@0 59 // at the y coordinate placed into |*firstRowIndex| and will continue in
michael@0 60 // order for the maximum number of rows in this circular buffer.
michael@0 61 //
michael@0 62 // The |firstRowIndex_| may be negative. This means the circular buffer
michael@0 63 // starts before the top of the image (it hasn't been filled yet).
michael@0 64 unsigned char* const* GetRowAddresses(int* firstRowIndex) {
michael@0 65 // Example for a 4-element circular buffer holding coords 6-9.
michael@0 66 // Row 0 Coord 8
michael@0 67 // Row 1 Coord 9
michael@0 68 // Row 2 Coord 6 <- fNextRow = 2, fNextRowCoordinate = 10.
michael@0 69 // Row 3 Coord 7
michael@0 70 //
michael@0 71 // The "next" row is also the first (lowest) coordinate. This computation
michael@0 72 // may yield a negative value, but that's OK, the math will work out
michael@0 73 // since the user of this buffer will compute the offset relative
michael@0 74 // to the firstRowIndex and the negative rows will never be used.
michael@0 75 *firstRowIndex = fNextRowCoordinate - fNumRows;
michael@0 76
michael@0 77 int curRow = fNextRow;
michael@0 78 for (int i = 0; i < fNumRows; i++) {
michael@0 79 fRowAddresses[i] = &fBuffer[curRow * fRowByteWidth];
michael@0 80
michael@0 81 // Advance to the next row, wrapping if necessary.
michael@0 82 curRow++;
michael@0 83 if (curRow == fNumRows) {
michael@0 84 curRow = 0;
michael@0 85 }
michael@0 86 }
michael@0 87 return &fRowAddresses[0];
michael@0 88 }
michael@0 89
michael@0 90 private:
michael@0 91 // The buffer storing the rows. They are packed, each one fRowByteWidth.
michael@0 92 SkTArray<unsigned char> fBuffer;
michael@0 93
michael@0 94 // Number of bytes per row in the |buffer|.
michael@0 95 int fRowByteWidth;
michael@0 96
michael@0 97 // The number of rows available in the buffer.
michael@0 98 int fNumRows;
michael@0 99
michael@0 100 // The next row index we should write into. This wraps around as the
michael@0 101 // circular buffer is used.
michael@0 102 int fNextRow;
michael@0 103
michael@0 104 // The y coordinate of the |fNextRow|. This is incremented each time a
michael@0 105 // new row is appended and does not wrap.
michael@0 106 int fNextRowCoordinate;
michael@0 107
michael@0 108 // Buffer used by GetRowAddresses().
michael@0 109 SkTArray<unsigned char*> fRowAddresses;
michael@0 110 };
michael@0 111
michael@0 112 // Convolves horizontally along a single row. The row data is given in
michael@0 113 // |srcData| and continues for the numValues() of the filter.
michael@0 114 template<bool hasAlpha>
michael@0 115 void ConvolveHorizontally(const unsigned char* srcData,
michael@0 116 const SkConvolutionFilter1D& filter,
michael@0 117 unsigned char* outRow) {
michael@0 118 // Loop over each pixel on this row in the output image.
michael@0 119 int numValues = filter.numValues();
michael@0 120 for (int outX = 0; outX < numValues; outX++) {
michael@0 121 // Get the filter that determines the current output pixel.
michael@0 122 int filterOffset, filterLength;
michael@0 123 const SkConvolutionFilter1D::ConvolutionFixed* filterValues =
michael@0 124 filter.FilterForValue(outX, &filterOffset, &filterLength);
michael@0 125
michael@0 126 // Compute the first pixel in this row that the filter affects. It will
michael@0 127 // touch |filterLength| pixels (4 bytes each) after this.
michael@0 128 const unsigned char* rowToFilter = &srcData[filterOffset * 4];
michael@0 129
michael@0 130 // Apply the filter to the row to get the destination pixel in |accum|.
michael@0 131 int accum[4] = {0};
michael@0 132 for (int filterX = 0; filterX < filterLength; filterX++) {
michael@0 133 SkConvolutionFilter1D::ConvolutionFixed curFilter = filterValues[filterX];
michael@0 134 accum[0] += curFilter * rowToFilter[filterX * 4 + 0];
michael@0 135 accum[1] += curFilter * rowToFilter[filterX * 4 + 1];
michael@0 136 accum[2] += curFilter * rowToFilter[filterX * 4 + 2];
michael@0 137 if (hasAlpha) {
michael@0 138 accum[3] += curFilter * rowToFilter[filterX * 4 + 3];
michael@0 139 }
michael@0 140 }
michael@0 141
michael@0 142 // Bring this value back in range. All of the filter scaling factors
michael@0 143 // are in fixed point with kShiftBits bits of fractional part.
michael@0 144 accum[0] >>= SkConvolutionFilter1D::kShiftBits;
michael@0 145 accum[1] >>= SkConvolutionFilter1D::kShiftBits;
michael@0 146 accum[2] >>= SkConvolutionFilter1D::kShiftBits;
michael@0 147 if (hasAlpha) {
michael@0 148 accum[3] >>= SkConvolutionFilter1D::kShiftBits;
michael@0 149 }
michael@0 150
michael@0 151 // Store the new pixel.
michael@0 152 outRow[outX * 4 + 0] = ClampTo8(accum[0]);
michael@0 153 outRow[outX * 4 + 1] = ClampTo8(accum[1]);
michael@0 154 outRow[outX * 4 + 2] = ClampTo8(accum[2]);
michael@0 155 if (hasAlpha) {
michael@0 156 outRow[outX * 4 + 3] = ClampTo8(accum[3]);
michael@0 157 }
michael@0 158 }
michael@0 159 }
michael@0 160
michael@0 161 // Does vertical convolution to produce one output row. The filter values and
michael@0 162 // length are given in the first two parameters. These are applied to each
michael@0 163 // of the rows pointed to in the |sourceDataRows| array, with each row
michael@0 164 // being |pixelWidth| wide.
michael@0 165 //
michael@0 166 // The output must have room for |pixelWidth * 4| bytes.
michael@0 167 template<bool hasAlpha>
michael@0 168 void ConvolveVertically(const SkConvolutionFilter1D::ConvolutionFixed* filterValues,
michael@0 169 int filterLength,
michael@0 170 unsigned char* const* sourceDataRows,
michael@0 171 int pixelWidth,
michael@0 172 unsigned char* outRow) {
michael@0 173 // We go through each column in the output and do a vertical convolution,
michael@0 174 // generating one output pixel each time.
michael@0 175 for (int outX = 0; outX < pixelWidth; outX++) {
michael@0 176 // Compute the number of bytes over in each row that the current column
michael@0 177 // we're convolving starts at. The pixel will cover the next 4 bytes.
michael@0 178 int byteOffset = outX * 4;
michael@0 179
michael@0 180 // Apply the filter to one column of pixels.
michael@0 181 int accum[4] = {0};
michael@0 182 for (int filterY = 0; filterY < filterLength; filterY++) {
michael@0 183 SkConvolutionFilter1D::ConvolutionFixed curFilter = filterValues[filterY];
michael@0 184 accum[0] += curFilter * sourceDataRows[filterY][byteOffset + 0];
michael@0 185 accum[1] += curFilter * sourceDataRows[filterY][byteOffset + 1];
michael@0 186 accum[2] += curFilter * sourceDataRows[filterY][byteOffset + 2];
michael@0 187 if (hasAlpha) {
michael@0 188 accum[3] += curFilter * sourceDataRows[filterY][byteOffset + 3];
michael@0 189 }
michael@0 190 }
michael@0 191
michael@0 192 // Bring this value back in range. All of the filter scaling factors
michael@0 193 // are in fixed point with kShiftBits bits of precision.
michael@0 194 accum[0] >>= SkConvolutionFilter1D::kShiftBits;
michael@0 195 accum[1] >>= SkConvolutionFilter1D::kShiftBits;
michael@0 196 accum[2] >>= SkConvolutionFilter1D::kShiftBits;
michael@0 197 if (hasAlpha) {
michael@0 198 accum[3] >>= SkConvolutionFilter1D::kShiftBits;
michael@0 199 }
michael@0 200
michael@0 201 // Store the new pixel.
michael@0 202 outRow[byteOffset + 0] = ClampTo8(accum[0]);
michael@0 203 outRow[byteOffset + 1] = ClampTo8(accum[1]);
michael@0 204 outRow[byteOffset + 2] = ClampTo8(accum[2]);
michael@0 205 if (hasAlpha) {
michael@0 206 unsigned char alpha = ClampTo8(accum[3]);
michael@0 207
michael@0 208 // Make sure the alpha channel doesn't come out smaller than any of the
michael@0 209 // color channels. We use premultipled alpha channels, so this should
michael@0 210 // never happen, but rounding errors will cause this from time to time.
michael@0 211 // These "impossible" colors will cause overflows (and hence random pixel
michael@0 212 // values) when the resulting bitmap is drawn to the screen.
michael@0 213 //
michael@0 214 // We only need to do this when generating the final output row (here).
michael@0 215 int maxColorChannel = SkTMax(outRow[byteOffset + 0],
michael@0 216 SkTMax(outRow[byteOffset + 1],
michael@0 217 outRow[byteOffset + 2]));
michael@0 218 if (alpha < maxColorChannel) {
michael@0 219 outRow[byteOffset + 3] = maxColorChannel;
michael@0 220 } else {
michael@0 221 outRow[byteOffset + 3] = alpha;
michael@0 222 }
michael@0 223 } else {
michael@0 224 // No alpha channel, the image is opaque.
michael@0 225 outRow[byteOffset + 3] = 0xff;
michael@0 226 }
michael@0 227 }
michael@0 228 }
michael@0 229
michael@0 230 void ConvolveVertically(const SkConvolutionFilter1D::ConvolutionFixed* filterValues,
michael@0 231 int filterLength,
michael@0 232 unsigned char* const* sourceDataRows,
michael@0 233 int pixelWidth,
michael@0 234 unsigned char* outRow,
michael@0 235 bool sourceHasAlpha) {
michael@0 236 if (sourceHasAlpha) {
michael@0 237 ConvolveVertically<true>(filterValues, filterLength,
michael@0 238 sourceDataRows, pixelWidth,
michael@0 239 outRow);
michael@0 240 } else {
michael@0 241 ConvolveVertically<false>(filterValues, filterLength,
michael@0 242 sourceDataRows, pixelWidth,
michael@0 243 outRow);
michael@0 244 }
michael@0 245 }
michael@0 246
michael@0 247 } // namespace
michael@0 248
michael@0 249 // SkConvolutionFilter1D ---------------------------------------------------------
michael@0 250
michael@0 251 SkConvolutionFilter1D::SkConvolutionFilter1D()
michael@0 252 : fMaxFilter(0) {
michael@0 253 }
michael@0 254
michael@0 255 SkConvolutionFilter1D::~SkConvolutionFilter1D() {
michael@0 256 }
michael@0 257
michael@0 258 void SkConvolutionFilter1D::AddFilter(int filterOffset,
michael@0 259 const float* filterValues,
michael@0 260 int filterLength) {
michael@0 261 SkASSERT(filterLength > 0);
michael@0 262
michael@0 263 SkTArray<ConvolutionFixed> fixedValues;
michael@0 264 fixedValues.reset(filterLength);
michael@0 265
michael@0 266 for (int i = 0; i < filterLength; ++i) {
michael@0 267 fixedValues.push_back(FloatToFixed(filterValues[i]));
michael@0 268 }
michael@0 269
michael@0 270 AddFilter(filterOffset, &fixedValues[0], filterLength);
michael@0 271 }
michael@0 272
michael@0 273 void SkConvolutionFilter1D::AddFilter(int filterOffset,
michael@0 274 const ConvolutionFixed* filterValues,
michael@0 275 int filterLength) {
michael@0 276 // It is common for leading/trailing filter values to be zeros. In such
michael@0 277 // cases it is beneficial to only store the central factors.
michael@0 278 // For a scaling to 1/4th in each dimension using a Lanczos-2 filter on
michael@0 279 // a 1080p image this optimization gives a ~10% speed improvement.
michael@0 280 int filterSize = filterLength;
michael@0 281 int firstNonZero = 0;
michael@0 282 while (firstNonZero < filterLength && filterValues[firstNonZero] == 0) {
michael@0 283 firstNonZero++;
michael@0 284 }
michael@0 285
michael@0 286 if (firstNonZero < filterLength) {
michael@0 287 // Here we have at least one non-zero factor.
michael@0 288 int lastNonZero = filterLength - 1;
michael@0 289 while (lastNonZero >= 0 && filterValues[lastNonZero] == 0) {
michael@0 290 lastNonZero--;
michael@0 291 }
michael@0 292
michael@0 293 filterOffset += firstNonZero;
michael@0 294 filterLength = lastNonZero + 1 - firstNonZero;
michael@0 295 SkASSERT(filterLength > 0);
michael@0 296
michael@0 297 for (int i = firstNonZero; i <= lastNonZero; i++) {
michael@0 298 fFilterValues.push_back(filterValues[i]);
michael@0 299 }
michael@0 300 } else {
michael@0 301 // Here all the factors were zeroes.
michael@0 302 filterLength = 0;
michael@0 303 }
michael@0 304
michael@0 305 FilterInstance instance;
michael@0 306
michael@0 307 // We pushed filterLength elements onto fFilterValues
michael@0 308 instance.fDataLocation = (static_cast<int>(fFilterValues.count()) -
michael@0 309 filterLength);
michael@0 310 instance.fOffset = filterOffset;
michael@0 311 instance.fTrimmedLength = filterLength;
michael@0 312 instance.fLength = filterSize;
michael@0 313 fFilters.push_back(instance);
michael@0 314
michael@0 315 fMaxFilter = SkTMax(fMaxFilter, filterLength);
michael@0 316 }
michael@0 317
michael@0 318 const SkConvolutionFilter1D::ConvolutionFixed* SkConvolutionFilter1D::GetSingleFilter(
michael@0 319 int* specifiedFilterlength,
michael@0 320 int* filterOffset,
michael@0 321 int* filterLength) const {
michael@0 322 const FilterInstance& filter = fFilters[0];
michael@0 323 *filterOffset = filter.fOffset;
michael@0 324 *filterLength = filter.fTrimmedLength;
michael@0 325 *specifiedFilterlength = filter.fLength;
michael@0 326 if (filter.fTrimmedLength == 0) {
michael@0 327 return NULL;
michael@0 328 }
michael@0 329
michael@0 330 return &fFilterValues[filter.fDataLocation];
michael@0 331 }
michael@0 332
michael@0 333 void BGRAConvolve2D(const unsigned char* sourceData,
michael@0 334 int sourceByteRowStride,
michael@0 335 bool sourceHasAlpha,
michael@0 336 const SkConvolutionFilter1D& filterX,
michael@0 337 const SkConvolutionFilter1D& filterY,
michael@0 338 int outputByteRowStride,
michael@0 339 unsigned char* output,
michael@0 340 const SkConvolutionProcs& convolveProcs,
michael@0 341 bool useSimdIfPossible) {
michael@0 342
michael@0 343 int maxYFilterSize = filterY.maxFilter();
michael@0 344
michael@0 345 // The next row in the input that we will generate a horizontally
michael@0 346 // convolved row for. If the filter doesn't start at the beginning of the
michael@0 347 // image (this is the case when we are only resizing a subset), then we
michael@0 348 // don't want to generate any output rows before that. Compute the starting
michael@0 349 // row for convolution as the first pixel for the first vertical filter.
michael@0 350 int filterOffset, filterLength;
michael@0 351 const SkConvolutionFilter1D::ConvolutionFixed* filterValues =
michael@0 352 filterY.FilterForValue(0, &filterOffset, &filterLength);
michael@0 353 int nextXRow = filterOffset;
michael@0 354
michael@0 355 // We loop over each row in the input doing a horizontal convolution. This
michael@0 356 // will result in a horizontally convolved image. We write the results into
michael@0 357 // a circular buffer of convolved rows and do vertical convolution as rows
michael@0 358 // are available. This prevents us from having to store the entire
michael@0 359 // intermediate image and helps cache coherency.
michael@0 360 // We will need four extra rows to allow horizontal convolution could be done
michael@0 361 // simultaneously. We also pad each row in row buffer to be aligned-up to
michael@0 362 // 16 bytes.
michael@0 363 // TODO(jiesun): We do not use aligned load from row buffer in vertical
michael@0 364 // convolution pass yet. Somehow Windows does not like it.
michael@0 365 int rowBufferWidth = (filterX.numValues() + 15) & ~0xF;
michael@0 366 int rowBufferHeight = maxYFilterSize +
michael@0 367 (convolveProcs.fConvolve4RowsHorizontally ? 4 : 0);
michael@0 368 CircularRowBuffer rowBuffer(rowBufferWidth,
michael@0 369 rowBufferHeight,
michael@0 370 filterOffset);
michael@0 371
michael@0 372 // Loop over every possible output row, processing just enough horizontal
michael@0 373 // convolutions to run each subsequent vertical convolution.
michael@0 374 SkASSERT(outputByteRowStride >= filterX.numValues() * 4);
michael@0 375 int numOutputRows = filterY.numValues();
michael@0 376
michael@0 377 // We need to check which is the last line to convolve before we advance 4
michael@0 378 // lines in one iteration.
michael@0 379 int lastFilterOffset, lastFilterLength;
michael@0 380
michael@0 381 // SSE2 can access up to 3 extra pixels past the end of the
michael@0 382 // buffer. At the bottom of the image, we have to be careful
michael@0 383 // not to access data past the end of the buffer. Normally
michael@0 384 // we fall back to the C++ implementation for the last row.
michael@0 385 // If the last row is less than 3 pixels wide, we may have to fall
michael@0 386 // back to the C++ version for more rows. Compute how many
michael@0 387 // rows we need to avoid the SSE implementation for here.
michael@0 388 filterX.FilterForValue(filterX.numValues() - 1, &lastFilterOffset,
michael@0 389 &lastFilterLength);
michael@0 390 int avoidSimdRows = 1 + convolveProcs.fExtraHorizontalReads /
michael@0 391 (lastFilterOffset + lastFilterLength);
michael@0 392
michael@0 393 filterY.FilterForValue(numOutputRows - 1, &lastFilterOffset,
michael@0 394 &lastFilterLength);
michael@0 395
michael@0 396 for (int outY = 0; outY < numOutputRows; outY++) {
michael@0 397 filterValues = filterY.FilterForValue(outY,
michael@0 398 &filterOffset, &filterLength);
michael@0 399
michael@0 400 // Generate output rows until we have enough to run the current filter.
michael@0 401 while (nextXRow < filterOffset + filterLength) {
michael@0 402 if (convolveProcs.fConvolve4RowsHorizontally &&
michael@0 403 nextXRow + 3 < lastFilterOffset + lastFilterLength -
michael@0 404 avoidSimdRows) {
michael@0 405 const unsigned char* src[4];
michael@0 406 unsigned char* outRow[4];
michael@0 407 for (int i = 0; i < 4; ++i) {
michael@0 408 src[i] = &sourceData[(nextXRow + i) * sourceByteRowStride];
michael@0 409 outRow[i] = rowBuffer.advanceRow();
michael@0 410 }
michael@0 411 convolveProcs.fConvolve4RowsHorizontally(src, filterX, outRow);
michael@0 412 nextXRow += 4;
michael@0 413 } else {
michael@0 414 // Check if we need to avoid SSE2 for this row.
michael@0 415 if (convolveProcs.fConvolveHorizontally &&
michael@0 416 nextXRow < lastFilterOffset + lastFilterLength -
michael@0 417 avoidSimdRows) {
michael@0 418 convolveProcs.fConvolveHorizontally(
michael@0 419 &sourceData[nextXRow * sourceByteRowStride],
michael@0 420 filterX, rowBuffer.advanceRow(), sourceHasAlpha);
michael@0 421 } else {
michael@0 422 if (sourceHasAlpha) {
michael@0 423 ConvolveHorizontally<true>(
michael@0 424 &sourceData[nextXRow * sourceByteRowStride],
michael@0 425 filterX, rowBuffer.advanceRow());
michael@0 426 } else {
michael@0 427 ConvolveHorizontally<false>(
michael@0 428 &sourceData[nextXRow * sourceByteRowStride],
michael@0 429 filterX, rowBuffer.advanceRow());
michael@0 430 }
michael@0 431 }
michael@0 432 nextXRow++;
michael@0 433 }
michael@0 434 }
michael@0 435
michael@0 436 // Compute where in the output image this row of final data will go.
michael@0 437 unsigned char* curOutputRow = &output[outY * outputByteRowStride];
michael@0 438
michael@0 439 // Get the list of rows that the circular buffer has, in order.
michael@0 440 int firstRowInCircularBuffer;
michael@0 441 unsigned char* const* rowsToConvolve =
michael@0 442 rowBuffer.GetRowAddresses(&firstRowInCircularBuffer);
michael@0 443
michael@0 444 // Now compute the start of the subset of those rows that the filter
michael@0 445 // needs.
michael@0 446 unsigned char* const* firstRowForFilter =
michael@0 447 &rowsToConvolve[filterOffset - firstRowInCircularBuffer];
michael@0 448
michael@0 449 if (convolveProcs.fConvolveVertically) {
michael@0 450 convolveProcs.fConvolveVertically(filterValues, filterLength,
michael@0 451 firstRowForFilter,
michael@0 452 filterX.numValues(), curOutputRow,
michael@0 453 sourceHasAlpha);
michael@0 454 } else {
michael@0 455 ConvolveVertically(filterValues, filterLength,
michael@0 456 firstRowForFilter,
michael@0 457 filterX.numValues(), curOutputRow,
michael@0 458 sourceHasAlpha);
michael@0 459 }
michael@0 460 }
michael@0 461 }

mercurial