1 /*
2 * Copyright 2017 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 "include/core/SkString.h"
9 #include "include/private/SkNx.h"
10 #include "src/core/SkArenaAlloc.h"
11 #include "src/core/SkAutoBlitterChoose.h"
12 #include "src/core/SkConvertPixels.h"
13 #include "src/core/SkCoreBlitters.h"
14 #include "src/core/SkDraw.h"
15 #include "src/core/SkMatrixProvider.h"
16 #include "src/core/SkRasterClip.h"
17 #include "src/core/SkRasterPipeline.h"
18 #include "src/core/SkScan.h"
19 #include "src/core/SkVM.h"
20 #include "src/core/SkVMBlitter.h"
21 #include "src/core/SkVertState.h"
22 #include "src/core/SkVerticesPriv.h"
23 #include "src/shaders/SkColorShader.h"
24 #include "src/shaders/SkComposeShader.h"
25 #include "src/shaders/SkShaderBase.h"
26
27 struct Matrix43 {
28 float fMat[12]; // column major
29
mapMatrix4330 Sk4f map(float x, float y) const {
31 return Sk4f::Load(&fMat[0]) * x + Sk4f::Load(&fMat[4]) * y + Sk4f::Load(&fMat[8]);
32 }
33
34 // Pass a by value, so we don't have to worry about aliasing with this
setConcatMatrix4335 void setConcat(const Matrix43 a, const SkMatrix& b) {
36 SkASSERT(!b.hasPerspective());
37
38 fMat[ 0] = a.dot(0, b.getScaleX(), b.getSkewY());
39 fMat[ 1] = a.dot(1, b.getScaleX(), b.getSkewY());
40 fMat[ 2] = a.dot(2, b.getScaleX(), b.getSkewY());
41 fMat[ 3] = a.dot(3, b.getScaleX(), b.getSkewY());
42
43 fMat[ 4] = a.dot(0, b.getSkewX(), b.getScaleY());
44 fMat[ 5] = a.dot(1, b.getSkewX(), b.getScaleY());
45 fMat[ 6] = a.dot(2, b.getSkewX(), b.getScaleY());
46 fMat[ 7] = a.dot(3, b.getSkewX(), b.getScaleY());
47
48 fMat[ 8] = a.dot(0, b.getTranslateX(), b.getTranslateY()) + a.fMat[ 8];
49 fMat[ 9] = a.dot(1, b.getTranslateX(), b.getTranslateY()) + a.fMat[ 9];
50 fMat[10] = a.dot(2, b.getTranslateX(), b.getTranslateY()) + a.fMat[10];
51 fMat[11] = a.dot(3, b.getTranslateX(), b.getTranslateY()) + a.fMat[11];
52 }
53
54 private:
dotMatrix4355 float dot(int index, float x, float y) const {
56 return fMat[index + 0] * x + fMat[index + 4] * y;
57 }
58 };
59
60 static bool SK_WARN_UNUSED_RESULT
texture_to_matrix(const VertState & state,const SkPoint verts[],const SkPoint texs[],SkMatrix * matrix)61 texture_to_matrix(const VertState& state, const SkPoint verts[], const SkPoint texs[],
62 SkMatrix* matrix) {
63 SkPoint src[3], dst[3];
64
65 src[0] = texs[state.f0];
66 src[1] = texs[state.f1];
67 src[2] = texs[state.f2];
68 dst[0] = verts[state.f0];
69 dst[1] = verts[state.f1];
70 dst[2] = verts[state.f2];
71 return matrix->setPolyToPoly(src, dst, 3);
72 }
73
74 class SkTriColorShader : public SkShaderBase {
75 public:
SkTriColorShader(bool isOpaque,bool usePersp)76 SkTriColorShader(bool isOpaque, bool usePersp) : fIsOpaque(isOpaque), fUsePersp(usePersp) {}
77
78 // This gets called for each triangle, without re-calling onAppendStages.
79 bool update(const SkMatrix& ctmInv, const SkPoint pts[], const SkPMColor4f colors[],
80 int index0, int index1, int index2);
81
82 protected:
83 #ifdef SK_ENABLE_LEGACY_SHADERCONTEXT
onMakeContext(const ContextRec & rec,SkArenaAlloc * alloc) const84 Context* onMakeContext(const ContextRec& rec, SkArenaAlloc* alloc) const override {
85 return nullptr;
86 }
87 #endif
onAppendStages(const SkStageRec & rec) const88 bool onAppendStages(const SkStageRec& rec) const override {
89 rec.fPipeline->append(SkRasterPipeline::seed_shader);
90 if (fUsePersp) {
91 rec.fPipeline->append(SkRasterPipeline::matrix_perspective, &fM33);
92 }
93 rec.fPipeline->append(SkRasterPipeline::matrix_4x3, &fM43);
94 return true;
95 }
96
97 skvm::Color onProgram(skvm::Builder*,
98 skvm::Coord, skvm::Coord, skvm::Color,
99 const SkMatrixProvider&, const SkMatrix*, const SkColorInfo&,
100 skvm::Uniforms*, SkArenaAlloc*) const override;
101
102 private:
isOpaque() const103 bool isOpaque() const override { return fIsOpaque; }
104 // For serialization. This will never be called.
getFactory() const105 Factory getFactory() const override { return nullptr; }
getTypeName() const106 const char* getTypeName() const override { return nullptr; }
107
108 // If fUsePersp, we need both of these matrices,
109 // otherwise we can combine them, and only use fM43
110
111 Matrix43 fM43;
112 SkMatrix fM33;
113 const bool fIsOpaque;
114 const bool fUsePersp; // controls our stages, and what we do in update()
115 mutable skvm::Uniform fColorMatrix;
116 mutable skvm::Uniform fCoordMatrix;
117
118 using INHERITED = SkShaderBase;
119 };
120
onProgram(skvm::Builder * b,skvm::Coord device,skvm::Coord local,skvm::Color,const SkMatrixProvider & matrices,const SkMatrix * localM,const SkColorInfo &,skvm::Uniforms * uniforms,SkArenaAlloc * alloc) const121 skvm::Color SkTriColorShader::onProgram(skvm::Builder* b,
122 skvm::Coord device, skvm::Coord local, skvm::Color,
123 const SkMatrixProvider& matrices, const SkMatrix* localM,
124 const SkColorInfo&, skvm::Uniforms* uniforms,
125 SkArenaAlloc* alloc) const {
126
127 fColorMatrix = uniforms->pushPtr(&fM43);
128
129 skvm::F32 x = local.x,
130 y = local.y;
131
132 if (fUsePersp) {
133 fCoordMatrix = uniforms->pushPtr(&fM33);
134 auto dot = [&, x, y](int row) {
135 return b->mad(x, b->arrayF(fCoordMatrix, row),
136 b->mad(y, b->arrayF(fCoordMatrix, row + 3),
137 b->arrayF(fCoordMatrix, row + 6)));
138 };
139
140 x = dot(0);
141 y = dot(1);
142 x = x * (1.0f / dot(2));
143 y = y * (1.0f / dot(2));
144 }
145
146 auto colorDot = [&, x, y](int row) {
147 return b->mad(x, b->arrayF(fColorMatrix, row),
148 b->mad(y, b->arrayF(fColorMatrix, row + 4),
149 b->arrayF(fColorMatrix, row + 8)));
150 };
151
152 skvm::Color color;
153 color.r = colorDot(0);
154 color.g = colorDot(1);
155 color.b = colorDot(2);
156 color.a = colorDot(3);
157 return color;
158 }
159
update(const SkMatrix & ctmInv,const SkPoint pts[],const SkPMColor4f colors[],int index0,int index1,int index2)160 bool SkTriColorShader::update(const SkMatrix& ctmInv, const SkPoint pts[],
161 const SkPMColor4f colors[], int index0, int index1, int index2) {
162 SkMatrix m, im;
163 m.reset();
164 m.set(0, pts[index1].fX - pts[index0].fX);
165 m.set(1, pts[index2].fX - pts[index0].fX);
166 m.set(2, pts[index0].fX);
167 m.set(3, pts[index1].fY - pts[index0].fY);
168 m.set(4, pts[index2].fY - pts[index0].fY);
169 m.set(5, pts[index0].fY);
170 if (!m.invert(&im)) {
171 return false;
172 }
173
174 fM33.setConcat(im, ctmInv);
175
176 Sk4f c0 = Sk4f::Load(colors[index0].vec()),
177 c1 = Sk4f::Load(colors[index1].vec()),
178 c2 = Sk4f::Load(colors[index2].vec());
179
180 (c1 - c0).store(&fM43.fMat[0]);
181 (c2 - c0).store(&fM43.fMat[4]);
182 c0.store(&fM43.fMat[8]);
183
184 if (!fUsePersp) {
185 fM43.setConcat(fM43, fM33);
186 }
187 return true;
188 }
189
190 // Convert the SkColors into float colors. The conversion depends on some conditions:
191 // - If the pixmap has a dst colorspace, we have to be "color-correct".
192 // Do we map into dst-colorspace before or after we interpolate?
193 // - We have to decide when to apply per-color alpha (before or after we interpolate)
194 //
195 // For now, we will take a simple approach, but recognize this is just a start:
196 // - convert colors into dst colorspace before interpolation (matches gradients)
197 // - apply per-color alpha before interpolation (matches old version of vertices)
198 //
convert_colors(const SkColor src[],int count,SkColorSpace * deviceCS,SkArenaAlloc * alloc)199 static SkPMColor4f* convert_colors(const SkColor src[], int count, SkColorSpace* deviceCS,
200 SkArenaAlloc* alloc) {
201 SkPMColor4f* dst = alloc->makeArray<SkPMColor4f>(count);
202 SkImageInfo srcInfo = SkImageInfo::Make(count, 1, kBGRA_8888_SkColorType,
203 kUnpremul_SkAlphaType, SkColorSpace::MakeSRGB());
204 SkImageInfo dstInfo = SkImageInfo::Make(count, 1, kRGBA_F32_SkColorType,
205 kPremul_SkAlphaType, sk_ref_sp(deviceCS));
206 SkAssertResult(SkConvertPixels(dstInfo, dst, 0, srcInfo, src, 0));
207 return dst;
208 }
209
compute_is_opaque(const SkColor colors[],int count)210 static bool compute_is_opaque(const SkColor colors[], int count) {
211 uint32_t c = ~0;
212 for (int i = 0; i < count; ++i) {
213 c &= colors[i];
214 }
215 return SkColorGetA(c) == 0xFF;
216 }
217
fill_triangle_2(const VertState & state,SkBlitter * blitter,const SkRasterClip & rc,const SkPoint dev2[])218 static void fill_triangle_2(const VertState& state, SkBlitter* blitter, const SkRasterClip& rc,
219 const SkPoint dev2[]) {
220 SkPoint tmp[] = {
221 dev2[state.f0], dev2[state.f1], dev2[state.f2]
222 };
223 SkScan::FillTriangle(tmp, rc, blitter);
224 }
225
226 static constexpr int kMaxClippedTrianglePointCount = 4;
fill_triangle_3(const VertState & state,SkBlitter * blitter,const SkRasterClip & rc,const SkPoint3 dev3[])227 static void fill_triangle_3(const VertState& state, SkBlitter* blitter, const SkRasterClip& rc,
228 const SkPoint3 dev3[]) {
229 // Compute the crossing point (across zero) for the two values, expressed as a
230 // normalized 0...1 value. If curr is 0, returns 0. If next is 0, returns 1.
231 auto computeT = [](float curr, float next) {
232 // Check that 0 is between next and curr.
233 SkASSERT((next <= 0 && 0 < curr) || (curr <= 0 && 0 < next));
234 float t = curr / (curr - next);
235 SkASSERT(0 <= t && t <= 1);
236 return t;
237 };
238
239 auto lerp = [](SkPoint3 curr, SkPoint3 next, float t) {
240 return curr + t * (next - curr);
241 };
242
243 constexpr float tol = 0.05f;
244 // tol is the nudge away from zero, to keep the numerics nice.
245 // Think of it as our near-clipping-plane (or w-plane).
246 auto clip = [&](SkPoint3 curr, SkPoint3 next) {
247 // Return the point between curr and next where the fZ value crosses tol.
248 // To be (really) perspective correct, we should be computing based on 1/Z, not Z.
249 // For now, this is close enough (and faster).
250 return lerp(curr, next, computeT(curr.fZ - tol, next.fZ - tol));
251 };
252
253 // Clip a triangle (based on its homogeneous W values), and return the projected polygon.
254 // Since we only clip against one "edge"/plane, the max number of points in the clipped
255 // polygon is 4.
256 auto clipTriangle = [&](SkPoint dst[], const int idx[3], const SkPoint3 pts[]) -> int {
257 SkPoint3 outPoints[kMaxClippedTrianglePointCount];
258 SkPoint3* outP = outPoints;
259
260 for (int i = 0; i < 3; ++i) {
261 int curr = idx[i];
262 int next = idx[(i + 1) % 3];
263 if (pts[curr].fZ > tol) {
264 *outP++ = pts[curr];
265 if (pts[next].fZ <= tol) { // curr is IN, next is OUT
266 *outP++ = clip(pts[curr], pts[next]);
267 }
268 } else {
269 if (pts[next].fZ > tol) { // curr is OUT, next is IN
270 *outP++ = clip(pts[curr], pts[next]);
271 }
272 }
273 }
274
275 const int count = SkTo<int>(outP - outPoints);
276 SkASSERT(count == 0 || count == 3 || count == 4);
277 for (int i = 0; i < count; ++i) {
278 float scale = 1.0f / outPoints[i].fZ;
279 dst[i].set(outPoints[i].fX * scale, outPoints[i].fY * scale);
280 }
281 return count;
282 };
283
284 SkPoint tmp[kMaxClippedTrianglePointCount];
285 int idx[] = { state.f0, state.f1, state.f2 };
286 if (int n = clipTriangle(tmp, idx, dev3)) {
287 // TODO: SkScan::FillConvexPoly(tmp, n, ...);
288 SkASSERT(n == 3 || n == 4);
289 SkScan::FillTriangle(tmp, rc, blitter);
290 if (n == 4) {
291 tmp[1] = tmp[2];
292 tmp[2] = tmp[3];
293 SkScan::FillTriangle(tmp, rc, blitter);
294 }
295 }
296 }
297
fill_triangle(const VertState & state,SkBlitter * blitter,const SkRasterClip & rc,const SkPoint dev2[],const SkPoint3 dev3[])298 static void fill_triangle(const VertState& state, SkBlitter* blitter, const SkRasterClip& rc,
299 const SkPoint dev2[], const SkPoint3 dev3[]) {
300 if (dev3) {
301 fill_triangle_3(state, blitter, rc, dev3);
302 } else {
303 fill_triangle_2(state, blitter, rc, dev2);
304 }
305 }
306
307 extern bool gUseSkVMBlitter;
308
drawFixedVertices(const SkVertices * vertices,sk_sp<SkBlender> blender,const SkPaint & paint,const SkMatrix & ctmInverse,const SkPoint * dev2,const SkPoint3 * dev3,SkArenaAlloc * outerAlloc) const309 void SkDraw::drawFixedVertices(const SkVertices* vertices,
310 sk_sp<SkBlender> blender,
311 const SkPaint& paint,
312 const SkMatrix& ctmInverse,
313 const SkPoint* dev2,
314 const SkPoint3* dev3,
315 SkArenaAlloc* outerAlloc) const {
316 SkVerticesPriv info(vertices->priv());
317
318 const int vertexCount = info.vertexCount();
319 const int indexCount = info.indexCount();
320 const SkPoint* positions = info.positions();
321 const SkPoint* texCoords = info.texCoords();
322 const uint16_t* indices = info.indices();
323 const SkColor* colors = info.colors();
324
325 SkShader* paintShader = paint.getShader();
326
327 if (paintShader) {
328 if (!texCoords) {
329 texCoords = positions;
330 }
331 } else {
332 texCoords = nullptr;
333 }
334
335 bool blenderIsDst = false;
336 // We can simplify things for certain blend modes. This is for speed, and SkShader_Blend
337 // itself insists we don't pass kSrc or kDst to it.
338 if (std::optional<SkBlendMode> bm = as_BB(blender)->asBlendMode(); bm.has_value() && colors) {
339 switch (*bm) {
340 case SkBlendMode::kSrc:
341 colors = nullptr;
342 break;
343 case SkBlendMode::kDst:
344 blenderIsDst = true;
345 texCoords = nullptr;
346 paintShader = nullptr;
347 break;
348 default: break;
349 }
350 }
351
352 // There is a paintShader iff there is texCoords.
353 SkASSERT((texCoords != nullptr) == (paintShader != nullptr));
354
355 SkMatrix ctm = fMatrixProvider->localToDevice();
356 const bool usePerspective = ctm.hasPerspective();
357
358 SkTriColorShader* triColorShader = nullptr;
359 SkPMColor4f* dstColors = nullptr;
360 if (colors) {
361 dstColors = convert_colors(colors, vertexCount, fDst.colorSpace(), outerAlloc);
362 triColorShader = outerAlloc->make<SkTriColorShader>(compute_is_opaque(colors, vertexCount),
363 usePerspective);
364 }
365
366 // Combines per-vertex colors with 'shader' using 'blender'.
367 auto applyShaderColorBlend = [&](SkShader* shader) -> SkShader* {
368 if (!colors) {
369 return shader;
370 }
371 if (blenderIsDst) {
372 return triColorShader;
373 }
374 if (!shader) {
375 // When there is no shader then the blender applies to the vertex colors and opaque
376 // paint color.
377 shader = outerAlloc->make<SkColor4Shader>(paint.getColor4f().makeOpaque(), nullptr);
378 }
379 return outerAlloc->make<SkShader_Blend>(
380 blender, sk_ref_sp(triColorShader), sk_ref_sp(shader));
381 };
382
383 auto rpblit = [&]() {
384 VertState state(vertexCount, indices, indexCount);
385 VertState::Proc vertProc = state.chooseProc(info.mode());
386 SkShader* shader = applyShaderColorBlend(paintShader);
387
388 SkPaint shaderPaint(paint);
389 shaderPaint.setShader(sk_ref_sp(shader));
390
391 if (!texCoords) { // only tricolor shader
392 auto blitter = SkCreateRasterPipelineBlitter(
393 fDst, shaderPaint, *fMatrixProvider, outerAlloc, this->fRC->clipShader());
394 if (!blitter) {
395 return false;
396 }
397 while (vertProc(&state)) {
398 if (triColorShader && !triColorShader->update(ctmInverse, positions, dstColors,
399 state.f0, state.f1, state.f2)) {
400 continue;
401 }
402 fill_triangle(state, blitter, *fRC, dev2, dev3);
403 }
404 return true;
405 }
406
407 SkRasterPipeline pipeline(outerAlloc);
408 SkStageRec rec = {&pipeline,
409 outerAlloc,
410 fDst.colorType(),
411 fDst.colorSpace(),
412 shaderPaint,
413 nullptr,
414 *fMatrixProvider};
415 if (auto updater = as_SB(shader)->appendUpdatableStages(rec)) {
416 bool isOpaque = shader->isOpaque();
417 if (triColorShader) {
418 isOpaque = false; // unless we want to walk all the colors, and see if they are
419 // all opaque (and the blend mode will keep them that way
420 }
421
422 // Positions as texCoords? The local matrix is always identity, so update once
423 if (texCoords == positions) {
424 if (!updater->update(ctm)) {
425 return true;
426 }
427 }
428
429 auto blitter = SkCreateRasterPipelineBlitter(
430 fDst, shaderPaint, pipeline, isOpaque, outerAlloc, fRC->clipShader());
431 if (!blitter) {
432 return false;
433 }
434 while (vertProc(&state)) {
435 if (triColorShader && !triColorShader->update(ctmInverse, positions, dstColors,
436 state.f0, state.f1, state.f2)) {
437 continue;
438 }
439
440 SkMatrix localM;
441 if ((texCoords == positions) ||
442 (texture_to_matrix(state, positions, texCoords, &localM) &&
443 updater->update(SkMatrix::Concat(ctm, localM)))) {
444 fill_triangle(state, blitter, *fRC, dev2, dev3);
445 }
446 }
447 } else {
448 // must rebuild pipeline for each triangle, to pass in the computed ctm
449 while (vertProc(&state)) {
450 if (triColorShader && !triColorShader->update(ctmInverse, positions, dstColors,
451 state.f0, state.f1, state.f2)) {
452 continue;
453 }
454
455 SkSTArenaAlloc<2048> innerAlloc;
456
457 const SkMatrixProvider* matrixProvider = fMatrixProvider;
458 SkTLazy<SkPreConcatMatrixProvider> preConcatMatrixProvider;
459 if (texCoords && (texCoords != positions)) {
460 SkMatrix localM;
461 if (!texture_to_matrix(state, positions, texCoords, &localM)) {
462 continue;
463 }
464 matrixProvider = preConcatMatrixProvider.init(*matrixProvider, localM);
465 }
466
467 // It'd be nice if we could detect this will fail earlier.
468 auto blitter = SkCreateRasterPipelineBlitter(
469 fDst, shaderPaint, *matrixProvider, &innerAlloc, this->fRC->clipShader());
470 if (!blitter) {
471 return false;
472 }
473 fill_triangle(state, blitter, *fRC, dev2, dev3);
474 }
475 }
476 return true;
477 };
478
479 if (gUseSkVMBlitter || !rpblit()) {
480 VertState state(vertexCount, indices, indexCount);
481 VertState::Proc vertProc = state.chooseProc(info.mode());
482
483 // No colors are changing and no texture coordinates are changing, so no updates between
484 // triangles are needed. Use SkVM to blit the triangles.
485 SkShader* shader = paintShader;
486 SkUpdatableShader* texCoordShader = nullptr;
487 if (texCoords && texCoords != positions) {
488 texCoordShader = as_SB(shader)->updatableShader(outerAlloc);
489 shader = texCoordShader;
490 }
491 shader = applyShaderColorBlend(shader);
492
493 SkPaint shaderPaint{paint};
494 shaderPaint.setShader(sk_ref_sp(shader));
495 auto blitter = SkVMBlitter::Make(
496 fDst, shaderPaint, *fMatrixProvider, outerAlloc, this->fRC->clipShader());
497 if (!blitter) {
498 return;
499 }
500 while (vertProc(&state)) {
501 SkMatrix localM;
502 if (texCoordShader && !(texture_to_matrix(state, positions, texCoords, &localM) &&
503 texCoordShader->update(SkMatrix::Concat(ctm, localM)))) {
504 continue;
505 }
506
507 if (triColorShader && !triColorShader->update(ctmInverse, positions, dstColors,state.f0,
508 state.f1, state.f2)) {
509 continue;
510 }
511
512 fill_triangle(state, blitter, *fRC, dev2, dev3);
513 }
514 }
515 }
516
drawVertices(const SkVertices * vertices,sk_sp<SkBlender> blender,const SkPaint & paint) const517 void SkDraw::drawVertices(const SkVertices* vertices,
518 sk_sp<SkBlender> blender,
519 const SkPaint& paint) const {
520 SkVerticesPriv info(vertices->priv());
521 const int vertexCount = info.vertexCount();
522 const int indexCount = info.indexCount();
523
524 // abort early if there is nothing to draw
525 if (vertexCount < 3 || (indexCount > 0 && indexCount < 3) || fRC->isEmpty()) {
526 return;
527 }
528 SkMatrix ctm = fMatrixProvider->localToDevice();
529 SkMatrix ctmInv;
530 if (!ctm.invert(&ctmInv)) {
531 return;
532 }
533
534 constexpr size_t kDefVertexCount = 16;
535 constexpr size_t kOuterSize = sizeof(SkTriColorShader) +
536 sizeof(SkShader_Blend) +
537 (2 * sizeof(SkPoint) + sizeof(SkColor4f)) * kDefVertexCount;
538 SkSTArenaAlloc<kOuterSize> outerAlloc;
539
540 SkPoint* dev2 = nullptr;
541 SkPoint3* dev3 = nullptr;
542
543 if (ctm.hasPerspective()) {
544 dev3 = outerAlloc.makeArray<SkPoint3>(vertexCount);
545 ctm.mapHomogeneousPoints(dev3, info.positions(), vertexCount);
546 // similar to the bounds check for 2d points (below)
547 if (!SkScalarsAreFinite((const SkScalar*)dev3, vertexCount * 3)) {
548 return;
549 }
550 } else {
551 dev2 = outerAlloc.makeArray<SkPoint>(vertexCount);
552 ctm.mapPoints(dev2, info.positions(), vertexCount);
553
554 SkRect bounds;
555 // this also sets bounds to empty if we see a non-finite value
556 bounds.setBounds(dev2, vertexCount);
557 if (bounds.isEmpty()) {
558 return;
559 }
560 }
561
562 this->drawFixedVertices(vertices, std::move(blender), paint, ctmInv, dev2, dev3, &outerAlloc);
563 }
564