1 /*
   2  * Copyright (c) 2009, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.prism.impl.ps;
  27 
  28 import com.sun.javafx.geom.BaseBounds;
  29 import com.sun.javafx.geom.RectBounds;
  30 import com.sun.javafx.geom.Shape;
  31 import com.sun.javafx.geom.transform.BaseTransform;
  32 import com.sun.prism.BasicStroke;
  33 import com.sun.prism.Graphics;
  34 import com.sun.prism.Texture;
  35 import com.sun.prism.Texture.WrapMode;
  36 import com.sun.prism.paint.Paint;
  37 import com.sun.prism.shape.ShapeRep;
  38 import com.sun.prism.impl.Disposer;
  39 import com.sun.prism.impl.PrismSettings;
  40 import com.sun.prism.impl.VertexBuffer;
  41 import com.sun.prism.impl.ps.BaseShaderContext.MaskType;
  42 import com.sun.prism.impl.shape.ShapeUtil;
  43 import com.sun.prism.impl.shape.MaskData;
  44 import com.sun.prism.ps.Shader;
  45 import java.util.Arrays;
  46 import java.util.Comparator;
  47 
  48 /**
  49  * An implementation of ShapeRep that attempts to cache and reuse the
  50  * mask texture that is used to render the filled/stroked geometry.
  51  * There is an artificial cap on the size of shapes that are considered
  52  * for caching, so that we do not fill up VRAM with lots of large shape
  53  * masks.  The shape is considered for caching only when it is rendered
  54  * a certain number of times with unchanging transform (ignoring the
  55  * translation components) and geometry.  This means that this class is
  56  * good for caching static shapes that are either completely stationary
  57  * or being translated.  This class will also attempt to reuse a mask
  58  * that corresponds to a completely different shape instance as long as
  59  * the two shapes are equal and the transforms only differ by their
  60  * translation components.  This means that if you have 1000 Path nodes
  61  * all with exactly the same geometry but with different translation factors,
  62  * then we will only rasterize and cache a single mask texture and reuse
  63  * it among all the Path nodes.
  64  *
  65  * (Of course, the fact that we reuse the same mask texture for different
  66  * sub-pixel translation factors means we're knowingly being a bit sloppy,
  67  * so when caching is enabled you may see some dancing at the shape edges
  68  * for slowly animating translations, but otherwise will hopefully not be
  69  * too noticeable.)
  70  *
  71  * The current implementation limits the size of the cache (512 pixels in
  72  * each dimension, and 4 MB in total) so that it doesn't grow without bound.
  73  * Space is granted on a first come first served basis.
  74  */
  75 public class CachingShapeRep implements ShapeRep {
  76 
  77     private CachingShapeRepState fillState;
  78     private CachingShapeRepState drawState;
  79 
  80     public CachingShapeRep() {
  81     }
  82 
  83     CachingShapeRepState createState() {
  84         return new CachingShapeRepState();
  85     }
  86 
  87     public boolean is3DCapable() {
  88         return false;
  89     }
  90 
  91     public void invalidate(InvalidationType type) {
  92         // NOTE: for now we invalidate for any location or geometry change;
  93         // should consider allowing certain location changes...
  94         if (fillState != null) {
  95             fillState.invalidate();
  96         }
  97         if (drawState != null) {
  98             drawState.invalidate();
  99         }
 100     }
 101 
 102     public void fill(Graphics g, Shape shape, BaseBounds bounds) {
 103         if (fillState == null) {
 104             fillState = createState();
 105         }
 106         fillState.render(g, shape, (RectBounds) bounds, null);
 107     }
 108 
 109     public void draw(Graphics g, Shape shape, BaseBounds bounds) {
 110         if (drawState == null) {
 111             drawState = createState();
 112         }
 113         drawState.render(g, shape,(RectBounds) bounds, g.getStroke());
 114     }
 115 
 116     public void dispose() {
 117         if (fillState != null) {
 118             fillState.dispose();
 119             fillState = null;
 120         }
 121         if (drawState != null) {
 122             drawState.dispose();
 123             drawState = null;
 124         }
 125     }
 126 }
 127 
 128 class CachingShapeRepState {
 129 
 130     private static class MaskTexData {
 131         private CacheEntry cacheEntry;
 132         private Texture maskTex;
 133         private float maskX;
 134         private float maskY;
 135         private int maskW;
 136         private int maskH;
 137 
 138         void adjustOrigin(BaseTransform xform) {
 139             float dx = (float)(xform.getMxt()-cacheEntry.xform.getMxt());
 140             float dy = (float)(xform.getMyt()-cacheEntry.xform.getMyt());
 141             this.maskX = cacheEntry.texData.maskX + dx;
 142             this.maskY = cacheEntry.texData.maskY + dy;
 143         }
 144 
 145         MaskTexData copy() {
 146             MaskTexData data = new MaskTexData();
 147             data.cacheEntry = this.cacheEntry;
 148             data.maskTex = this.maskTex;
 149             data.maskX = this.maskX;
 150             data.maskY = this.maskY;
 151             data.maskW = this.maskW;
 152             data.maskH = this.maskH;
 153             return data;
 154         }
 155 
 156         void copyInto(MaskTexData other) {
 157             if (other == null) {
 158                 throw new InternalError("MaskTexData must be non-null");
 159             }
 160             other.cacheEntry = this.cacheEntry;
 161             other.maskTex = this.maskTex;
 162             other.maskX = this.maskX;
 163             other.maskY = this.maskY;
 164             other.maskW = this.maskW;
 165             other.maskH = this.maskH;
 166         }
 167     }
 168 
 169     private static class CacheEntry {
 170         Shape shape;
 171         BasicStroke stroke;
 172         BaseTransform xform;
 173         RectBounds xformBounds;
 174         MaskTexData texData;
 175         boolean antialiasedShape;
 176         int refCount;
 177     }
 178 
 179     private static class MaskCache {
 180         private static final int MAX_MASK_DIM = 512;
 181         private static final int MAX_SIZE_IN_PIXELS = 4194304; // 4 MB
 182         private static Comparator<CacheEntry> comparator = (o1, o2) -> {
 183             int widthCompare = Float.compare(o1.xformBounds.getWidth(), o2.xformBounds.getWidth());
 184             if (widthCompare != 0) {
 185                 return widthCompare;
 186             }
 187             return Float.compare(o1.xformBounds.getHeight(), o2.xformBounds.getHeight());
 188         };
 189 
 190         private CacheEntry[] entries = new CacheEntry[8];
 191         private int entriesSize = 0;
 192         private int totalPixels;
 193 
 194         private CacheEntry tmpKey = new CacheEntry();
 195         {
 196             tmpKey.xformBounds = new RectBounds();
 197         }
 198 
 199         private void ensureSize(int size) {
 200             if (entries.length < size) {
 201                 CacheEntry[] newEntries = new CacheEntry[size * 3 / 2];
 202                 System.arraycopy(entries, 0, newEntries, 0, entries.length);
 203                 entries = newEntries;
 204             }
 205         }
 206 
 207         private void addEntry(CacheEntry entry) {
 208             ensureSize(entriesSize + 1);
 209             int pos = Arrays.binarySearch(entries, 0, entriesSize, entry, comparator);
 210             if (pos < 0) {
 211                 pos = ~pos;
 212             }
 213             System.arraycopy(entries, pos, entries, pos + 1, entriesSize - pos);
 214             entries[pos] = entry;
 215             ++entriesSize;
 216         }
 217 
 218         private void removeEntry(CacheEntry entry) {
 219             int pos = Arrays.binarySearch(entries, 0, entriesSize, entry, comparator);
 220             if (pos < 0) {
 221                 throw new IllegalStateException("Trying to remove a cached item that's not in the cache");
 222             }
 223             if (entries[pos] != entry) {
 224                 tmpKey.xformBounds.deriveWithNewBounds(0, 0, 0, entry.xformBounds.getWidth(), Math.nextAfter(entry.xformBounds.getHeight(), Float.NEGATIVE_INFINITY), 0);
 225                 pos = Arrays.binarySearch(entries, 0, entriesSize, tmpKey, comparator);
 226                 if (pos < 0) {
 227                     pos = ~pos;
 228                 }
 229                 tmpKey.xformBounds.deriveWithNewBounds(0, 0, 0, entry.xformBounds.getWidth(), Math.nextAfter(entry.xformBounds.getHeight(), Float.POSITIVE_INFINITY), 0);
 230                 int toPos = Arrays.binarySearch(entries, 0, entriesSize, tmpKey, comparator);
 231                 if (toPos < 0) {
 232                     toPos = ~toPos;
 233                 }
 234                 while (entries[pos] != entry && pos < toPos) { ++pos; };
 235                 if (pos >= toPos) {
 236                     throw new IllegalStateException("Trying to remove a cached item that's not in the cache");
 237                 }
 238             }
 239             System.arraycopy(entries, pos + 1, entries, pos, entriesSize - pos - 1);
 240             --entriesSize;
 241         }
 242 
 243         boolean hasRoom(RectBounds xformBounds) {
 244             int w = (int)(xformBounds.getWidth() + 0.5f);
 245             int h = (int)(xformBounds.getHeight() + 0.5f);
 246             int size = w*h;
 247             return
 248                 w <= MAX_MASK_DIM &&
 249                 h <= MAX_MASK_DIM &&
 250                 totalPixels + size <= MAX_SIZE_IN_PIXELS;
 251         }
 252 
 253         boolean entryMatches(CacheEntry entry, Shape shape, BasicStroke stroke, BaseTransform xform, boolean antialiasedShape) {
 254             return (entry.antialiasedShape == antialiasedShape) && equalsIgnoreTranslation(xform, entry.xform) && entry.shape.equals(shape) &&
 255                    (stroke == null ? entry.stroke == null : stroke.equals(entry.stroke));
 256 
 257         }
 258 
 259         void get(BaseShaderContext context,
 260                  MaskTexData texData,
 261                  Shape shape, BasicStroke stroke, BaseTransform xform,
 262                  RectBounds xformBounds,
 263                  boolean xformBoundsIsACopy, boolean antialiasedShape)
 264         {
 265             if (texData == null) {
 266                 throw new InternalError("MaskTexData must be non-null");
 267             }
 268             if (texData.cacheEntry != null) {
 269                 throw new InternalError("CacheEntry should already be null");
 270             }
 271 
 272             tmpKey.xformBounds.deriveWithNewBounds(0, 0, 0, xformBounds.getWidth(), Math.nextAfter(xformBounds.getHeight(), Float.NEGATIVE_INFINITY), 0);
 273             int i = Arrays.binarySearch(entries, 0, entriesSize, tmpKey, comparator);
 274             if (i < 0) {
 275                 i = ~i;
 276             }
 277 
 278             tmpKey.xformBounds.deriveWithNewBounds(0, 0, 0, xformBounds.getWidth(), Math.nextAfter(xformBounds.getHeight(), Float.POSITIVE_INFINITY), 0);
 279             int toPos = Arrays.binarySearch(entries, 0, entriesSize, tmpKey, comparator);
 280             if (toPos < 0) {
 281                 toPos = ~toPos;
 282             }
 283             for (;i < toPos; i++) {
 284                 CacheEntry entry = entries[i];
 285 
 286                 if (entryMatches(entry, shape, stroke, xform, antialiasedShape))
 287                 {
 288                     entry.texData.maskTex.lock();
 289                     if (entry.texData.maskTex.isSurfaceLost()) {
 290                         // Eventually refcount will go to zero and entry will be freed
 291                         entry.texData.maskTex.unlock();
 292                         continue;
 293                     }
 294                     // increment ref count for the chosen entry and
 295                     // link the given texData to it
 296                     entry.refCount++;
 297                     entry.texData.copyInto(texData);
 298                     texData.cacheEntry = entry;
 299                     // adjust the maskX/maskY by the delta between the
 300                     // cached transform and the current transform
 301                     texData.adjustOrigin(xform);
 302                     return;
 303                 }
 304             }
 305 
 306             // did not find an existing mask; create a new one here
 307             MaskData maskData =
 308                 ShapeUtil.rasterizeShape(shape, stroke, xformBounds, xform, true, antialiasedShape);
 309             int mw = maskData.getWidth();
 310             int mh = maskData.getHeight();
 311             texData.maskX = maskData.getOriginX();
 312             texData.maskY = maskData.getOriginY();
 313             texData.maskW = mw;
 314             texData.maskH = mh;
 315             texData.maskTex =
 316                 context.getResourceFactory().createMaskTexture(mw, mh, WrapMode.CLAMP_TO_ZERO);
 317             maskData.uploadToTexture(texData.maskTex, 0, 0, false);
 318             texData.maskTex.contentsUseful();
 319 
 320             // add the new mask texture to the cache; note that we copy the
 321             // shape and transform so that dependents are not affected
 322             // if the original geometry is mutated (since NGPath will reuse
 323             // and mutate a single Path2D instance, for example)
 324             CacheEntry entry = new CacheEntry();
 325             entry.shape = shape.copy();
 326             if (stroke != null) entry.stroke = stroke.copy();
 327             entry.xform = xform.copy();
 328             entry.xformBounds = xformBoundsIsACopy ? xformBounds : (RectBounds)xformBounds.copy();
 329             entry.texData = texData.copy();
 330             entry.antialiasedShape = antialiasedShape;
 331             entry.refCount = 1;
 332             texData.cacheEntry = entry;
 333             addEntry(entry);
 334             totalPixels += mw*mh;
 335         }
 336 
 337         void unref(MaskTexData texData) {
 338             if (texData == null) {
 339                 throw new InternalError("MaskTexData must be non-null");
 340             }
 341             CacheEntry entry = texData.cacheEntry;
 342             if (entry == null) {
 343                 return;
 344             }
 345             texData.cacheEntry = null;
 346             texData.maskTex = null;
 347             entry.refCount--;
 348             if (entry.refCount <= 0) {
 349                 removeEntry(entry);
 350                 entry.shape = null;
 351                 entry.stroke = null;
 352                 entry.xform = null;
 353                 entry.xformBounds = null;
 354                 entry.texData.maskTex.dispose();
 355                 entry.antialiasedShape = false;
 356                 entry.texData = null;
 357                 totalPixels -= (texData.maskW * texData.maskH);
 358             }
 359         }
 360     }
 361 
 362     /**
 363      * Returns true if the two transforms are equal or if they differ only
 364      * in their translation components; otherwise returns false.
 365      */
 366     private static boolean equalsIgnoreTranslation(BaseTransform a,
 367                                                    BaseTransform b)
 368     {
 369         if (a == b) {
 370             return true;
 371         }
 372 
 373         return
 374             a.getMxx() == b.getMxx() &&
 375             a.getMxy() == b.getMxy() &&
 376             a.getMyx() == b.getMyx() &&
 377             a.getMyy() == b.getMyy();
 378     }
 379 
 380     private static final BaseTransform IDENT = BaseTransform.IDENTITY_TRANSFORM;
 381     // NOTE: need separate MaskCache per context?
 382     private static final MaskCache maskCache = new MaskCache();
 383     // Number of back to back similar renderings before we cache the mask
 384     private static final int CACHE_THRESHOLD = 2;
 385 
 386     private int renderCount;
 387     private Boolean tryCache;
 388     private BaseTransform lastXform;
 389     private final MaskTexData texData;
 390     private float[] bbox;
 391 
 392     private final Object disposerReferent = new Object();
 393     private final Disposer.Record disposerRecord;
 394 
 395     CachingShapeRepState() {
 396         this.texData = new MaskTexData();
 397         this.disposerRecord = new CSRDisposerRecord(texData);
 398         Disposer.addRecord(disposerReferent, disposerRecord);
 399     }
 400 
 401     // Note: Subclasses may override this method to use a more direct op.
 402     void fillNoCache(Graphics g, Shape shape) {
 403         g.fill(shape);
 404     }
 405 
 406     // Note: Subclasses may override this method to use a more direct op.
 407     void drawNoCache(Graphics g, Shape shape) {
 408         g.draw(shape);
 409     }
 410 
 411     void invalidate() {
 412         // Note: this method will be called from the FX thread, so just mark
 413         // the state as invalid and then the next render() call will take
 414         // care of unref'ing the cache entry (on the render thread).
 415         renderCount = 0;
 416         tryCache = null;
 417         lastXform = null;
 418         bbox = null;
 419     }
 420 
 421     private void invalidateMaskTexData() {
 422         // Note: this method should only be called from the render thread
 423         // (since calling unref() may cause textures to be disposed).
 424         tryCache = null;
 425         lastXform = null;
 426         maskCache.unref(texData);
 427     }
 428 
 429     void render(Graphics g, Shape shape, RectBounds shapeBounds, BasicStroke stroke) {
 430         // The following is safe; this method does not mutate xform
 431         BaseTransform xform = g.getTransformNoClone();
 432 
 433         boolean doResetMask;
 434         boolean doUpdateMask;
 435 
 436         if (lastXform == null) {
 437             doResetMask = doUpdateMask = true;
 438         } else if (equalsIgnoreTranslation(xform, lastXform)) {
 439             doResetMask = false;
 440             doUpdateMask = (xform.getMxt() != lastXform.getMxt() ||
 441                             xform.getMyt() != lastXform.getMyt());
 442         } else {
 443             doResetMask = doUpdateMask = true;
 444         }
 445 
 446         // we need to invalidate our cached MaskTexData if:
 447         //   - lastXform is null, indicating that we were marked invalid
 448         //     (due to a geometry or location change), or
 449         //   - the current transform is significantly different than the last
 450         if (doResetMask) {
 451             invalidateMaskTexData();
 452 
 453             renderCount = 0;
 454         }
 455         if (doResetMask || doUpdateMask) {
 456             if (lastXform == null) {
 457                 lastXform = xform.copy();
 458             } else {
 459                 lastXform.setTransform(xform);
 460             }
 461         }
 462 
 463         if (texData.cacheEntry != null) {
 464             texData.maskTex.lock();
 465             if (texData.maskTex.isSurfaceLost()) {
 466                 texData.maskTex.unlock();
 467                 invalidateMaskTexData();
 468             }
 469         }
 470 
 471         RectBounds xformBounds = null;
 472         boolean boundsCopy = false;
 473 
 474         if (tryCache == null) {
 475             // determine whether the shape size is within the limits
 476             if (xform.isIdentity()) {
 477                 xformBounds = shapeBounds;
 478             } else {
 479                 xformBounds = new RectBounds();
 480                 boundsCopy = true;
 481                 //this is a safe cast as this ShapeRep is not 3d capable.
 482                 // all 3d transformed shapes are thus rendered by different ShapeRep
 483                 xformBounds = (RectBounds) xform.transform(shapeBounds, xformBounds);
 484             }
 485             tryCache = !xformBounds.isEmpty() && maskCache.hasRoom(xformBounds);
 486         }
 487 
 488         renderCount++;
 489         if (tryCache == Boolean.FALSE ||
 490             renderCount < CACHE_THRESHOLD ||
 491             (!(g instanceof BaseShaderGraphics)) ||
 492             ((BaseShaderGraphics)g).isComplexPaint())
 493         {
 494             // render the slow way if:
 495             //   - the shape size exceeds the threshold, or
 496             //   - we haven't rendered enough times to get an idea of
 497             //     whether it is worth caching the mask, or
 498             //   - there is a complex paint set (note that we could
 499             //     potentially use the cached mask in this case, but the
 500             //     complex paint case is not common enough to warrant
 501             //     further optimization at this time)
 502             if (stroke == null) {
 503                 fillNoCache(g, shape);
 504             } else {
 505                 drawNoCache(g, shape);
 506             }
 507             return;
 508         }
 509 
 510         BaseShaderGraphics bsg = (BaseShaderGraphics)g;
 511         BaseShaderContext context = bsg.getContext();
 512         if (doUpdateMask || texData.cacheEntry == null) {
 513             // need to create a new mask texture, or reuse an existing one
 514             if (xformBounds == null) {
 515                 if (xform.isIdentity()) {
 516                     xformBounds = shapeBounds;
 517                 } else {
 518                     xformBounds = new RectBounds();
 519                     boundsCopy = true;
 520                     xformBounds = (RectBounds) xform.transform(shapeBounds, xformBounds);
 521                 }
 522             }
 523 
 524             if (texData.cacheEntry != null) {
 525                 // in this case, we already have a valid mask texture, but
 526                 // the transform (translation) has changed since we last used
 527                 // it, so we just need to update the maskX/maskY variables
 528                 texData.adjustOrigin(xform);
 529             } else {
 530                 // the following will locate an existing cached mask that
 531                 // matches the given parameters, or failing that, will create
 532                 // a new mask and put it in the cache
 533                 maskCache.get(context, texData, shape, stroke, xform, xformBounds, boundsCopy, g.isAntialiasedShape());
 534             }
 535         }
 536 
 537         Paint paint = bsg.getPaint();
 538         float bx = 0f, by = 0f, bw = 0f, bh = 0f;
 539         if (paint.isProportional()) {
 540             if (bbox == null) {
 541                 bbox = new float[] {
 542                     Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
 543                     Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY,
 544                 };
 545                 Shape.accumulate(bbox, shape, BaseTransform.IDENTITY_TRANSFORM);
 546             }
 547             bx = bbox[0];
 548             by = bbox[1];
 549             bw = bbox[2] - bx;
 550             bh = bbox[3] - by;
 551         }
 552 
 553         int mw = texData.maskW;
 554         int mh = texData.maskH;
 555         Texture maskTex = texData.maskTex;
 556         float tw = maskTex.getPhysicalWidth();
 557         float th = maskTex.getPhysicalHeight();
 558         float dx1 = texData.maskX;
 559         float dy1 = texData.maskY;
 560         float dx2 = dx1 + mw;
 561         float dy2 = dy1 + mh;
 562         float tx1 = maskTex.getContentX() / tw;
 563         float ty1 = maskTex.getContentY() / th;
 564         float tx2 = tx1 + mw / tw;
 565         float ty2 = ty1 + mh / th;
 566 
 567         if (PrismSettings.primTextureSize != 0) {
 568             // the mask has been generated in device space, so we use
 569             // identity transform here
 570             Shader shader =
 571                 context.validatePaintOp(bsg, IDENT,
 572                                         MaskType.ALPHA_TEXTURE, texData.maskTex,
 573                                         bx, by, bw, bh);
 574 
 575             VertexBuffer vb = context.getVertexBuffer();
 576             vb.addQuad(dx1, dy1, dx2, dy2, tx1, ty1, tx2, ty2,
 577                        bsg.getPaintTextureTx(xform, shader, bx, by, bw, bh));
 578         } else {
 579             // the mask has been generated in device space, so we use
 580             // identity transform here
 581             context.validatePaintOp(bsg, IDENT, texData.maskTex, bx, by, bw, bh);
 582 
 583             VertexBuffer vb = context.getVertexBuffer();
 584             vb.addQuad(dx1, dy1, dx2, dy2, tx1, ty1, tx2, ty2);
 585         }
 586         maskTex.unlock();
 587     }
 588 
 589     void dispose() {
 590         // Note: this method will be called from the FX thread; just
 591         // invalidate and let the DisposerRecord take care of cutting
 592         // the ties with the mask cache and disposing textures if necessary
 593         // (on the render thread).
 594         invalidate();
 595     }
 596 
 597     private static class CSRDisposerRecord implements Disposer.Record {
 598         private MaskTexData texData;
 599 
 600         private CSRDisposerRecord(MaskTexData texData) {
 601             this.texData = texData;
 602         }
 603 
 604         public void dispose() {
 605             // Note: this method should only be called from the rendering thread
 606             if (texData != null) {
 607                 maskCache.unref(texData);
 608                 texData = null;
 609             }
 610         }
 611     }
 612 }