1 /* 2 * Copyright (c) 2011, 2014, 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 pixelScale = getPixelScaleFactor(); 178 // Initialize renderEverything based on various conditions that will cause us to render 179 // the entire scene every time. 180 boolean renderEverything = overlayRoot != null || 181 freshBackBuffer || 182 sceneState.getScene().isEntireSceneDirty() || 183 sceneState.getScene().getDepthBuffer() || 184 !PrismSettings.dirtyOptsEnabled; 185 // We are going to draw dirty opt boxes either if we're supposed to show the dirty 186 // regions, or if we're supposed to show the overdraw boxes. 187 final boolean showDirtyOpts = PrismSettings.showDirtyRegions || PrismSettings.showOverdraw; 188 // If showDirtyOpts is turned on and we're not using a depth buffer 189 // then we will render the scene to an intermediate texture, and then at the end we'll 190 // draw that intermediate texture to the back buffer. 191 if (showDirtyOpts && !sceneState.getScene().getDepthBuffer()) { 192 final int bufferWidth = (int) Math.ceil(width * pixelScale); 193 final int bufferHeight = (int) Math.ceil(height * pixelScale); 194 // Check whether the sceneBuffer texture needs to be reconstructed 195 if (sceneBuffer != null) { 196 sceneBuffer.lock(); 197 if (sceneBuffer.isSurfaceLost() || 198 bufferWidth != sceneBuffer.getContentWidth() || 199 bufferHeight != sceneBuffer.getContentHeight()) { 200 sceneBuffer.unlock(); 201 sceneBuffer.dispose(); 202 sceneBuffer = null; 203 } 204 } 205 // If sceneBuffer is null, we need to create a new texture. In this 206 // case we will also need to render the whole scene (so don't bother 207 // with dirty opts) 208 if (sceneBuffer == null) { 209 sceneBuffer = g.getResourceFactory().createRTTexture( 210 bufferWidth, 211 bufferHeight, 212 Texture.WrapMode.CLAMP_TO_ZERO, 213 false); 214 renderEverything = true; 215 } 216 sceneBuffer.contentsUseful(); 217 // Hijack the "g" graphics variable 218 g = sceneBuffer.createGraphics(); 219 g.scale(pixelScale, pixelScale); 220 } else if (sceneBuffer != null) { 221 // We're in a situation where we have previously rendered to the sceneBuffer, but in 222 // this render pass for whatever reason we're going to draw directly to the back buffer. 223 // In this case we need to release the sceneBuffer. 224 sceneBuffer.dispose(); 225 sceneBuffer = null; 226 } 227 228 // The status will be set only if we're rendering with dirty regions 229 int status = -1; 230 231 // If we're rendering with dirty regions, then we'll call the root node to accumulate 232 // the dirty regions and then again to do the pre culling. 233 if (!renderEverything) { 234 if (PULSE_LOGGING_ENABLED) { 235 PulseLogger.newPhase("Dirty Opts Computed"); 236 } 237 clip.setBounds(0, 0, width, height); 238 dirtyRegionTemp.makeEmpty(); 239 dirtyRegionContainer.reset(); 240 tx.setToIdentity(); 241 projTx.setIdentity(); 242 adjustPerspective(sceneState.getCamera()); 243 status = root.accumulateDirtyRegions(clip, dirtyRegionTemp, 244 dirtyRegionPool, dirtyRegionContainer, 245 tx, projTx); 246 dirtyRegionContainer.roundOut(); 247 if (status == DirtyRegionContainer.DTR_OK) { 248 root.doPreCulling(dirtyRegionContainer, tx, projTx); 249 } 250 } 251 252 // We're going to need to iterate over the dirty region container a lot, so we 253 // might as well save this reference. 254 final int dirtyRegionSize = status == DirtyRegionContainer.DTR_OK ? dirtyRegionContainer.size() : 0; 255 256 if (dirtyRegionSize > 0) { 257 // We set this flag on Graphics so that subsequent code in the render paths of 258 // NGNode know whether they ought to be paying attention to dirty region 259 // culling bits. 260 g.setHasPreCullingBits(true); 261 262 // Find the render roots. There is a different render root for each dirty region 263 if (PULSE_LOGGING_ENABLED) { 264 PulseLogger.newPhase("Render Roots Discovered"); 265 } 266 for (int i = 0; i < dirtyRegionSize; ++i) { 267 NodePath path = getRootPath(i); 268 path.clear(); 269 root.getRenderRoot(getRootPath(i), dirtyRegionContainer.getDirtyRegion(i), i, tx, projTx); 270 } 271 272 // For debug purposes, write out to the pulse logger the number and size of the dirty 273 // regions that are being used to render this pulse. 274 if (PULSE_LOGGING_ENABLED) { 275 PulseLogger.addMessage(dirtyRegionSize + " different dirty regions to render"); 276 for (int i=0; i<dirtyRegionSize; i++) { 277 PulseLogger.addMessage("Dirty Region " + i + ": " + dirtyRegionContainer.getDirtyRegion(i)); 278 PulseLogger.addMessage("Render Root Path " + i + ": " + getRootPath(i)); 279 } 280 } 281 282 // If -Dprism.printrendergraph=true then we want to print out the render graph to the 283 // pulse logger, annotated with all the dirty opts. Invisible nodes are skipped. 284 if (PULSE_LOGGING_ENABLED && PrismSettings.printRenderGraph) { 285 StringBuilder s = new StringBuilder(); 286 List<NGNode> roots = new ArrayList<>(); 287 for (int i = 0; i < dirtyRegionSize; i++) { 288 final RectBounds dirtyRegion = dirtyRegionContainer.getDirtyRegion(i); 289 // TODO it should be impossible to have ever created a dirty region that was empty... 290 if (dirtyRegion.getWidth() > 0 && dirtyRegion.getHeight() > 0) { 291 NodePath nodePath = getRootPath(i); 292 if (!nodePath.isEmpty()) { 293 roots.add(nodePath.last()); 294 } 295 } 296 } 297 root.printDirtyOpts(s, roots); 298 PulseLogger.addMessage(s.toString()); 299 } 300 301 // Paint each dirty region 302 for (int i = 0; i < dirtyRegionSize; ++i) { 303 final RectBounds dirtyRegion = dirtyRegionContainer.getDirtyRegion(i); 304 // TODO it should be impossible to have ever created a dirty region that was empty... 305 // Make sure we are not trying to render in some invalid region 306 if (dirtyRegion.getWidth() > 0 && dirtyRegion.getHeight() > 0) { 307 // Set the clip rectangle using integer bounds since a fractional bounding box will 308 // still require a complete repaint on pixel boundaries 309 dirtyRect.setBounds(dirtyRegion); 310 // TODO I don't understand why this is needed. And if it is, are fractional pixelScale 311 // values OK? And if not, shouldn't pixelScale be an int instead? 312 if (pixelScale != 1.0f) { 313 dirtyRect.x *= pixelScale; 314 dirtyRect.y *= pixelScale; 315 dirtyRect.width *= pixelScale; 316 dirtyRect.height *= pixelScale; 317 } 318 g.setClipRect(dirtyRect); 319 g.setClipRectIndex(i); 320 doPaint(g, getRootPath(i)); 321 } 322 } 323 } else { 324 // There are no dirty regions, so just paint everything 325 g.setHasPreCullingBits(false); 326 g.setClipRect(null); 327 this.doPaint(g, null); 328 } 329 root.renderForcedContent(g); 330 331 // If we have an overlay then we need to render it too. 332 if (overlayRoot != null) { 333 overlayRoot.render(g); 334 } 335 336 // If we're showing dirty regions or overdraw, then we're going to need to draw 337 // over-top the normal scene. If we have been drawing do the back buffer, then we 338 // will just draw on top of it. If we have been drawing to the sceneBuffer, then 339 // we will first blit the sceneBuffer into the back buffer, and then draw directly 340 // on the back buffer. 341 if (showDirtyOpts) { 342 if (sceneBuffer != null) { 343 g.sync(); 344 backBufferGraphics.clear(); 345 backBufferGraphics.drawTexture(sceneBuffer, 0, 0, width, height, 346 sceneBuffer.getContentX(), sceneBuffer.getContentY(), 347 sceneBuffer.getContentX() + sceneBuffer.getContentWidth(), 348 sceneBuffer.getContentY() + sceneBuffer.getContentHeight()); 349 sceneBuffer.unlock(); 350 } 351 352 if (PrismSettings.showOverdraw) { 353 // We are going to show the overdraw rectangles. 354 if (dirtyRegionSize > 0) { 355 // In this case we have dirty regions, so we will iterate over them all 356 // and draw each dirty region's overdraw individually 357 for (int i = 0; i < dirtyRegionSize; i++) { 358 final Rectangle clip = new Rectangle(dirtyRegionContainer.getDirtyRegion(i)); 359 backBufferGraphics.setClipRectIndex(i); 360 paintOverdraw(backBufferGraphics, clip); 361 backBufferGraphics.setPaint(new Color(1, 0, 0, .3f)); 362 backBufferGraphics.drawRect(clip.x, clip.y, clip.width, clip.height); 363 } 364 } else { 365 // In this case there were no dirty regions, so the clip is the entire scene 366 final Rectangle clip = new Rectangle(0, 0, width, height); 367 assert backBufferGraphics.getClipRectIndex() == 0; 368 paintOverdraw(backBufferGraphics, clip); 369 backBufferGraphics.setPaint(new Color(1, 0, 0, .3f)); 370 backBufferGraphics.drawRect(clip.x, clip.y, clip.width, clip.height); 371 } 372 } else { 373 // We are going to show the dirty regions 374 if (dirtyRegionSize > 0) { 375 // We have dirty regions to draw 376 backBufferGraphics.setPaint(new Color(1, 0, 0, .3f)); 377 for (int i = 0; i < dirtyRegionSize; i++) { 378 final RectBounds reg = dirtyRegionContainer.getDirtyRegion(i); 379 backBufferGraphics.fillRect(reg.getMinX(), reg.getMinY(), reg.getWidth(), reg.getHeight()); 380 } 381 } else { 382 // No dirty regions, fill the entire view area 383 backBufferGraphics.setPaint(new Color(1, 0, 0, .3f)); 384 backBufferGraphics.fillRect(0, 0, width, height); 385 } 386 } 387 root.clearPainted(); 388 } 389 } 390 391 /** 392 * Utility method for painting the overdraw rectangles. Right now we're using a computationally 393 * intensive approach of having an array of integers (image data) that we then write to in the 394 * NGNodes, recording how many times each pixel position has been touched (well, technically, we're 395 * just recording the bounds of drawn objects, so some pixels might be "red" but actually were never 396 * drawn). 397 * 398 * @param g 399 * @param clip 400 */ 401 private void paintOverdraw(final Graphics g, final Rectangle clip) { 402 final int[] pixels = new int[clip.width * clip.height]; 403 root.drawDirtyOpts(BaseTransform.IDENTITY_TRANSFORM, projTx, clip, pixels, g.getClipRectIndex()); 404 final Image image = Image.fromIntArgbPreData(pixels, clip.width, clip.height); 405 final Texture texture = factory.getCachedTexture(image, Texture.WrapMode.CLAMP_TO_EDGE); 406 g.drawTexture(texture, clip.x, clip.y, clip.x+clip.width, clip.y+clip.height, 0, 0, clip.width, clip.height); 407 texture.unlock(); 408 } 409 410 private static NodePath getRootPath(int i) { 411 if (ROOT_PATHS[i] == null) { 412 ROOT_PATHS[i] = new NodePath(); 413 } 414 return ROOT_PATHS[i]; 415 } 416 417 protected void disposePresentable() { 418 if (presentable instanceof GraphicsResource) { 419 ((GraphicsResource)presentable).dispose(); 420 } 421 presentable = null; 422 } 423 424 protected boolean validateStageGraphics() { 425 if (!sceneState.isValid()) { 426 // indicates something happened between the scheduling of the 427 // job and the running of this job. 428 return false; 429 } 430 431 width = viewWidth = sceneState.getWidth(); 432 height = viewHeight = sceneState.getHeight(); 433 434 return sceneState.isWindowVisible() && !sceneState.isWindowMinimized(); 435 } 436 437 protected float getPixelScaleFactor() { 438 return presentable == null ? 1.0f : presentable.getPixelScaleFactor(); 439 } 440 441 private void doPaint(Graphics g, NodePath renderRootPath) { 442 // Null path indicates that occlusion culling is not used 443 if (renderRootPath != null) { 444 if (renderRootPath.isEmpty()) { 445 // empty render path indicates that no rendering is needed. 446 // There may be occluded dirty Nodes however, so we need to clear them 447 root.clearDirtyTree(); 448 return; 449 } 450 // If the path is not empty, the first node must be the root node 451 assert(renderRootPath.getCurrentNode() == root); 452 } 453 if (PULSE_LOGGING_ENABLED) { 454 PulseLogger.newPhase("Painting"); 455 } 456 GlassScene scene = sceneState.getScene(); 457 scene.clearEntireSceneDirty(); 458 g.setLights(scene.getLights()); 459 g.setDepthBuffer(scene.getDepthBuffer()); 460 Color clearColor = sceneState.getClearColor(); 461 if (clearColor != null) { 462 g.clear(clearColor); 463 } 464 Paint curPaint = sceneState.getCurrentPaint(); 465 if (curPaint != null) { 466 if (curPaint.getType() != com.sun.prism.paint.Paint.Type.COLOR) { 467 g.getRenderTarget().setOpaque(curPaint.isOpaque()); 468 } 469 g.setPaint(curPaint); 470 g.fillQuad(0, 0, width, height); 471 } 472 g.setCamera(sceneState.getCamera()); 473 g.setRenderRoot(renderRootPath); 474 root.render(g); 475 } 476 }