|
25 | 25 |
|
26 | 26 | import java.util.HashMap;
|
27 | 27 | import java.util.WeakHashMap;
|
| 28 | +import java.util.concurrent.ArrayBlockingQueue; |
| 29 | +import java.util.concurrent.BlockingQueue; |
| 30 | +import java.util.concurrent.ExecutorService; |
| 31 | +import java.util.concurrent.Executors; |
| 32 | +import java.util.concurrent.RejectedExecutionException; |
| 33 | +import java.util.concurrent.TimeUnit; |
28 | 34 |
|
29 | 35 | import processing.opengl.PGL;
|
30 | 36 | import processing.opengl.PShader;
|
@@ -5664,4 +5670,158 @@ public boolean is3D() {
|
5664 | 5670 | public boolean isGL() {
|
5665 | 5671 | return false;
|
5666 | 5672 | }
|
| 5673 | + |
| 5674 | + |
| 5675 | + ////////////////////////////////////////////////////////////// |
| 5676 | + |
| 5677 | + // ASYNC IMAGE SAVING |
| 5678 | + |
| 5679 | + |
| 5680 | + @Override |
| 5681 | + public boolean save(String filename) { // ignore |
| 5682 | + |
| 5683 | + if (hints[DISABLE_ASYNC_SAVEFRAME]) { |
| 5684 | + return super.save(filename); |
| 5685 | + } |
| 5686 | + |
| 5687 | + if (asyncImageSaver == null) { |
| 5688 | + asyncImageSaver = new AsyncImageSaver(); |
| 5689 | + } |
| 5690 | + |
| 5691 | + if (!loaded) loadPixels(); |
| 5692 | + PImage target = asyncImageSaver.getAvailableTarget(pixelWidth, pixelHeight, |
| 5693 | + format); |
| 5694 | + if (target == null) return false; |
| 5695 | + int count = PApplet.min(pixels.length, target.pixels.length); |
| 5696 | + System.arraycopy(pixels, 0, target.pixels, 0, count); |
| 5697 | + asyncImageSaver.saveTargetAsync(this, target, filename); |
| 5698 | + |
| 5699 | + return true; |
| 5700 | + } |
| 5701 | + |
| 5702 | + protected void processImageBeforeAsyncSave(PImage image) { } |
| 5703 | + |
| 5704 | + |
| 5705 | + protected static AsyncImageSaver asyncImageSaver; |
| 5706 | + |
| 5707 | + protected static class AsyncImageSaver { |
| 5708 | + |
| 5709 | + static final int TARGET_COUNT = |
| 5710 | + Math.max(1, Runtime.getRuntime().availableProcessors() - 1); |
| 5711 | + |
| 5712 | + BlockingQueue<PImage> targetPool = new ArrayBlockingQueue<>(TARGET_COUNT); |
| 5713 | + ExecutorService saveExecutor = Executors.newFixedThreadPool(TARGET_COUNT); |
| 5714 | + |
| 5715 | + int targetsCreated = 0; |
| 5716 | + |
| 5717 | + |
| 5718 | + static final int TIME_AVG_FACTOR = 32; |
| 5719 | + |
| 5720 | + volatile long avgNanos = 0; |
| 5721 | + long lastTime = 0; |
| 5722 | + int lastFrameCount = 0; |
| 5723 | + |
| 5724 | + |
| 5725 | + public AsyncImageSaver() { } // ignore |
| 5726 | + |
| 5727 | + |
| 5728 | + public void dispose() { // ignore |
| 5729 | + saveExecutor.shutdown(); |
| 5730 | + try { |
| 5731 | + saveExecutor.awaitTermination(5000, TimeUnit.SECONDS); |
| 5732 | + } catch (InterruptedException e) { } |
| 5733 | + } |
| 5734 | + |
| 5735 | + |
| 5736 | + public boolean hasAvailableTarget() { // ignore |
| 5737 | + return targetsCreated < TARGET_COUNT || targetPool.isEmpty(); |
| 5738 | + } |
| 5739 | + |
| 5740 | + |
| 5741 | + /** |
| 5742 | + * After taking a target, you must call saveTargetAsync() or |
| 5743 | + * returnUnusedTarget(), otherwise one thread won't be able to run |
| 5744 | + */ |
| 5745 | + public PImage getAvailableTarget(int requestedWidth, int requestedHeight, // ignore |
| 5746 | + int format) { |
| 5747 | + try { |
| 5748 | + PImage target; |
| 5749 | + if (targetsCreated < TARGET_COUNT && targetPool.isEmpty()) { |
| 5750 | + target = new PImage(requestedWidth, requestedHeight); |
| 5751 | + targetsCreated++; |
| 5752 | + } else { |
| 5753 | + target = targetPool.take(); |
| 5754 | + if (target.width != requestedWidth || |
| 5755 | + target.height != requestedHeight) { |
| 5756 | + target.width = requestedWidth; |
| 5757 | + target.height = requestedHeight; |
| 5758 | + // TODO: this kills performance when saving different sizes |
| 5759 | + target.pixels = new int[requestedWidth * requestedHeight]; |
| 5760 | + } |
| 5761 | + } |
| 5762 | + target.format = format; |
| 5763 | + return target; |
| 5764 | + } catch (InterruptedException e) { |
| 5765 | + return null; |
| 5766 | + } |
| 5767 | + } |
| 5768 | + |
| 5769 | + |
| 5770 | + public void returnUnusedTarget(PImage target) { // ignore |
| 5771 | + targetPool.offer(target); |
| 5772 | + } |
| 5773 | + |
| 5774 | + |
| 5775 | + public void saveTargetAsync(final PGraphics renderer, final PImage target, // ignore |
| 5776 | + final String filename) { |
| 5777 | + target.parent = renderer.parent; |
| 5778 | + |
| 5779 | + // if running every frame, smooth the framerate |
| 5780 | + if (target.parent.frameCount - 1 == lastFrameCount && TARGET_COUNT > 1) { |
| 5781 | + |
| 5782 | + // count with one less thread to reduce jitter |
| 5783 | + // 2 cores - 1 save thread - no wait |
| 5784 | + // 4 cores - 3 save threads - wait 1/2 of save time |
| 5785 | + // 8 cores - 7 save threads - wait 1/6 of save time |
| 5786 | + long avgTimePerFrame = avgNanos / (Math.max(1, TARGET_COUNT - 1)); |
| 5787 | + long now = System.nanoTime(); |
| 5788 | + long delay = PApplet.round((lastTime + avgTimePerFrame - now) / 1e6f); |
| 5789 | + try { |
| 5790 | + if (delay > 0) Thread.sleep(delay); |
| 5791 | + } catch (InterruptedException e) { } |
| 5792 | + } |
| 5793 | + |
| 5794 | + lastFrameCount = target.parent.frameCount; |
| 5795 | + lastTime = System.nanoTime(); |
| 5796 | + |
| 5797 | + try { |
| 5798 | + saveExecutor.submit(new Runnable() { |
| 5799 | + @Override |
| 5800 | + public void run() { // ignore |
| 5801 | + try { |
| 5802 | + long startTime = System.nanoTime(); |
| 5803 | + renderer.processImageBeforeAsyncSave(target); |
| 5804 | + target.save(filename); |
| 5805 | + long saveNanos = System.nanoTime() - startTime; |
| 5806 | + synchronized (AsyncImageSaver.this) { |
| 5807 | + if (avgNanos == 0) { |
| 5808 | + avgNanos = saveNanos; |
| 5809 | + } else if (saveNanos < avgNanos) { |
| 5810 | + avgNanos = (avgNanos * (TIME_AVG_FACTOR - 1) + saveNanos) / |
| 5811 | + (TIME_AVG_FACTOR); |
| 5812 | + } else { |
| 5813 | + avgNanos = saveNanos; |
| 5814 | + } |
| 5815 | + } |
| 5816 | + } finally { |
| 5817 | + targetPool.offer(target); |
| 5818 | + } |
| 5819 | + } |
| 5820 | + }); |
| 5821 | + } catch (RejectedExecutionException e) { |
| 5822 | + // the executor service was probably shut down, no more saving for us |
| 5823 | + } |
| 5824 | + } |
| 5825 | + } |
| 5826 | + |
5667 | 5827 | }
|
0 commit comments