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