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 }