/* Copyright (c) 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.scene; import se.llbit.chunky.renderer.RenderContext; import se.llbit.chunky.renderer.RenderMode; import se.llbit.chunky.renderer.RenderStatus; import se.llbit.chunky.renderer.Renderer; import se.llbit.chunky.renderer.ResetReason; import se.llbit.chunky.renderer.SceneProvider; import se.llbit.chunky.world.ChunkPosition; import se.llbit.chunky.world.World; import se.llbit.log.Log; import se.llbit.util.ProgressListener; import se.llbit.util.TaskTracker; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.function.Consumer; /** * A synchronous scene manager runs its operations on the calling thread. * * <p>The scene manager stores the current scene state and pending * scene state changes. The scene manager is responsible for protecting * parts of the scene data from concurrent writes & reads by * the user (through the UI) and renderer. */ public class SynchronousSceneManager implements SceneProvider, SceneManager { /** * This stores all pending scene state changes. When the scene edit * grace period has expired any changes to this scene state are not * copied directly to the stored scene state. * * Multiple threads can try to read/write the mutable scene concurrently, * so multiple accesses are serialized by the intrinsic lock of the Scene * class. * * NB: lock ordering for scene and storedScene is always scene->storedScene! */ private final Scene scene; /** * Stores the current scene configuration. When the scene edit grace period has * expired a reset confirm dialog will be shown before applying any further * non-transitory changes to the stored scene state. */ private final Scene storedScene; private final RenderContext context; private final Renderer renderer; private RenderResetHandler resetHandler = () -> true; private TaskTracker taskTracker = new TaskTracker(ProgressListener.NONE); private Runnable onSceneLoaded = () -> {}; private Runnable onChunksLoaded = () -> {}; public SynchronousSceneManager(RenderContext context, Renderer renderer) { this.context = context; this.renderer = renderer; scene = context.getChunky().getSceneFactory().newScene(); scene.initBuffers(); // The stored scene is a copy of the mutable scene. They even share // some data structures that are only used by the renderer. storedScene = context.getChunky().getSceneFactory().copyScene(scene); } public void setResetHandler(RenderResetHandler resetHandler) { this.resetHandler = resetHandler; } public void setTaskTracker(TaskTracker taskTracker) { this.taskTracker = taskTracker; } public void setOnSceneLoaded(Runnable onSceneLoaded) { this.onSceneLoaded = onSceneLoaded; } public void setOnChunksLoaded(Runnable onChunksLoaded) { this.onChunksLoaded = onChunksLoaded; } @Override public Scene getScene() { return scene; } @Override public void saveScene() throws InterruptedException { try { synchronized (storedScene) { String sceneName = storedScene.name(); Log.info("Saving scene " + sceneName); File sceneDir = context.getSceneDirectory(); if (!sceneDir.isDirectory()) { Log.warn("Scene directory does not exist. Creating directory at: " + sceneDir.getAbsolutePath()); boolean success = sceneDir.mkdirs(); if (!success) { Log.warn("Failed to create scene directory: " + sceneDir.getAbsolutePath()); return; } } // Create backup of scene description and current render dump. storedScene.backupFile(context, context.getSceneDescriptionFile(sceneName)); storedScene.backupFile(context, sceneName + ".dump"); // Copy render status over from the renderer. RenderStatus status = renderer.getRenderStatus(); storedScene.renderTime = status.getRenderTime(); storedScene.spp = status.getSpp(); storedScene.saveScene(context, taskTracker); Log.info("Scene saved"); } } catch (IOException e) { Log.error("Failed to save scene. Reason: " + e.getMessage(), e); } } @Override public void loadScene(String sceneName) throws IOException, SceneLoadingError, InterruptedException { // Do not change lock ordering here. // Lock order: scene -> storedScene. synchronized (scene) { try (TaskTracker.Task ignored = taskTracker.task("Loading scene", 1)) { scene.loadScene(context, sceneName, taskTracker); } // Update progress bar. taskTracker.backgroundTask().update("Rendering", scene.getTargetSpp(), scene.spp); scene.setResetReason(ResetReason.SCENE_LOADED); // Wake up waiting threads in awaitSceneStateChange(). scene.notifyAll(); } onSceneLoaded.run(); } @Override public void loadFreshChunks(World world, Collection<ChunkPosition> chunksToLoad) { synchronized (scene) { scene.clear(); scene.loadChunks(taskTracker, world, chunksToLoad); scene.moveCameraToCenter(); scene.refresh(); scene.setResetReason(ResetReason.SCENE_LOADED); scene.setRenderMode(RenderMode.PREVIEW); } onSceneLoaded.run(); } @Override public void loadChunks(World world, Collection<ChunkPosition> chunksToLoad) { synchronized (scene) { scene.loadChunks(taskTracker, world, chunksToLoad); scene.refresh(); scene.setResetReason(ResetReason.SCENE_LOADED); scene.setRenderMode(RenderMode.PREVIEW); } onChunksLoaded.run(); } @Override public void reloadChunks() { synchronized (scene) { scene.reloadChunks(taskTracker); scene.refresh(); scene.setResetReason(ResetReason.SCENE_LOADED); scene.setRenderMode(RenderMode.PREVIEW); } onChunksLoaded.run(); } @Override public ResetReason awaitSceneStateChange() throws InterruptedException { synchronized (scene) { while (true) { if (scene.shouldRefresh() && (scene.getForceReset() || resetHandler.allowSceneRefresh())) { synchronized (storedScene) { storedScene.copyState(scene); storedScene.mode = scene.mode; } ResetReason reason = scene.getResetReason(); scene.clearResetFlags(); return reason; } else if (scene.getMode() != storedScene.getMode()) { // Make sure the renderer sees the updated render mode. // TODO: handle buffer finalization updates as state change. synchronized (storedScene) { storedScene.mode = scene.mode; } return ResetReason.MODE_CHANGE; } scene.wait(); } } } @Override public boolean pollSceneStateChange() { if (scene.shouldRefresh() && (scene.getForceReset() || resetHandler.allowSceneRefresh())) { return true; } else if (scene.getMode() != storedScene.getMode()) { return true; } return false; } @Override public void withSceneProtected(Consumer<Scene> fun) { // Lock order: scene -> storedScene. synchronized (scene) { synchronized (storedScene) { storedScene.copyTransients(scene); fun.accept(storedScene); } } } @Override public void withEditSceneProtected(Consumer<Scene> fun) { synchronized (scene) { fun.accept(scene); } } /** * Merge a render dump into the current render. * * @param dumpFile the file to be merged. */ protected void mergeDump(File dumpFile) { synchronized (scene) { renderer.withSampleBufferProtected((samples, width, height) ->{ if (width != scene.width || height != scene.height) { throw new Error("Failed to merge render dump - wrong canvas size."); } scene.mergeDump(dumpFile, taskTracker); }); scene.setResetReason(ResetReason.SCENE_LOADED); } } /** * Discard pending scene changes. */ public void applySceneChanges() { // Lock order: scene -> storedScene. synchronized (scene) { synchronized (storedScene) { // Setting SCENE_LOADED will force the reset. scene.setResetReason(ResetReason.SCENE_LOADED); // Wake up the threads waiting in awaitSceneStateChange(). scene.notifyAll(); } } } /** * Apply pending scene changes. */ public void discardSceneChanges() { // Lock order: scene -> storedScene. synchronized (scene) { synchronized (storedScene) { scene.copyState(storedScene); scene.clearResetFlags(); } } } }