1 /* 2 * Copyright (c) 2011, 2015, 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.javafx.tk.quantum; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 import java.util.concurrent.locks.ReentrantLock; 31 import com.sun.javafx.geom.DirtyRegionContainer; 32 import com.sun.javafx.geom.DirtyRegionPool; 33 import com.sun.javafx.geom.RectBounds; 34 import com.sun.javafx.geom.Rectangle; 35 import com.sun.javafx.geom.transform.Affine3D; 36 import com.sun.javafx.geom.transform.BaseTransform; 37 import com.sun.javafx.geom.transform.GeneralTransform3D; 38 import com.sun.javafx.sg.prism.NGCamera; 39 import com.sun.javafx.sg.prism.NGNode; 40 import com.sun.javafx.sg.prism.NGPerspectiveCamera; 41 import com.sun.javafx.sg.prism.NodePath; 42 import com.sun.prism.Graphics; 43 import com.sun.prism.GraphicsResource; 44 import com.sun.prism.Image; 45 import com.sun.prism.Presentable; 46 import com.sun.prism.RTTexture; 47 import com.sun.prism.ResourceFactory; 48 import com.sun.prism.Texture; 49 import com.sun.prism.impl.PrismSettings; 50 import com.sun.prism.paint.Color; 51 import com.sun.prism.paint.Paint; 52 import com.sun.javafx.logging.PulseLogger; 53 import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; 54 55 /** 56 * Responsible for "painting" a scene. It invokes as appropriate API on the root NGNode 57 * of a scene to determine dirty regions, render roots, etc. Also calls the render root 58 * to render. Also invokes code to print dirty opts and paint overdraw rectangles according 59 * to debug flags. 60 */ 61 abstract class ViewPainter implements Runnable { 62 /** 63 * An array of initially empty ROOT_PATHS. They are created on demand as 64 * needed. Each path is associated with a different dirty region. We have 65 * up to PrismSettings.dirtyRegionCount max dirty regions 66 */ 67 private static NodePath[] ROOT_PATHS = new NodePath[PrismSettings.dirtyRegionCount]; 68 69 /* 70 * This could be a per-scene lock but there is no guarantee that the 71 * FX handlers called in GlassViewEventHandler would not modify other scenes. 72 */ 73 protected static final ReentrantLock renderLock = new ReentrantLock(); 74 75 // Pen dimensions. Pen width and height are checked on every repaint 76 // to match its scene width/height. If any difference is found, the 77 // pen surface (Presentable or RTTexture) is recreated. 78 protected int penWidth = -1; 79 protected int penHeight = -1; 80 protected int viewWidth; 81 protected int viewHeight; 82 83 protected final SceneState sceneState; 84 85 protected Presentable presentable; 86 protected ResourceFactory factory; 87 protected boolean freshBackBuffer; 88 89 private int width; 90 private int height; 91 92 /** 93 * root is the root node of the scene. overlayRoot is the root node of any 94 * overlay which may be present (such as used for full screen overlay). 95 */ 96 private NGNode root, overlayRoot; 97 98 // These variables are all used as part of the dirty region optimizations, 99 // and if dirty opts are turned off via a runtime flag, then these fields 100 // are never initialized or used. 101 private Rectangle dirtyRect; 102 private RectBounds clip; 103 private RectBounds dirtyRegionTemp; 104 private DirtyRegionPool dirtyRegionPool; 105 private DirtyRegionContainer dirtyRegionContainer; 106 private Affine3D tx; 107 private Affine3D scaleTx; 108 private GeneralTransform3D viewProjTx; 109 private GeneralTransform3D projTx; 110 111 /** 112 * This is used for drawing dirty regions and overdraw rectangles in cases where we are 113 * not drawing the entire scene every time (specifically, when depth buffer is disabled). 114 * In those cases we will draw the scene to the sceneBuffer, clear the actual back buffer, 115 * blit the sceneBuffer into the back buffer, and then scribble on top of the back buffer 116 * with the dirty regions and/or overdraw rectangles. 117 * 118 * When the depthBuffer is enabled on a scene, we always end up drawing the entire scene 119 * anyway, so we don't bother with this sceneBuffer in that case. Of course, if dirty 120 * region / overdraw rectangle drawing is turned off, then we don't use this. Thus, 121 * only when you are doing some kind of debugging would this field be used and the 122 * extra buffer copy incurred. 123 */ 124 private RTTexture sceneBuffer; 125 126 protected ViewPainter(GlassScene gs) { 127 sceneState = gs.getSceneState(); 128 if (sceneState == null) { 129 throw new NullPointerException("Scene state is null"); 130 } 131 132 if (PrismSettings.dirtyOptsEnabled) { 133 tx = new Affine3D(); 134 viewProjTx = new GeneralTransform3D(); 135 projTx = new GeneralTransform3D(); 136 scaleTx = new Affine3D(); 137 clip = new RectBounds(); 138 dirtyRect = new Rectangle(); 139 dirtyRegionTemp = new RectBounds(); 140 dirtyRegionPool = new DirtyRegionPool(PrismSettings.dirtyRegionCount); 141 dirtyRegionContainer = dirtyRegionPool.checkOut(); 142 } 143 } 144 145 protected final void setRoot(NGNode node) { 146 root = node; 147 } 148 149 protected final void setOverlayRoot(NGNode node) { 150 overlayRoot = node; 151 } 152 153 private void adjustPerspective(NGCamera camera) { 154 // This should definitely be true since this is only called by setDirtyRect 155 assert PrismSettings.dirtyOptsEnabled; 156 if (camera instanceof NGPerspectiveCamera) { 157 scaleTx.setToScale(width / 2.0, -height / 2.0, 1); 158 scaleTx.translate(1, -1); 159 projTx.mul(scaleTx); 160 viewProjTx = camera.getProjViewTx(viewProjTx); 161 projTx.mul(viewProjTx); 162 } 163 } 164 165 protected void paintImpl(final Graphics backBufferGraphics) { 166 // We should not be painting anything with a width / height 167 // that is <= 0, so we might as well bail right off. 168 if (width <= 0 || height <= 0 || backBufferGraphics == null) { 169 root.renderForcedContent(backBufferGraphics); 170 return; 171 } 172 173 // This "g" variable might represent the back buffer graphics, or it 174 // might be reassigned to the sceneBuffer graphics. 175 Graphics g = backBufferGraphics; 176 // Take into account the pixel scale factor for retina displays 177 final float pixelScaleX = getPixelScaleFactorX(); 178 final float pixelScaleY = getPixelScaleFactorY(); 179 // Cache pixelScale in Graphics for use in 3D shaders such as camera and light positions. 180 g.setPixelScaleFactors(pixelScaleX, pixelScaleY); 181 182 // Initialize renderEverything based on various conditions that will cause us to render 183 // the entire scene every time. 184 boolean renderEverything = overlayRoot != null || 185 freshBackBuffer || 186 sceneState.getScene().isEntireSceneDirty() || 187 sceneState.getScene().getDepthBuffer() || 188 !PrismSettings.dirtyOptsEnabled; 189 // We are going to draw dirty opt boxes either if we're supposed to show the dirty 190 // regions, or if we're supposed to show the overdraw boxes. 191 final boolean showDirtyOpts = PrismSettings.showDirtyRegions || PrismSettings.showOverdraw; 192 // If showDirtyOpts is turned on and we're not using a depth buffer 193 // then we will render the scene to an intermediate texture, and then at the end we'll 194 // draw that intermediate texture to the back buffer. 195 if (showDirtyOpts && !sceneState.getScene().getDepthBuffer()) { 196 final int bufferWidth = (int) Math.ceil(width * pixelScaleX); 197 final int bufferHeight = (int) Math.ceil(height * pixelScaleY); 198 // Check whether the sceneBuffer texture needs to be reconstructed 199 if (sceneBuffer != null) { 200 sceneBuffer.lock(); 201 if (sceneBuffer.isSurfaceLost() || 202 bufferWidth != sceneBuffer.getContentWidth() || 203 bufferHeight != sceneBuffer.getContentHeight()) { 204 sceneBuffer.unlock(); 205 sceneBuffer.dispose(); 206 sceneBuffer = null; 207 } 208 } 209 // If sceneBuffer is null, we need to create a new texture. In this 210 // case we will also need to render the whole scene (so don't bother 211 // with dirty opts) 212 if (sceneBuffer == null) { 213 sceneBuffer = g.getResourceFactory().createRTTexture( 214 bufferWidth, 215 bufferHeight, 216 Texture.WrapMode.CLAMP_TO_ZERO, 217 false); 218 renderEverything = true; 219 } 220 sceneBuffer.contentsUseful(); 221 // Hijack the "g" graphics variable 222 g = sceneBuffer.createGraphics(); 223 g.setPixelScaleFactors(pixelScaleX, pixelScaleY); 224 g.scale(pixelScaleX, pixelScaleY); 225 } else if (sceneBuffer != null) { 226 // We're in a situation where we have previously rendered to the sceneBuffer, but in 227 // this render pass for whatever reason we're going to draw directly to the back buffer. 228 // In this case we need to release the sceneBuffer. 229 sceneBuffer.dispose(); 230 sceneBuffer = null; 231 } 232 233 // The status will be set only if we're rendering with dirty regions 234 int status = -1; 235 236 // If we're rendering with dirty regions, then we'll call the root node to accumulate 237 // the dirty regions and then again to do the pre culling. 238 if (!renderEverything) { 239 if (PULSE_LOGGING_ENABLED) { 240 PulseLogger.newPhase("Dirty Opts Computed"); 241 } 242 clip.setBounds(0, 0, width, height); 243 dirtyRegionTemp.makeEmpty(); 244 dirtyRegionContainer.reset(); 245 tx.setToIdentity(); 246 projTx.setIdentity(); 247 adjustPerspective(sceneState.getCamera()); 248 status = root.accumulateDirtyRegions(clip, dirtyRegionTemp, 249 dirtyRegionPool, dirtyRegionContainer, 250 tx, projTx); 251 dirtyRegionContainer.roundOut(); 252 if (status == DirtyRegionContainer.DTR_OK) { 253 root.doPreCulling(dirtyRegionContainer, tx, projTx); 254 } 255 } 256 257 // We're going to need to iterate over the dirty region container a lot, so we 258 // might as well save this reference. 259 final int dirtyRegionSize = status == DirtyRegionContainer.DTR_OK ? dirtyRegionContainer.size() : 0; 260 261 if (dirtyRegionSize > 0) { 262 // We set this flag on Graphics so that subsequent code in the render paths of 263 // NGNode know whether they ought to be paying attention to dirty region 264 // culling bits. 265 g.setHasPreCullingBits(true); 266 267 // Find the render roots. There is a different render root for each dirty region 268 if (PULSE_LOGGING_ENABLED) { 269 PulseLogger.newPhase("Render Roots Discovered"); 270 } 271 for (int i = 0; i < dirtyRegionSize; ++i) { 272 NodePath path = getRootPath(i); 273 path.clear(); 274 root.getRenderRoot(getRootPath(i), dirtyRegionContainer.getDirtyRegion(i), i, tx, projTx); 275 } 276 277 // For debug purposes, write out to the pulse logger the number and size of the dirty 278 // regions that are being used to render this pulse. 279 if (PULSE_LOGGING_ENABLED) { 280 PulseLogger.addMessage(dirtyRegionSize + " different dirty regions to render"); 281 for (int i=0; i<dirtyRegionSize; i++) { 282 PulseLogger.addMessage("Dirty Region " + i + ": " + dirtyRegionContainer.getDirtyRegion(i)); 283 PulseLogger.addMessage("Render Root Path " + i + ": " + getRootPath(i)); 284 } 285 } 286 287 // If -Dprism.printrendergraph=true then we want to print out the render graph to the 288 // pulse logger, annotated with all the dirty opts. Invisible nodes are skipped. 289 if (PULSE_LOGGING_ENABLED && PrismSettings.printRenderGraph) { 290 StringBuilder s = new StringBuilder(); 291 List<NGNode> roots = new ArrayList<>(); 292 for (int i = 0; i < dirtyRegionSize; i++) { 293 final RectBounds dirtyRegion = dirtyRegionContainer.getDirtyRegion(i); 294 // TODO it should be impossible to have ever created a dirty region that was empty... 295 if (dirtyRegion.getWidth() > 0 && dirtyRegion.getHeight() > 0) { 296 NodePath nodePath = getRootPath(i); 297 if (!nodePath.isEmpty()) { 298 roots.add(nodePath.last()); 299 } 300 } 301 } 302 root.printDirtyOpts(s, roots); 303 PulseLogger.addMessage(s.toString()); 304 } 305 306 // Paint each dirty region 307 for (int i = 0; i < dirtyRegionSize; ++i) { 308 final RectBounds dirtyRegion = dirtyRegionContainer.getDirtyRegion(i); 309 // TODO it should be impossible to have ever created a dirty region that was empty... 310 // Make sure we are not trying to render in some invalid region 311 if (dirtyRegion.getWidth() > 0 && dirtyRegion.getHeight() > 0) { 312 // Set the clip rectangle using integer bounds since a fractional bounding box will 313 // still require a complete repaint on pixel boundaries 314 dirtyRect.setBounds(dirtyRegion); 315 // TODO I don't understand why this is needed. And if it is, are fractional pixelScale 316 // values OK? And if not, shouldn't pixelScale be an int instead? 317 if (pixelScaleX != 1.0f || pixelScaleY != 1.0f) { 318 dirtyRect.x *= pixelScaleX; 319 dirtyRect.y *= pixelScaleY; 320 dirtyRect.width *= pixelScaleX; 321 dirtyRect.height *= pixelScaleY; 322 } 323 g.setClipRect(dirtyRect); 324 g.setClipRectIndex(i); 325 doPaint(g, getRootPath(i)); 326 } 327 } 328 } else { 329 // There are no dirty regions, so just paint everything 330 g.setHasPreCullingBits(false); 331 g.setClipRect(null); 332 this.doPaint(g, null); 333 } 334 root.renderForcedContent(g); 335 336 // If we have an overlay then we need to render it too. 337 if (overlayRoot != null) { 338 overlayRoot.render(g); 339 } 340 341 // If we're showing dirty regions or overdraw, then we're going to need to draw 342 // over-top the normal scene. If we have been drawing do the back buffer, then we 343 // will just draw on top of it. If we have been drawing to the sceneBuffer, then 344 // we will first blit the sceneBuffer into the back buffer, and then draw directly 345 // on the back buffer. 346 if (showDirtyOpts) { 347 if (sceneBuffer != null) { 348 g.sync(); 349 backBufferGraphics.clear(); 350 backBufferGraphics.drawTexture(sceneBuffer, 0, 0, width, height, 351 sceneBuffer.getContentX(), sceneBuffer.getContentY(), 352 sceneBuffer.getContentX() + sceneBuffer.getContentWidth(), 353 sceneBuffer.getContentY() + sceneBuffer.getContentHeight()); 354 sceneBuffer.unlock(); 355 } 356 357 if (PrismSettings.showOverdraw) { 358 // We are going to show the overdraw rectangles. 359 if (dirtyRegionSize > 0) { 360 // In this case we have dirty regions, so we will iterate over them all 361 // and draw each dirty region's overdraw individually 362 for (int i = 0; i < dirtyRegionSize; i++) { 363 final Rectangle clip = new Rectangle(dirtyRegionContainer.getDirtyRegion(i)); 364 backBufferGraphics.setClipRectIndex(i); 365 paintOverdraw(backBufferGraphics, clip); 366 backBufferGraphics.setPaint(new Color(1, 0, 0, .3f)); 367 backBufferGraphics.drawRect(clip.x, clip.y, clip.width, clip.height); 368 } 369 } else { 370 // In this case there were no dirty regions, so the clip is the entire scene 371 final Rectangle clip = new Rectangle(0, 0, width, height); 372 assert backBufferGraphics.getClipRectIndex() == 0; 373 paintOverdraw(backBufferGraphics, clip); 374 backBufferGraphics.setPaint(new Color(1, 0, 0, .3f)); 375 backBufferGraphics.drawRect(clip.x, clip.y, clip.width, clip.height); 376 } 377 } else { 378 // We are going to show the dirty regions 379 if (dirtyRegionSize > 0) { 380 // We have dirty regions to draw 381 backBufferGraphics.setPaint(new Color(1, 0, 0, .3f)); 382 for (int i = 0; i < dirtyRegionSize; i++) { 383 final RectBounds reg = dirtyRegionContainer.getDirtyRegion(i); 384 backBufferGraphics.fillRect(reg.getMinX(), reg.getMinY(), reg.getWidth(), reg.getHeight()); 385 } 386 } else { 387 // No dirty regions, fill the entire view area 388 backBufferGraphics.setPaint(new Color(1, 0, 0, .3f)); 389 backBufferGraphics.fillRect(0, 0, width, height); 390 } 391 } 392 root.clearPainted(); 393 } 394 } 395 396 /** 397 * Utility method for painting the overdraw rectangles. Right now we're using a computationally 398 * intensive approach of having an array of integers (image data) that we then write to in the 399 * NGNodes, recording how many times each pixel position has been touched (well, technically, we're 400 * just recording the bounds of drawn objects, so some pixels might be "red" but actually were never 401 * drawn). 402 * 403 * @param g 404 * @param clip 405 */ 406 private void paintOverdraw(final Graphics g, final Rectangle clip) { 407 final int[] pixels = new int[clip.width * clip.height]; 408 root.drawDirtyOpts(BaseTransform.IDENTITY_TRANSFORM, projTx, clip, pixels, g.getClipRectIndex()); 409 final Image image = Image.fromIntArgbPreData(pixels, clip.width, clip.height); 410 final Texture texture = factory.getCachedTexture(image, Texture.WrapMode.CLAMP_TO_EDGE); 411 g.drawTexture(texture, clip.x, clip.y, clip.x+clip.width, clip.y+clip.height, 0, 0, clip.width, clip.height); 412 texture.unlock(); 413 } 414 415 private static NodePath getRootPath(int i) { 416 if (ROOT_PATHS[i] == null) { 417 ROOT_PATHS[i] = new NodePath(); 418 } 419 return ROOT_PATHS[i]; 420 } 421 422 protected void disposePresentable() { 423 if (presentable instanceof GraphicsResource) { 424 ((GraphicsResource)presentable).dispose(); 425 } 426 presentable = null; 427 } 428 429 protected boolean validateStageGraphics() { 430 if (!sceneState.isValid()) { 431 // indicates something happened between the scheduling of the 432 // job and the running of this job. 433 return false; 434 } 435 436 width = viewWidth = sceneState.getWidth(); 437 height = viewHeight = sceneState.getHeight(); 438 439 return sceneState.isWindowVisible() && !sceneState.isWindowMinimized(); 440 } 441 442 protected float getPixelScaleFactorX() { 443 return presentable == null ? 1.0f : presentable.getPixelScaleFactorX(); 444 } 445 446 protected float getPixelScaleFactorY() { 447 return presentable == null ? 1.0f : presentable.getPixelScaleFactorY(); 448 } 449 450 private void doPaint(Graphics g, NodePath renderRootPath) { 451 // Null path indicates that occlusion culling is not used 452 if (renderRootPath != null) { 453 if (renderRootPath.isEmpty()) { 454 // empty render path indicates that no rendering is needed. 455 // There may be occluded dirty Nodes however, so we need to clear them 456 root.clearDirtyTree(); 457 return; 458 } 459 // If the path is not empty, the first node must be the root node 460 assert(renderRootPath.getCurrentNode() == root); 461 } 462 if (PULSE_LOGGING_ENABLED) { 463 PulseLogger.newPhase("Painting"); 464 } 465 GlassScene scene = sceneState.getScene(); 466 scene.clearEntireSceneDirty(); 467 g.setLights(scene.getLights()); 468 g.setDepthBuffer(scene.getDepthBuffer()); 469 Color clearColor = sceneState.getClearColor(); 470 if (clearColor != null) { 471 g.clear(clearColor); 472 } 473 Paint curPaint = sceneState.getCurrentPaint(); 474 if (curPaint != null) { 475 if (curPaint.getType() != com.sun.prism.paint.Paint.Type.COLOR) { 476 g.getRenderTarget().setOpaque(curPaint.isOpaque()); 477 } 478 g.setPaint(curPaint); 479 g.fillQuad(0, 0, width, height); 480 } 481 g.setCamera(sceneState.getCamera()); 482 g.setRenderRoot(renderRootPath); 483 root.render(g); 484 } 485 }