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 }