/* Copyright (c) 2012-2016 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chunky is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Chunky. If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.renderer;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.chunky.resources.BitmapImage;
import se.llbit.log.Log;
import se.llbit.util.TaskTracker;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* This class manages render workers. Each worker renders one tile at a time,
* and the render manager ensures that each worker is assigned unique tiles.
*
* <p>The secondary purpose of the render manager is to manage the scene state
* which the workers use.
*
* <p>Scene state is kept in Scene objects. The render controls dialog
* stores the scene state in its own Scene object, which is read by
* the render manager through the SceneProvider interface.
* The render manager keeps an internal copy of the scene state which
* is ensured to be unmodified while render workers are rendering.
*
* <p>A snapshot of the current render can be accessed by calling withSnapshot().
*
* @author Jesper Öqvist <jesper@llbit.se>
*/
public class RenderManager extends AbstractRenderManager implements Renderer {
public static final Repaintable EMPTY_CANVAS = () -> {};
private boolean finalizeAllFrames = false;
private Repaintable canvas = EMPTY_CANVAS;
private Thread[] workers = {};
private int numJobs = 0;
/**
* This scene state is used by render workers while rendering.
* The buffered scene is only updated when the workers are
* quiescent.
*/
private final Scene bufferedScene;
/**
* Gives the next tile index for a worker.
*/
private final AtomicInteger nextJob = new AtomicInteger(0);
/** Number of completed jobs. */
private final AtomicInteger finishedJobs = new AtomicInteger(0);
private Collection<RenderStatusListener> listeners = new ArrayList<>();
private BiConsumer<Long, Integer> renderCompletionListener = (time, sps) -> {};
private BiConsumer<Scene, Integer> frameCompletionListener = (scene, spp) -> {};
private TaskTracker.Task renderTask = TaskTracker.Task.NONE;
/**
* Decides if render threads shut down after reaching the target SPP.
*/
private final boolean headless;
/**
* Current renderer mode.
*/
private RenderMode mode = RenderMode.PREVIEW;
private final Collection<SceneStatusListener> sceneListeners = new ArrayList<>();
private SnapshotControl snapshotControl = SnapshotControl.DEFAULT;
/**
* @param headless {@code true} if rendering threads should be shut
* down after reaching the render target.
*/
public RenderManager(RenderContext context, boolean headless) {
super(context);
this.headless = headless;
bufferedScene = context.getChunky().getSceneFactory().newScene();
manageWorkers();
}
@Override public synchronized void addRenderListener(RenderStatusListener listener) {
listeners.add(listener);
}
@Override public void removeRenderListener(RenderStatusListener listener) {
listeners.remove(listener);
}
private void manageWorkers() {
if (numThreads != workers.length) {
long seed = System.currentTimeMillis();
Thread[] pool = new Thread[numThreads];
int i;
for (i = 0; i < workers.length && i < numThreads; ++i) {
pool[i] = workers[i];
}
// Start additional workers.
for (; i < numThreads; ++i) {
pool[i] = workerFactory.buildWorker(this, i, seed + i);
pool[i].start();
}
// Stop extra workers.
for (; i < workers.length; ++i) {
workers[i].interrupt();
}
workers = pool;
}
}
@Override public void setCanvas(Repaintable canvas) {
this.canvas = canvas;
}
@Override public void run() {
try {
while (!isInterrupted()) {
ResetReason reason = sceneProvider.awaitSceneStateChange();
synchronized (bufferedScene) {
sceneProvider.withSceneProtected(scene -> {
if (reason.overwriteState()) {
bufferedScene.copyState(scene);
scene.importMaterials();
}
bufferedScene.copyTransients(scene);
finalizeAllFrames = scene.shouldFinalizeBuffer();
updateRenderState(scene);
if (reason == ResetReason.SCENE_LOADED) {
// Swap buffers so the render canvas will see the current frame.
bufferedScene.swapBuffers();
// Notify the scene listeners (this triggers a canvas repaint).
sendSceneStatus(bufferedScene.sceneStatus());
}
});
}
if (mode == RenderMode.PREVIEW) {
previewLoop();
} else {
int spp, targetSpp;
synchronized (bufferedScene) {
spp = bufferedScene.spp;
targetSpp = bufferedScene.getTargetSpp();
if (spp < targetSpp) {
updateRenderProgress();
}
}
if (spp < targetSpp) {
pathTraceLoop();
} else {
sceneProvider.withEditSceneProtected(scene -> {
scene.pauseRender();
updateRenderState(scene);
});
}
}
if (headless) {
break;
}
}
} catch (InterruptedException e) {
// 3D view was closed.
} catch (Throwable e) {
Log.error("Unchecked exception in render manager", e);
}
stopWorkers();
}
private void updateRenderState(Scene scene) {
finalizeAllFrames = scene.shouldFinalizeBuffer();
if (mode != scene.getMode()) {
mode = scene.getMode();
// TODO: make render state update faster by moving this to Scene?
listeners.forEach(listener -> listener.renderStateChanged(mode));
}
}
/**
* Continually render frames until we reach the SPP target, or until
* the render state is changed externally.
* @throws InterruptedException
*/
private void pathTraceLoop() throws InterruptedException {
while (true) {
sceneProvider.withSceneProtected(scene -> {
synchronized (bufferedScene) {
bufferedScene.copyTransients(scene);
updateRenderState(scene);
}
});
if (mode == RenderMode.PAUSED || sceneProvider.pollSceneStateChange()) {
return;
}
synchronized (bufferedScene) {
long frameStart = System.currentTimeMillis();
giveTickets();
waitOnWorkers();
bufferedScene.swapBuffers();
bufferedScene.renderTime += System.currentTimeMillis() - frameStart;
}
// Notify the canvas to repaint.
canvas.repaint();
synchronized (bufferedScene) {
bufferedScene.spp += RenderConstants.SPP_PER_PASS;
int currentSpp = bufferedScene.spp;
frameCompletionListener.accept(bufferedScene, currentSpp);
updateRenderProgress();
if (currentSpp >= bufferedScene.getTargetSpp()) {
renderCompletionListener.accept(bufferedScene.renderTime, samplesPerSecond());
return;
}
}
}
}
/**
* @return the current rendering speed in samples per second (SPS)
*/
private int samplesPerSecond() {
int canvasWidth = bufferedScene.canvasWidth();
int canvasHeight = bufferedScene.canvasHeight();
long pixelsPerFrame = canvasWidth * canvasHeight;
double renderTime = bufferedScene.renderTime / 1000.0;
return (int) ((bufferedScene.spp * pixelsPerFrame) / renderTime);
}
private void updateRenderProgress() {
double renderTime = bufferedScene.renderTime / 1000.0;
// Notify progress listener.
int target = bufferedScene.getTargetSpp();
long etaSeconds = (long) (((target - bufferedScene.spp) * renderTime) / bufferedScene.spp);
if (etaSeconds > 0) {
int seconds = (int) ((etaSeconds) % 60);
int minutes = (int) ((etaSeconds / 60) % 60);
int hours = (int) (etaSeconds / 3600);
String eta = String.format("%d:%02d:%02d", hours, minutes, seconds);
renderTask.update("Rendering", target, bufferedScene.spp, eta);
} else {
renderTask.update("Rendering", target, bufferedScene.spp, "");
}
synchronized (this) {
// Update render status display.
listeners.forEach(listener -> {
listener.setRenderTime(bufferedScene.renderTime);
listener.setSamplesPerSecond(samplesPerSecond());
listener.setSpp(bufferedScene.spp);
});
}
}
private void previewLoop() throws InterruptedException {
long frameStart;
renderTask.update("Preview", 2, 0, "");
synchronized (bufferedScene) {
bufferedScene.previewCount = 2;
}
while (true) {
int previewCount;
synchronized (bufferedScene) {
previewCount = bufferedScene.previewCount;
}
if (!finalizeAllFrames || previewCount <= 0 || sceneProvider.pollSceneStateChange()) {
return;
}
int progress;
long renderTime;
synchronized (bufferedScene) {
frameStart = System.currentTimeMillis();
giveTickets();
waitOnWorkers();
bufferedScene.swapBuffers();
sendSceneStatus(bufferedScene.sceneStatus());
bufferedScene.renderTime += System.currentTimeMillis() - frameStart;
bufferedScene.previewCount -= 1;
bufferedScene.spp = 0;
progress = 2 - bufferedScene.previewCount;
renderTime = bufferedScene.renderTime;
}
// Update render status display.
listeners.forEach(listener -> {
listener.setRenderTime(renderTime);
listener.setSamplesPerSecond(0);
listener.setSpp(0);
});
// Update render progress.
renderTask.update("Preview", 2, progress, "");
// Notify the canvas to repaint.
canvas.repaint();
}
}
private synchronized void waitOnFrameCompletion() throws InterruptedException {
while (finishedJobs.get() < numJobs) {
wait();
}
}
private synchronized void waitOnWorkers() throws InterruptedException {
waitOnFrameCompletion();
manageWorkers(); // Adjust number of worker threads if needed.
}
private synchronized void giveTickets() {
int nextSpp = bufferedScene.spp + RenderConstants.SPP_PER_PASS;
bufferedScene.setBufferFinalization(finalizeAllFrames
|| snapshotControl.saveSnapshot(bufferedScene, nextSpp));
int canvasWidth = bufferedScene.canvasWidth();
int canvasHeight = bufferedScene.canvasHeight();
numJobs = ((canvasWidth + (tileWidth - 1)) / tileWidth) * ((canvasHeight + (tileWidth - 1))
/ tileWidth);
nextJob.set(0);
finishedJobs.set(0);
notifyAll();
}
@Override public int getNextJob() throws InterruptedException {
int jobId = nextJob.getAndIncrement();
if (jobId >= numJobs) {
synchronized (this) {
do {
wait();
jobId = nextJob.getAndIncrement();
} while (jobId >= numJobs);
}
}
return jobId;
}
@Override public void jobDone() {
int finished = finishedJobs.incrementAndGet();
if (finished >= numJobs) {
synchronized (this) {
notifyAll();
}
}
}
@Override public Scene getBufferedScene() {
return bufferedScene;
}
/**
* Call the consumer with the current front frame buffer.
*/
@Override public void withBufferedImage(Consumer<BitmapImage> consumer) {
bufferedScene.withBufferedImage(consumer);
}
/**
* Change number of render workers.
*
* @param threads new required thread count.
*/
@Override public void setNumThreads(int threads) {
numThreads = Math.max(1, threads);
}
@Override public void setOnRenderCompleted(BiConsumer<Long, Integer> listener) {
renderCompletionListener = listener;
}
@Override public void setOnFrameCompleted(BiConsumer<Scene, Integer> listener) {
frameCompletionListener = listener;
}
@Override public void setSnapshotControl(SnapshotControl callback) {
this.snapshotControl = callback;
}
@Override public void setRenderTask(TaskTracker.Task task) {
renderTask = task;
}
/**
* Set CPU load percentage.
*
* @param value new load percentage.
*/
@Override public void setCPULoad(int value) {
cpuLoad = value;
}
/**
* Stop render workers.
*/
private synchronized void stopWorkers() {
// Halt all worker threads.
for (int i = 0; i < numThreads; ++i) {
workers[i].interrupt();
}
}
@Override public synchronized void addSceneStatusListener(SceneStatusListener listener) {
sceneListeners.add(listener);
}
@Override public synchronized void removeSceneStatusListener(SceneStatusListener listener) {
sceneListeners.remove(listener);
}
@Override public RenderStatus getRenderStatus() {
RenderStatus status;
synchronized (bufferedScene) {
status = new RenderStatus(bufferedScene.renderTime, bufferedScene.spp);
}
return status;
}
@Override public void withSampleBufferProtected(SampleBufferConsumer consumer) {
// Synchronizing on bufferedScene ensures that we are outside the frame rendering loop.
synchronized (bufferedScene) {
consumer.accept(bufferedScene.getSampleBuffer(), bufferedScene.width, bufferedScene.height);
}
}
/**
* Sends scene status text to the render preview tooltip.
*/
private synchronized void sendSceneStatus(String status) {
for (SceneStatusListener listener : sceneListeners) {
listener.sceneStatus(status);
}
}
@Override public void shutdown() {
interrupt();
}
}