1 /* libs/graphics/sgl/SkScan_Path.cpp
2 **
3 ** Copyright 2006, The Android Open Source Project
4 **
5 ** Licensed under the Apache License, Version 2.0 (the "License");
6 ** you may not use this file except in compliance with the License.
7 ** You may obtain a copy of the License at
8 **
9 ** http://www.apache.org/licenses/LICENSE-2.0
10 **
11 ** Unless required by applicable law or agreed to in writing, software
12 ** distributed under the License is distributed on an "AS IS" BASIS,
13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 ** See the License for the specific language governing permissions and
15 ** limitations under the License.
16 */
17
18 #include "SkScanPriv.h"
19 #include "SkBlitter.h"
20 #include "SkEdge.h"
21 #include "SkGeometry.h"
22 #include "SkPath.h"
23 #include "SkQuadClipper.h"
24 #include "SkRegion.h"
25 #include "SkTemplates.h"
26
27 #define kEDGE_HEAD_Y SK_MinS32
28 #define kEDGE_TAIL_Y SK_MaxS32
29
30 #ifdef SK_DEBUG
validate_sort(const SkEdge * edge)31 static void validate_sort(const SkEdge* edge)
32 {
33 int y = kEDGE_HEAD_Y;
34
35 while (edge->fFirstY != SK_MaxS32)
36 {
37 edge->validate();
38 SkASSERT(y <= edge->fFirstY);
39
40 y = edge->fFirstY;
41 edge = edge->fNext;
42 }
43 }
44 #else
45 #define validate_sort(edge)
46 #endif
47
remove_edge(SkEdge * edge)48 static inline void remove_edge(SkEdge* edge)
49 {
50 edge->fPrev->fNext = edge->fNext;
51 edge->fNext->fPrev = edge->fPrev;
52 }
53
swap_edges(SkEdge * prev,SkEdge * next)54 static inline void swap_edges(SkEdge* prev, SkEdge* next)
55 {
56 SkASSERT(prev->fNext == next && next->fPrev == prev);
57
58 // remove prev from the list
59 prev->fPrev->fNext = next;
60 next->fPrev = prev->fPrev;
61
62 // insert prev after next
63 prev->fNext = next->fNext;
64 next->fNext->fPrev = prev;
65 next->fNext = prev;
66 prev->fPrev = next;
67 }
68
backward_insert_edge_based_on_x(SkEdge * edge SkDECLAREPARAM (int,curr_y))69 static void backward_insert_edge_based_on_x(SkEdge* edge SkDECLAREPARAM(int, curr_y))
70 {
71 SkFixed x = edge->fX;
72
73 for (;;)
74 {
75 SkEdge* prev = edge->fPrev;
76
77 // add 1 to curr_y since we may have added new edges (built from curves)
78 // that start on the next scanline
79 SkASSERT(prev && prev->fFirstY <= curr_y + 1);
80
81 if (prev->fX <= x)
82 break;
83
84 swap_edges(prev, edge);
85 }
86 }
87
insert_new_edges(SkEdge * newEdge,int curr_y)88 static void insert_new_edges(SkEdge* newEdge, int curr_y)
89 {
90 SkASSERT(newEdge->fFirstY >= curr_y);
91
92 while (newEdge->fFirstY == curr_y)
93 {
94 SkEdge* next = newEdge->fNext;
95 backward_insert_edge_based_on_x(newEdge SkPARAM(curr_y));
96 newEdge = next;
97 }
98 }
99
100 #ifdef SK_DEBUG
validate_edges_for_y(const SkEdge * edge,int curr_y)101 static void validate_edges_for_y(const SkEdge* edge, int curr_y)
102 {
103 while (edge->fFirstY <= curr_y)
104 {
105 SkASSERT(edge->fPrev && edge->fNext);
106 SkASSERT(edge->fPrev->fNext == edge);
107 SkASSERT(edge->fNext->fPrev == edge);
108 SkASSERT(edge->fFirstY <= edge->fLastY);
109
110 SkASSERT(edge->fPrev->fX <= edge->fX);
111 edge = edge->fNext;
112 }
113 }
114 #else
115 #define validate_edges_for_y(edge, curr_y)
116 #endif
117
118 #if defined _WIN32 && _MSC_VER >= 1300 // disable warning : local variable used without having been initialized
119 #pragma warning ( push )
120 #pragma warning ( disable : 4701 )
121 #endif
122
123 typedef void (*PrePostProc)(SkBlitter* blitter, int y, bool isStartOfScanline);
124 #define PREPOST_START true
125 #define PREPOST_END false
126
walk_edges(SkEdge * prevHead,SkPath::FillType fillType,SkBlitter * blitter,int stop_y,PrePostProc proc)127 static void walk_edges(SkEdge* prevHead, SkPath::FillType fillType,
128 SkBlitter* blitter, int stop_y, PrePostProc proc)
129 {
130 validate_sort(prevHead->fNext);
131
132 int curr_y = prevHead->fNext->fFirstY;
133 // returns 1 for evenodd, -1 for winding, regardless of inverse-ness
134 int windingMask = (fillType & 1) ? 1 : -1;
135
136 for (;;)
137 {
138 int w = 0;
139 int left SK_INIT_TO_AVOID_WARNING;
140 bool in_interval = false;
141 SkEdge* currE = prevHead->fNext;
142 SkFixed prevX = prevHead->fX;
143
144 validate_edges_for_y(currE, curr_y);
145
146 if (proc) {
147 proc(blitter, curr_y, PREPOST_START); // pre-proc
148 }
149
150 while (currE->fFirstY <= curr_y)
151 {
152 SkASSERT(currE->fLastY >= curr_y);
153
154 int x = (currE->fX + SK_Fixed1/2) >> 16;
155 w += currE->fWinding;
156 if ((w & windingMask) == 0) // we finished an interval
157 {
158 SkASSERT(in_interval);
159 int width = x - left;
160 SkASSERT(width >= 0);
161 if (width)
162 blitter->blitH(left, curr_y, width);
163 in_interval = false;
164 }
165 else if (!in_interval)
166 {
167 left = x;
168 in_interval = true;
169 }
170
171 SkEdge* next = currE->fNext;
172 SkFixed newX;
173
174 if (currE->fLastY == curr_y) // are we done with this edge?
175 {
176 if (currE->fCurveCount < 0)
177 {
178 if (((SkCubicEdge*)currE)->updateCubic())
179 {
180 SkASSERT(currE->fFirstY == curr_y + 1);
181
182 newX = currE->fX;
183 goto NEXT_X;
184 }
185 }
186 else if (currE->fCurveCount > 0)
187 {
188 if (((SkQuadraticEdge*)currE)->updateQuadratic())
189 {
190 newX = currE->fX;
191 goto NEXT_X;
192 }
193 }
194 remove_edge(currE);
195 }
196 else
197 {
198 SkASSERT(currE->fLastY > curr_y);
199 newX = currE->fX + currE->fDX;
200 currE->fX = newX;
201 NEXT_X:
202 if (newX < prevX) // ripple currE backwards until it is x-sorted
203 backward_insert_edge_based_on_x(currE SkPARAM(curr_y));
204 else
205 prevX = newX;
206 }
207 currE = next;
208 SkASSERT(currE);
209 }
210
211 if (proc) {
212 proc(blitter, curr_y, PREPOST_END); // post-proc
213 }
214
215 curr_y += 1;
216 if (curr_y >= stop_y)
217 break;
218
219 // now currE points to the first edge with a Yint larger than curr_y
220 insert_new_edges(currE, curr_y);
221 }
222 }
223
224 ///////////////////////////////////////////////////////////////////////////////
225
226 // this guy overrides blitH, and will call its proxy blitter with the inverse
227 // of the spans it is given (clipped to the left/right of the cliprect)
228 //
229 // used to implement inverse filltypes on paths
230 //
231 class InverseBlitter : public SkBlitter {
232 public:
setBlitter(SkBlitter * blitter,const SkIRect & clip,int shift)233 void setBlitter(SkBlitter* blitter, const SkIRect& clip, int shift) {
234 fBlitter = blitter;
235 fFirstX = clip.fLeft << shift;
236 fLastX = clip.fRight << shift;
237 }
prepost(int y,bool isStart)238 void prepost(int y, bool isStart) {
239 if (isStart) {
240 fPrevX = fFirstX;
241 } else {
242 int invWidth = fLastX - fPrevX;
243 if (invWidth > 0) {
244 fBlitter->blitH(fPrevX, y, invWidth);
245 }
246 }
247 }
248
249 // overrides
blitH(int x,int y,int width)250 virtual void blitH(int x, int y, int width) {
251 int invWidth = x - fPrevX;
252 if (invWidth > 0) {
253 fBlitter->blitH(fPrevX, y, invWidth);
254 }
255 fPrevX = x + width;
256 }
257
258 // we do not expect to get called with these entrypoints
blitAntiH(int,int,const SkAlpha[],const int16_t runs[])259 virtual void blitAntiH(int, int, const SkAlpha[], const int16_t runs[]) {
260 SkASSERT(!"blitAntiH unexpected");
261 }
blitV(int x,int y,int height,SkAlpha alpha)262 virtual void blitV(int x, int y, int height, SkAlpha alpha) {
263 SkASSERT(!"blitV unexpected");
264 }
blitRect(int x,int y,int width,int height)265 virtual void blitRect(int x, int y, int width, int height) {
266 SkASSERT(!"blitRect unexpected");
267 }
blitMask(const SkMask &,const SkIRect & clip)268 virtual void blitMask(const SkMask&, const SkIRect& clip) {
269 SkASSERT(!"blitMask unexpected");
270 }
justAnOpaqueColor(uint32_t * value)271 virtual const SkBitmap* justAnOpaqueColor(uint32_t* value) {
272 SkASSERT(!"justAnOpaqueColor unexpected");
273 return NULL;
274 }
275
276 private:
277 SkBlitter* fBlitter;
278 int fFirstX, fLastX, fPrevX;
279 };
280
PrePostInverseBlitterProc(SkBlitter * blitter,int y,bool isStart)281 static void PrePostInverseBlitterProc(SkBlitter* blitter, int y, bool isStart) {
282 ((InverseBlitter*)blitter)->prepost(y, isStart);
283 }
284
285 ///////////////////////////////////////////////////////////////////////////////
286
287 #if defined _WIN32 && _MSC_VER >= 1300
288 #pragma warning ( pop )
289 #endif
290
291 /* Our line edge relies on the maximum span being <= 512, so that it can
292 use FDot6 and keep the dx,dy in 16bits (for much faster slope divide).
293 This function returns true if the specified line is too big.
294 */
line_too_big(const SkPoint pts[2])295 static inline bool line_too_big(const SkPoint pts[2])
296 {
297 SkScalar dx = pts[1].fX - pts[0].fX;
298 SkScalar dy = pts[1].fY - pts[0].fY;
299
300 return SkScalarAbs(dx) > SkIntToScalar(511) ||
301 SkScalarAbs(dy) > SkIntToScalar(511);
302 }
303
build_edges(SkEdge edge[],const SkPath & path,const SkIRect * clipRect,SkEdge * list[],int shiftUp)304 static int build_edges(SkEdge edge[], const SkPath& path,
305 const SkIRect* clipRect, SkEdge* list[], int shiftUp) {
306 SkEdge** start = list;
307 SkPath::Iter iter(path, true);
308 SkPoint pts[4];
309 SkPath::Verb verb;
310
311 SkQuadClipper qclipper;
312 if (clipRect) {
313 SkIRect r;
314 r.set(clipRect->fLeft >> shiftUp, clipRect->fTop >> shiftUp,
315 clipRect->fRight >> shiftUp, clipRect->fBottom >> shiftUp);
316 qclipper.setClip(r);
317 }
318
319 while ((verb = iter.next(pts)) != SkPath::kDone_Verb) {
320 switch (verb) {
321 case SkPath::kLine_Verb:
322 if (edge->setLine(pts[0], pts[1], clipRect, shiftUp)) {
323 *list++ = edge;
324 edge = (SkEdge*)((char*)edge + sizeof(SkEdge));
325 }
326 break;
327 case SkPath::kQuad_Verb: {
328 SkPoint tmp[5], clippedPts[3];
329 SkPoint* p = tmp;
330 int count = SkChopQuadAtYExtrema(pts, tmp);
331
332 do {
333 const SkPoint* qpts = p;
334 if (clipRect) {
335 if (!qclipper.clipQuad(p, clippedPts)) {
336 goto NEXT_CHOPPED_QUAD;
337 }
338 qpts = clippedPts;
339 }
340 if (((SkQuadraticEdge*)edge)->setQuadratic(qpts, shiftUp)) {
341 *list++ = edge;
342 edge = (SkEdge*)((char*)edge + sizeof(SkQuadraticEdge));
343 }
344 NEXT_CHOPPED_QUAD:
345 p += 2;
346 } while (--count >= 0);
347 break;
348 }
349 case SkPath::kCubic_Verb: {
350 SkPoint tmp[10];
351 SkPoint* p = tmp;
352 int count = SkChopCubicAtYExtrema(pts, tmp);
353 SkASSERT(count >= 0 && count <= 2);
354
355 do {
356 if (((SkCubicEdge*)edge)->setCubic(p, clipRect, shiftUp))
357 {
358 *list++ = edge;
359 edge = (SkEdge*)((char*)edge + sizeof(SkCubicEdge));
360 }
361 p += 3;
362 } while (--count >= 0);
363 break;
364 }
365 default:
366 break;
367 }
368 }
369 return (int)(list - start);
370 }
371
372 extern "C" {
edge_compare(const void * a,const void * b)373 static int edge_compare(const void* a, const void* b)
374 {
375 const SkEdge* edgea = *(const SkEdge**)a;
376 const SkEdge* edgeb = *(const SkEdge**)b;
377
378 int valuea = edgea->fFirstY;
379 int valueb = edgeb->fFirstY;
380
381 if (valuea == valueb)
382 {
383 valuea = edgea->fX;
384 valueb = edgeb->fX;
385 }
386
387 // this overflows if valuea >>> valueb or vice-versa
388 // return valuea - valueb;
389 // do perform the slower but safe compares
390 return (valuea < valueb) ? -1 : (valuea > valueb);
391 }
392 }
393
sort_edges(SkEdge * list[],int count,SkEdge ** last)394 static SkEdge* sort_edges(SkEdge* list[], int count, SkEdge** last)
395 {
396 qsort(list, count, sizeof(SkEdge*), edge_compare);
397
398 // now make the edges linked in sorted order
399 for (int i = 1; i < count; i++)
400 {
401 list[i - 1]->fNext = list[i];
402 list[i]->fPrev = list[i - 1];
403 }
404
405 *last = list[count - 1];
406 return list[0];
407 }
408
409 #ifdef SK_DEBUG
410 /* 'quick' computation of the max sized needed to allocated for
411 our edgelist.
412 */
worst_case_edge_count(const SkPath & path,size_t * storage)413 static int worst_case_edge_count(const SkPath& path, size_t* storage)
414 {
415 size_t size = 0;
416 int edgeCount = 0;
417
418 SkPath::Iter iter(path, true);
419 SkPath::Verb verb;
420
421 while ((verb = iter.next(NULL)) != SkPath::kDone_Verb)
422 {
423 switch (verb) {
424 case SkPath::kLine_Verb:
425 edgeCount += 1;
426 size += sizeof(SkQuadraticEdge); // treat line like Quad (in case its > 512)
427 break;
428 case SkPath::kQuad_Verb:
429 edgeCount += 2; // might need 2 edges when we chop on Y extrema
430 size += 2 * sizeof(SkQuadraticEdge);
431 break;
432 case SkPath::kCubic_Verb:
433 edgeCount += 3; // might need 3 edges when we chop on Y extrema
434 size += 3 * sizeof(SkCubicEdge);
435 break;
436 default:
437 break;
438 }
439 }
440
441 SkASSERT(storage);
442 *storage = size;
443 return edgeCount;
444 }
445 #endif
446
447 /* Much faster than worst_case_edge_count, but over estimates even more
448 */
cheap_worst_case_edge_count(const SkPath & path,size_t * storage)449 static int cheap_worst_case_edge_count(const SkPath& path, size_t* storage)
450 {
451 int ptCount = path.getPoints(NULL, 0);
452 int edgeCount = ptCount;
453 *storage = edgeCount * sizeof(SkCubicEdge);
454 return edgeCount;
455 }
456
457 // clipRect may be null, even though we always have a clip. This indicates that
458 // the path is contained in the clip, and so we can ignore it during the blit
459 //
460 // clipRect (if no null) has already been shifted up
461 //
sk_fill_path(const SkPath & path,const SkIRect * clipRect,SkBlitter * blitter,int stop_y,int shiftEdgesUp,const SkRegion & clipRgn)462 void sk_fill_path(const SkPath& path, const SkIRect* clipRect, SkBlitter* blitter,
463 int stop_y, int shiftEdgesUp, const SkRegion& clipRgn)
464 {
465 SkASSERT(&path && blitter);
466
467 size_t size;
468 int maxCount = cheap_worst_case_edge_count(path, &size);
469
470 #ifdef SK_DEBUG
471 {
472 size_t size2;
473 int maxCount2 = worst_case_edge_count(path, &size2);
474
475 SkASSERT(maxCount >= maxCount2 && size >= size2);
476 }
477 #endif
478
479 SkAutoMalloc memory(maxCount * sizeof(SkEdge*) + size);
480 SkEdge** list = (SkEdge**)memory.get();
481 SkEdge* edge = (SkEdge*)(list + maxCount);
482 int count = build_edges(edge, path, clipRect, list, shiftEdgesUp);
483 SkEdge headEdge, tailEdge, *last;
484
485 SkASSERT(count <= maxCount);
486 if (count < 2) {
487 return;
488 }
489
490 // this returns the first and last edge after they're sorted into a dlink list
491 edge = sort_edges(list, count, &last);
492
493 headEdge.fPrev = NULL;
494 headEdge.fNext = edge;
495 headEdge.fFirstY = kEDGE_HEAD_Y;
496 headEdge.fX = SK_MinS32;
497 edge->fPrev = &headEdge;
498
499 tailEdge.fPrev = last;
500 tailEdge.fNext = NULL;
501 tailEdge.fFirstY = kEDGE_TAIL_Y;
502 last->fNext = &tailEdge;
503
504 // now edge is the head of the sorted linklist
505
506 stop_y <<= shiftEdgesUp;
507 if (clipRect && stop_y > clipRect->fBottom) {
508 stop_y = clipRect->fBottom;
509 }
510
511 InverseBlitter ib;
512 PrePostProc proc = NULL;
513
514 if (path.isInverseFillType()) {
515 ib.setBlitter(blitter, clipRgn.getBounds(), shiftEdgesUp);
516 blitter = &ib;
517 proc = PrePostInverseBlitterProc;
518 }
519
520 walk_edges(&headEdge, path.getFillType(), blitter, stop_y, proc);
521 }
522
sk_blit_above_and_below(SkBlitter * blitter,const SkIRect & ir,const SkRegion & clip)523 void sk_blit_above_and_below(SkBlitter* blitter, const SkIRect& ir,
524 const SkRegion& clip) {
525 const SkIRect& cr = clip.getBounds();
526 SkIRect tmp;
527
528 tmp.fLeft = cr.fLeft;
529 tmp.fRight = cr.fRight;
530
531 tmp.fTop = cr.fTop;
532 tmp.fBottom = ir.fTop;
533 if (!tmp.isEmpty()) {
534 blitter->blitRectRegion(tmp, clip);
535 }
536
537 tmp.fTop = ir.fBottom;
538 tmp.fBottom = cr.fBottom;
539 if (!tmp.isEmpty()) {
540 blitter->blitRectRegion(tmp, clip);
541 }
542 }
543
544 /////////////////////////////////////////////////////////////////////////////////////
545
SkScanClipper(SkBlitter * blitter,const SkRegion * clip,const SkIRect & ir)546 SkScanClipper::SkScanClipper(SkBlitter* blitter, const SkRegion* clip, const SkIRect& ir)
547 {
548 fBlitter = NULL; // null means blit nothing
549 fClipRect = NULL;
550
551 if (clip)
552 {
553 fClipRect = &clip->getBounds();
554 if (!SkIRect::Intersects(*fClipRect, ir)) // completely clipped out
555 return;
556
557 if (clip->isRect())
558 {
559 if (fClipRect->contains(ir))
560 fClipRect = NULL;
561 else
562 {
563 // only need a wrapper blitter if we're horizontally clipped
564 if (fClipRect->fLeft > ir.fLeft || fClipRect->fRight < ir.fRight)
565 {
566 fRectBlitter.init(blitter, *fClipRect);
567 blitter = &fRectBlitter;
568 }
569 }
570 }
571 else
572 {
573 fRgnBlitter.init(blitter, clip);
574 blitter = &fRgnBlitter;
575 }
576 }
577 fBlitter = blitter;
578 }
579
580 ///////////////////////////////////////////////////////////////////////////////
581
FillPath(const SkPath & path,const SkRegion & clip,SkBlitter * blitter)582 void SkScan::FillPath(const SkPath& path, const SkRegion& clip,
583 SkBlitter* blitter) {
584 if (clip.isEmpty()) {
585 return;
586 }
587
588 SkIRect ir;
589 path.getBounds().round(&ir);
590 if (ir.isEmpty()) {
591 if (path.isInverseFillType()) {
592 blitter->blitRegion(clip);
593 }
594 return;
595 }
596
597 SkScanClipper clipper(blitter, &clip, ir);
598
599 blitter = clipper.getBlitter();
600 if (blitter) {
601 if (path.isInverseFillType()) {
602 sk_blit_above_and_below(blitter, ir, clip);
603 }
604 sk_fill_path(path, clipper.getClipRect(), blitter, ir.fBottom, 0, clip);
605 } else {
606 // what does it mean to not have a blitter if path.isInverseFillType???
607 }
608 }
609
610 ///////////////////////////////////////////////////////////////////////////////
611
build_tri_edges(SkEdge edge[],const SkPoint pts[],const SkIRect * clipRect,SkEdge * list[])612 static int build_tri_edges(SkEdge edge[], const SkPoint pts[],
613 const SkIRect* clipRect, SkEdge* list[]) {
614 SkEdge** start = list;
615
616 if (edge->setLine(pts[0], pts[1], clipRect, 0)) {
617 *list++ = edge;
618 edge = (SkEdge*)((char*)edge + sizeof(SkEdge));
619 }
620 if (edge->setLine(pts[1], pts[2], clipRect, 0)) {
621 *list++ = edge;
622 edge = (SkEdge*)((char*)edge + sizeof(SkEdge));
623 }
624 if (edge->setLine(pts[2], pts[0], clipRect, 0)) {
625 *list++ = edge;
626 }
627 return (int)(list - start);
628 }
629
630
sk_fill_triangle(const SkPoint pts[],const SkIRect * clipRect,SkBlitter * blitter,const SkIRect & ir)631 void sk_fill_triangle(const SkPoint pts[], const SkIRect* clipRect,
632 SkBlitter* blitter, const SkIRect& ir) {
633 SkASSERT(pts && blitter);
634
635 SkEdge edgeStorage[3];
636 SkEdge* list[3];
637
638 int count = build_tri_edges(edgeStorage, pts, clipRect, list);
639 if (count < 2) {
640 return;
641 }
642
643 SkEdge headEdge, tailEdge, *last;
644
645 // this returns the first and last edge after they're sorted into a dlink list
646 SkEdge* edge = sort_edges(list, count, &last);
647
648 headEdge.fPrev = NULL;
649 headEdge.fNext = edge;
650 headEdge.fFirstY = kEDGE_HEAD_Y;
651 headEdge.fX = SK_MinS32;
652 edge->fPrev = &headEdge;
653
654 tailEdge.fPrev = last;
655 tailEdge.fNext = NULL;
656 tailEdge.fFirstY = kEDGE_TAIL_Y;
657 last->fNext = &tailEdge;
658
659 // now edge is the head of the sorted linklist
660 int stop_y = ir.fBottom;
661 if (clipRect && stop_y > clipRect->fBottom) {
662 stop_y = clipRect->fBottom;
663 }
664 walk_edges(&headEdge, SkPath::kEvenOdd_FillType, blitter, stop_y, NULL);
665 }
666
FillTriangle(const SkPoint pts[],const SkRegion * clip,SkBlitter * blitter)667 void SkScan::FillTriangle(const SkPoint pts[], const SkRegion* clip,
668 SkBlitter* blitter) {
669 if (clip && clip->isEmpty()) {
670 return;
671 }
672
673 SkRect r;
674 SkIRect ir;
675 r.set(pts, 3);
676 r.round(&ir);
677 if (ir.isEmpty()) {
678 return;
679 }
680
681 SkScanClipper clipper(blitter, clip, ir);
682
683 blitter = clipper.getBlitter();
684 if (NULL != blitter) {
685 sk_fill_triangle(pts, clipper.getClipRect(), blitter, ir);
686 }
687 }
688
689