1 /*
   2  * Copyright (c) 2010, 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.Collections;
  30 import java.util.Comparator;
  31 import java.util.List;
  32 import java.util.concurrent.CountDownLatch;
  33 import com.sun.javafx.PlatformUtil;
  34 
  35 import com.sun.glass.ui.Application;
  36 import com.sun.glass.ui.Window;
  37 import com.sun.javafx.tk.CompletionListener;
  38 import com.sun.javafx.tk.RenderJob;
  39 
  40 import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED;
  41 import com.sun.javafx.logging.PulseLogger;
  42 
  43 /**
  44  * Manages the collection and rendering of dirty scenes. This class has
  45  * methods which may be called from one of several threads, depending
  46  * on the method.
  47  *
  48  * <ul>
  49  *     <li>createInstance: Called by QuantumToolkit once during initialization</li>
  50  *     <li>getInstance: May be called from any thread</li>
  51  *     <li>hasDirty: May be called from any thread</li>
  52  *     <li>addDirtyScene: Called only on the FX Thread</li>
  53  *     <li>removeDirtyScene: Called only on the FX Thread</li>
  54  *     <li>getRendered: May be called from any thread</li>
  55  *     <li>liveRepaintRenderJob: Called only on the FX Thread</li>
  56  *     <li>renderAll: Called only on the FX Thread</li>
  57  * </ul>
  58  *
  59  * Assertions have been added to each method to verify whether the calling
  60  * thread is the expected thread.
  61  */
  62 final class PaintCollector implements CompletionListener {
  63     /*
  64         Generally we would prefer to remove this static state and pass the
  65         collector where it needs to go rather than having code reach into this
  66         static method to get the instance. IoC (inversion of control) makes
  67         the code more readable and testable, in general.
  68     */
  69     private static volatile PaintCollector collector;
  70 
  71     static PaintCollector createInstance(QuantumToolkit toolkit) {
  72         return collector = new PaintCollector(toolkit);
  73     }
  74 
  75     static PaintCollector getInstance() {
  76         return collector;
  77     }
  78 
  79     /**
  80      * Sorts the dirty scenes such that asynchronous scenes come first
  81      */
  82     private static final Comparator<GlassScene> DIRTY_SCENE_SORTER = (o1, o2) -> {
  83         int i1 = o1.isSynchronous() ? 1 : 0;
  84         int i2 = o2.isSynchronous() ? 1 : 0;
  85         return i1 - i2;
  86     };
  87 
  88     /**
  89      * Contains a list of all of the dirty scenes. This list is populated
  90      * only from the FX Thread in consequence of a call to addDirtyScene,
  91      * or cleared from the FX Thread in consequence of a call to renderAll
  92      * or removeDirtyScene. It is only ever accessed (both read and write!)
  93      * from the FX thread.
  94      */
  95     private final List<GlassScene> dirtyScenes = new ArrayList<>();
  96 
  97     /**
  98      * Keeps track of the number of scenes which still need to be processed.
  99      * In the renderAll method, we will await on this latch until all currently
 100      * pending scenes are completed. Once they are all completed, we will
 101      * create a new CountDownLatch initialized to the size of the number of
 102      * scenes to be processed, and then process each scene in turn (or rather,
 103      * cause them to render on the render thread). As each scene completes,
 104      * the CompletionListener will be invoked which will decrement the
 105      * allWorkCompletedLatch.
 106      */
 107     private volatile CountDownLatch allWorkCompletedLatch = new CountDownLatch(0);
 108 
 109     /**
 110      * Indicates whether this PaintCollector has any dirty scenes that
 111      * need to be processed. This is used by the QuantumToolkit to detect
 112      * in the postPulse() method whether there are dirty scenes. If there
 113      * are, then the postPulse will potentially post a new pulse event.
 114      * Updated from the FX Thread, but may be read from any thread.
 115      */
 116     private volatile boolean hasDirty;
 117 
 118     /**
 119      * A reference to the toolkit. This is supplied in the constructor.
 120      * Although a Toolkit.getToolkit() call and cast to QuantumToolkit
 121      * could be used, it is somewhat cleaner to simply supply these
 122      * parameters in the constructor and not reach out to static state.
 123      */
 124     private final QuantumToolkit toolkit;
 125 
 126     /**
 127      * Indicates whether we should attempt to wait for vsync at
 128      * the conclusion of rendering all scenes. This is set in the
 129      * renderAll method if there are any synchronous scenes. If true,
 130      * then after the last scene is processed we will indicate to the
 131      * Toolkit that it should exercise the vsync block, and let it
 132      * decide whether to actually do so or not (based on flags, or
 133      * what OS we're on, etc).
 134      *
 135      * <p>This field will be set from the FX thread and read from
 136      * the Render thread, hence it is volatile.</p>
 137      */
 138     private volatile boolean needsHint;
 139 
 140     /**
 141      * Singleton constructor.
 142      *
 143      * @param qt The QuantumToolkit instance.
 144      */
 145     private PaintCollector(QuantumToolkit qt) {
 146         toolkit  = qt;
 147     }
 148 
 149     /**
 150      * Called by renderAll to wait for rendering to complete before
 151      * continuing.
 152      */
 153     void waitForRenderingToComplete() {
 154         while (true) {
 155             try {
 156                 // We need to keep waiting until things are done!
 157                 allWorkCompletedLatch.await();
 158                 return;
 159             } catch (InterruptedException ex) {
 160                 // An interrupted exception at this point is a
 161                 // bad thing. It might have happened during shutdown,
 162                 // perhaps? Or somebody is poking the FX thread and
 163                 // asking it to interrupt. Either way, it means
 164                 // that we have not yet completed rendering some
 165                 // scenes and we're about to make a mess of things.
 166                 // Best thing to do is to retry.
 167             }
 168         }
 169     }
 170 
 171     /**
 172      * Gets whether there are any dirty scenes that need to be rendered. If
 173      * true, then a subsequent pulse event and renderAll call is required.
 174      *
 175      * @return Whether there are any dirty scenes that need to be rendered.
 176      */
 177     final boolean hasDirty() {
 178         return hasDirty;
 179     }
 180 
 181     /**
 182      * Adds a dirty scene to the PaintCollector for subsequent processing.
 183      * This method simply makes the PaintCollector aware of this new
 184      * scene and ensure it gets processed on the next call to renderAll.
 185      *
 186      * The next QuantumToolkit Glass timer generated pulse or PaintCollector
 187      * rendering vsync hinted pulse will process these dirty scenes.
 188      *
 189      * <p>This method must only be called on the FX Thread</p>
 190      * 
 191      * @param scene    The scene which is dirty. This must not be null.
 192      */
 193     final void addDirtyScene(GlassScene scene) {
 194         // Check that we are on the expected thread.
 195         assert Thread.currentThread() == QuantumToolkit.getFxUserThread();
 196         // Scene must not be null (using assert for performance)
 197         assert scene != null;
 198 
 199         if (QuantumToolkit.verbose) {
 200             System.err.println("PC.addDirtyScene: " + System.nanoTime() + scene);
 201         }
 202 
 203         // Because dirtyScenes is ever only accessed from the FX Thread,
 204         // we don't need any form of concurrent access here. Note also
 205         // that doing a contains() call here is probably faster than using
 206         // a HashSet because we are dealing with such a small number of
 207         // scenes that simple iteration is likely to be much faster
 208         if (!dirtyScenes.contains(scene)) {
 209             dirtyScenes.add(scene);
 210             // Now that we know we have added a scene to dirtyScenes,
 211             // we should ensure hasDirty is true.
 212             hasDirty = true;
 213         }
 214     }
 215 
 216     /**
 217      * Removes a scene from the dirtyScene list. If the given scene
 218      * was previously added with a call to addDirtyScene, it will
 219      * be removed. Potentially this means that after this call the
 220      * PaintCollector will no longer have any dirty scenes and will
 221      * no longer require a repaint.
 222      * 
 223      * <p>This method is typically called when a scene is removed
 224      * from a stage, or when visible becomes false.
 225      * </p>
 226      *
 227      * <p>This method must only be called on the FX Thread</p>
 228      *
 229      * @param scene    The scene which is no longer dirty. Must not be null.
 230      */
 231     final void removeDirtyScene(GlassScene scene) {
 232         // Ensure we're called only from the FX thread
 233         assert Thread.currentThread() == QuantumToolkit.getFxUserThread();
 234         assert scene != null;
 235 
 236         // Need to convert to use JavaFX Logging instead.
 237         if (QuantumToolkit.verbose) {
 238             System.err.println("PC.removeDirtyScene: " + scene);
 239         }
 240 
 241         // Remove the scene
 242         dirtyScenes.remove(scene);
 243         // Update hasDirty
 244         hasDirty = !dirtyScenes.isEmpty();
 245     }
 246 
 247     /**
 248      * Gets the CompletionListener which must be notified when a
 249      * GlassScene has completed rendering.
 250      *
 251      * @return The CompletionListener. Will never be null.
 252      */
 253     final CompletionListener getRendered() {
 254         return this;
 255     }
 256 
 257     /**
 258      * This object is a CompletionListener is registered with every GlassScene,
 259      * such that when the repaint has completed, this method is called.
 260      * This method will decrement the count on the allWorkCompletedLatch.
 261      */
 262     @Override public void done(RenderJob job) {
 263         // It would be better to have an assertive check that
 264         // this call is being made on the render thread, rather
 265         // than on the FXT, but this is easier for now.
 266         assert Thread.currentThread() != QuantumToolkit.getFxUserThread();
 267 
 268         if (!(job instanceof PaintRenderJob)) {
 269             throw new IllegalArgumentException("PaintCollector: invalid RenderJob");
 270         }
 271 
 272         final PaintRenderJob paintjob = (PaintRenderJob)job;
 273         final GlassScene scene = paintjob.getScene();
 274 
 275         if (scene == null) {
 276             throw new IllegalArgumentException("PaintCollector: null scene");
 277         }
 278 
 279         // This callback on Scene only exists to allow the performance
 280         // counter to be notified when a scene has been rendered. We
 281         // could reduce the class count and indirection if we had a more
 282         // direct method for notifying some performance tracker rather
 283         // than going through this round-about way.
 284         scene.frameRendered();
 285 
 286         // Work to be done after all rendering is completed. Note that
 287         // I check against "1" to indicate all rendering is done, and
 288         // only decrement the allWorkCompletedLatch after wards. This is
 289         // because as soon as I decrement the allWorkCompletedLatch to 0,
 290         // then whatever code remains in this method will run concurrently
 291         // with the FX app thread, and I'd prefer to minimize the number
 292         // of things here that could be happening in parallel.
 293         if (allWorkCompletedLatch.getCount() == 1) {
 294             // In some cases we need to tell the toolkit that
 295             // now would be a great time to vsync! 
 296             if (needsHint && !toolkit.hasNativeSystemVsync()) {
 297                 toolkit.vsyncHint();
 298             }
 299 
 300             Application.GetApplication().notifyRenderingFinished();
 301 
 302             // If pulse logging is enabled, then we must call renderEnd now
 303             // that we know that all of the scene's being rendered are finished
 304             if (PULSE_LOGGING_ENABLED) {
 305                 PulseLogger.renderEnd();
 306             }
 307         }
 308 
 309         // Count down the latch, indicating that drawing has
 310         // completed for some scene.
 311         allWorkCompletedLatch.countDown();
 312     }
 313 
 314     /**
 315      * Run a full pulse and repaint before returning.
 316      */
 317     final void liveRepaintRenderJob(final ViewScene scene) {
 318          ViewPainter viewPainter = scene.getPainter();
 319          QuantumToolkit quantum = (QuantumToolkit)QuantumToolkit.getToolkit();
 320          quantum.pulse(false);
 321          final CountDownLatch latch = new CountDownLatch(1);
 322          QuantumToolkit.runWithoutRenderLock(() -> {
 323              quantum.addRenderJob(new RenderJob(viewPainter, rj -> latch.countDown()));
 324              try {
 325                  latch.await();
 326              } catch (InterruptedException e) {
 327                  //Fail silently.  If interrupted, then proceed with the UI ...
 328              }
 329              return null;
 330          });
 331      }
 332 
 333     /**
 334      * Called by QuantumToolkit during a pulse to render whatever dirty scenes
 335      * we have. This method is only called on the FX thread.
 336      */
 337     final void renderAll() {
 338         // Ensure we're called only from the FX thread
 339         assert Thread.currentThread() == QuantumToolkit.getFxUserThread();
 340 
 341         // TODO switch to using a logger
 342         if (QuantumToolkit.pulseDebug) {
 343             System.err.println("PC.renderAll(" + dirtyScenes.size() + "): " + System.nanoTime());
 344         }
 345 
 346         // Since hasDirty can only be set to true from the FX Thread,
 347         // we can do just a simple boolean check here. If we don't
 348         // have any dirty scenes to process, then we are done.
 349         if (!hasDirty) {
 350             return;
 351         }
 352 
 353         // Because hasDirty is tied to dirtyScenes, it should
 354         // not be possible that we reach this point if dirtyScenes
 355         // is empty (since hasDirty was true)
 356         assert !dirtyScenes.isEmpty();
 357 
 358         // Sort the dirty scenes based on whether they are
 359         // synchronous or not. If they are not synchronous,
 360         // then we want to process them first.
 361         Collections.sort(dirtyScenes, DIRTY_SCENE_SORTER);
 362 
 363         // Reset the fields
 364         hasDirty = false;
 365         needsHint = false;
 366 
 367         // If pulse logging is enabled, then we must call renderStart
 368         // BEFORE we actually call repaint on any of the dirty scenes.
 369         if (PULSE_LOGGING_ENABLED) {
 370             PulseLogger.renderStart();
 371         }
 372 
 373         // This part needs to be handled a bit differently depending on whether our platform has a native
 374         // window manager or not.
 375         // So, check to see if we do (Note: how we determine this need to be improved, this should
 376         // eventually call down into platform-specific glass code and not rely on
 377         // a system property, but we will use this for now)
 378         if (!Application.GetApplication().hasWindowManager()) {
 379             // No native window manager.  We call repaint on every scene (to make sure it gets recopied
 380             // to the screen) but we may be able to skip some steps in the repaint.
 381 
 382             // Obtain a z-ordered window list from glass.  For platforms without a native window manager,
 383             // we need to recopy the all of the window contents to the screen on every frame.
 384             final List<com.sun.glass.ui.Window> glassWindowList = com.sun.glass.ui.Window.getWindows();
 385             allWorkCompletedLatch = new CountDownLatch(glassWindowList.size());
 386             for (int i = 0, n = glassWindowList.size(); i < n; i++) {
 387                 final Window w = glassWindowList.get(i);
 388                 final WindowStage ws = WindowStage.findWindowStage(w);
 389                 if (ws != null) {
 390                     final ViewScene vs = ws.getViewScene();
 391 
 392                     // Check to see if this scene is in our dirty list.  If so, we will need to render
 393                     // the scene before we recopy it to the screen.  If not, we can skip this step.
 394                     if (dirtyScenes.indexOf(vs) != -1) {
 395                         if (!needsHint) {
 396                             needsHint = vs.isSynchronous();
 397                         }
 398                     }
 399                     if (!PlatformUtil.useEGL() || i == (n - 1)) {
 400                         // for platforms without a native window manager, we only want to do the
 401                         // swap to the screen after the last window has been rendered
 402                         vs.setDoPresent(true);
 403                     } else {
 404                         vs.setDoPresent(false);
 405                     }
 406                     try {
 407                         vs.repaint();
 408                     } catch (Throwable t) {
 409                         t.printStackTrace();
 410                     }
 411                 }
 412             }
 413         } else {
 414             // We have a native window manager.  Only call repaint on the dirty scenes,
 415             // and swap to the screen on a per-window basis.
 416             //
 417             // Now we are ready to repaint each scene. We will first process
 418             // the uploadScenes, followed by the syncScenes. The reason we
 419             // want to do this is that when the last syncScene is processed,
 420             // if needsHint is true, then we will wait for vsync. We clearly
 421             // don't want to do this until all the dirty scenes have been
 422             // processed.
 423             allWorkCompletedLatch = new CountDownLatch(dirtyScenes.size());
 424 
 425             for (final GlassScene gs : dirtyScenes) {
 426                 // Only post the vsync hint if there are synchronous scenes
 427                 if (!needsHint) {
 428                     needsHint = gs.isSynchronous();
 429                 }
 430                 // On platforms with a window manager, we always set doPresent = true, because
 431                 // we always need to rerender the scene  if it's in the dirty list and we do a
 432                 // swap on a per-window basis
 433                 gs.setDoPresent(true);
 434                 try {
 435                     gs.repaint();
 436                 } catch (Throwable t) {
 437                     t.printStackTrace();
 438                 }
 439             }
 440         }
 441 
 442         dirtyScenes.clear();
 443 
 444         if (toolkit.shouldWaitForRenderingToComplete()) {
 445             waitForRenderingToComplete();
 446         }
 447     }
 448 }