|
36 | 36 | import java.util.Map; |
37 | 37 | import java.util.HashMap; |
38 | 38 | import java.util.WeakHashMap; |
| 39 | +import java.util.concurrent.ArrayBlockingQueue; |
| 40 | +import java.util.concurrent.BlockingQueue; |
| 41 | +import java.util.concurrent.ExecutorService; |
| 42 | +import java.util.concurrent.Executors; |
| 43 | +import java.util.concurrent.RejectedExecutionException; |
| 44 | +import java.util.concurrent.TimeUnit; |
39 | 45 |
|
40 | 46 | import processing.opengl.PGL; |
41 | 47 | import processing.opengl.PShader; |
@@ -796,6 +802,10 @@ public void setSize(int w, int h) { // ignore |
796 | 802 | * endRaw(), in order to shut things off. |
797 | 803 | */ |
798 | 804 | public void dispose() { // ignore |
| 805 | + if (primaryGraphics && asyncImageSaver != null) { |
| 806 | + asyncImageSaver.dispose(); |
| 807 | + asyncImageSaver = null; |
| 808 | + } |
799 | 809 | } |
800 | 810 |
|
801 | 811 |
|
@@ -1112,20 +1122,25 @@ protected void reapplySettings() { |
1112 | 1122 | * hint(DISABLE_OPENGL_ERROR_REPORT) - Speeds up the P3D renderer setting |
1113 | 1123 | * by not checking for errors while running. Undo with hint(ENABLE_OPENGL_ERROR_REPORT). |
1114 | 1124 | * <br/> <br/> |
1115 | | - * hint(ENABLE_DEPTH_READING) - Depth and stencil buffers in P2D/P3D will be |
| 1125 | + * hint(ENABLE_BUFFER_READING) - Depth and stencil buffers in P2D/P3D will be |
1116 | 1126 | * downsampled to make PGL#readPixels work with multisampling. Enabling this |
1117 | 1127 | * introduces some overhead, so if you experience bad performance, disable |
1118 | 1128 | * multisampling with noSmooth() instead. This hint is not intended to be |
1119 | 1129 | * enabled and disabled repeatedely, so call this once in setup() or after |
1120 | 1130 | * creating your PGraphics2D/3D. You can restore the default with |
1121 | | - * hint(DISABLE_DEPTH_READING) if you don't plan to read depth from |
| 1131 | + * hint(DISABLE_BUFFER_READING) if you don't plan to read depth from |
1122 | 1132 | * this PGraphics anymore. |
1123 | 1133 | * <br/> <br/> |
1124 | | - * hint(ENABLE_KEY_AUTO_REPEAT) - Auto-repeating key events are discarded |
| 1134 | + * hint(ENABLE_KEY_REPEAT) - Auto-repeating key events are discarded |
1125 | 1135 | * by default (works only in P2D/P3D); use this hint to get all the key events |
1126 | | - * (including auto-repeated). Call hint(DISABLE_KEY_AUTO_REPEAT) to get events |
| 1136 | + * (including auto-repeated). Call hint(DISABLE_KEY_REPEAT) to get events |
1127 | 1137 | * only when the key goes physically up or down. |
1128 | 1138 | * <br/> <br/> |
| 1139 | + * hint(DISABLE_ASYNC_SAVEFRAME) - P2D/P3D only - save() and saveFrame() |
| 1140 | + * will not use separate threads for saving and will block until the image |
| 1141 | + * is written to the drive. This was the default behavior in 3.0b7 and before. |
| 1142 | + * To enable, call hint(ENABLE_ASYNC_SAVEFRAME). |
| 1143 | + * <br/> <br/> |
1129 | 1144 | * As of release 0149, unhint() has been removed in favor of adding |
1130 | 1145 | * additional ENABLE/DISABLE constants to reset the default behavior. This |
1131 | 1146 | * prevents the double negatives, and also reinforces which hints can be |
@@ -8226,4 +8241,158 @@ public boolean isGL() { // ignore |
8226 | 8241 | public boolean is2X() { |
8227 | 8242 | return pixelDensity == 2; |
8228 | 8243 | } |
| 8244 | + |
| 8245 | + |
| 8246 | + ////////////////////////////////////////////////////////////// |
| 8247 | + |
| 8248 | + // ASYNC IMAGE SAVING |
| 8249 | + |
| 8250 | + |
| 8251 | + @Override |
| 8252 | + public boolean save(String filename) { |
| 8253 | + |
| 8254 | + if (hints[DISABLE_ASYNC_SAVEFRAME]) { |
| 8255 | + return super.save(filename); |
| 8256 | + } |
| 8257 | + |
| 8258 | + if (asyncImageSaver == null) { |
| 8259 | + asyncImageSaver = new AsyncImageSaver(); |
| 8260 | + } |
| 8261 | + |
| 8262 | + if (!loaded) loadPixels(); |
| 8263 | + PImage target = asyncImageSaver.getAvailableTarget(pixelWidth, pixelHeight, |
| 8264 | + format); |
| 8265 | + if (target == null) return false; |
| 8266 | + int count = PApplet.min(pixels.length, target.pixels.length); |
| 8267 | + System.arraycopy(pixels, 0, target.pixels, 0, count); |
| 8268 | + asyncImageSaver.saveTargetAsync(this, target, filename); |
| 8269 | + |
| 8270 | + return true; |
| 8271 | + } |
| 8272 | + |
| 8273 | + protected void processImageBeforeAsyncSave(PImage image) { } |
| 8274 | + |
| 8275 | + |
| 8276 | + protected static AsyncImageSaver asyncImageSaver; |
| 8277 | + |
| 8278 | + protected static class AsyncImageSaver { |
| 8279 | + |
| 8280 | + static final int TARGET_COUNT = |
| 8281 | + Math.max(1, Runtime.getRuntime().availableProcessors() - 1); |
| 8282 | + |
| 8283 | + BlockingQueue<PImage> targetPool = new ArrayBlockingQueue<>(TARGET_COUNT); |
| 8284 | + ExecutorService saveExecutor = Executors.newFixedThreadPool(TARGET_COUNT); |
| 8285 | + |
| 8286 | + int targetsCreated = 0; |
| 8287 | + |
| 8288 | + |
| 8289 | + static final int TIME_AVG_FACTOR = 32; |
| 8290 | + |
| 8291 | + volatile long avgNanos = 0; |
| 8292 | + long lastTime = 0; |
| 8293 | + int lastFrameCount = 0; |
| 8294 | + |
| 8295 | + |
| 8296 | + public AsyncImageSaver() { } |
| 8297 | + |
| 8298 | + |
| 8299 | + public void dispose() { |
| 8300 | + saveExecutor.shutdown(); |
| 8301 | + try { |
| 8302 | + saveExecutor.awaitTermination(5000, TimeUnit.SECONDS); |
| 8303 | + } catch (InterruptedException e) { } |
| 8304 | + } |
| 8305 | + |
| 8306 | + |
| 8307 | + public boolean hasAvailableTarget() { |
| 8308 | + return targetsCreated < TARGET_COUNT || targetPool.isEmpty(); |
| 8309 | + } |
| 8310 | + |
| 8311 | + |
| 8312 | + /** |
| 8313 | + * After taking a target, you must call saveTargetAsync() or |
| 8314 | + * returnUnusedTarget(), otherwise one thread won't be able to run |
| 8315 | + */ |
| 8316 | + public PImage getAvailableTarget(int requestedWidth, int requestedHeight, |
| 8317 | + int format) { |
| 8318 | + try { |
| 8319 | + PImage target; |
| 8320 | + if (targetsCreated < TARGET_COUNT && targetPool.isEmpty()) { |
| 8321 | + target = new PImage(requestedWidth, requestedHeight); |
| 8322 | + targetsCreated++; |
| 8323 | + } else { |
| 8324 | + target = targetPool.take(); |
| 8325 | + if (target.width != requestedWidth || |
| 8326 | + target.height != requestedHeight) { |
| 8327 | + target.width = requestedWidth; |
| 8328 | + target.height = requestedHeight; |
| 8329 | + // TODO: this kills performance when saving different sizes |
| 8330 | + target.pixels = new int[requestedWidth * requestedHeight]; |
| 8331 | + } |
| 8332 | + } |
| 8333 | + target.format = format; |
| 8334 | + return target; |
| 8335 | + } catch (InterruptedException e) { |
| 8336 | + return null; |
| 8337 | + } |
| 8338 | + } |
| 8339 | + |
| 8340 | + |
| 8341 | + public void returnUnusedTarget(PImage target) { |
| 8342 | + targetPool.offer(target); |
| 8343 | + } |
| 8344 | + |
| 8345 | + |
| 8346 | + public void saveTargetAsync(final PGraphics renderer, final PImage target, |
| 8347 | + final String filename) { |
| 8348 | + target.parent = renderer.parent; |
| 8349 | + |
| 8350 | + // if running every frame, smooth the framerate |
| 8351 | + if (target.parent.frameCount - 1 == lastFrameCount && TARGET_COUNT > 1) { |
| 8352 | + |
| 8353 | + // count with one less thread to reduce jitter |
| 8354 | + // 2 cores - 1 save thread - no wait |
| 8355 | + // 4 cores - 3 save threads - wait 1/2 of save time |
| 8356 | + // 8 cores - 7 save threads - wait 1/6 of save time |
| 8357 | + long avgTimePerFrame = avgNanos / (Math.max(1, TARGET_COUNT - 1)); |
| 8358 | + long now = System.nanoTime(); |
| 8359 | + long delay = PApplet.round((lastTime + avgTimePerFrame - now) / 1e6f); |
| 8360 | + try { |
| 8361 | + if (delay > 0) Thread.sleep(delay); |
| 8362 | + } catch (InterruptedException e) { } |
| 8363 | + } |
| 8364 | + |
| 8365 | + lastFrameCount = target.parent.frameCount; |
| 8366 | + lastTime = System.nanoTime(); |
| 8367 | + |
| 8368 | + try { |
| 8369 | + saveExecutor.submit(new Runnable() { |
| 8370 | + @Override |
| 8371 | + public void run() { |
| 8372 | + try { |
| 8373 | + long startTime = System.nanoTime(); |
| 8374 | + renderer.processImageBeforeAsyncSave(target); |
| 8375 | + target.save(filename); |
| 8376 | + long saveNanos = System.nanoTime() - startTime; |
| 8377 | + synchronized (AsyncImageSaver.this) { |
| 8378 | + if (avgNanos == 0) { |
| 8379 | + avgNanos = saveNanos; |
| 8380 | + } else if (saveNanos < avgNanos) { |
| 8381 | + avgNanos = (avgNanos * (TIME_AVG_FACTOR - 1) + saveNanos) / |
| 8382 | + (TIME_AVG_FACTOR); |
| 8383 | + } else { |
| 8384 | + avgNanos = saveNanos; |
| 8385 | + } |
| 8386 | + } |
| 8387 | + } finally { |
| 8388 | + targetPool.offer(target); |
| 8389 | + } |
| 8390 | + } |
| 8391 | + }); |
| 8392 | + } catch (RejectedExecutionException e) { |
| 8393 | + // the executor service was probably shut down, no more saving for us |
| 8394 | + } |
| 8395 | + } |
| 8396 | + } |
| 8397 | + |
8229 | 8398 | } |
0 commit comments