/*
* Copyright 2016 MovingBlocks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.rendering.dag;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Map;
/**
* Instances of this class are responsible for generating the the list of tasks
* the renderer executes to (eventually) generate the image shown to the user.
*
* Tasks are generated out of an ordered list of nodes in the render graph. Each node provides the set
* of state changes it needs. Tasks are generated for each state change unless they are redundant.
* State changes from a node are redundant if the previous node want the exact same state change.
*
* After tasks for the non-redundant state changes desired by a node have been generated a task
* is appended to the list, to execute the node's process() method, which is where any rendering
* actually happens.
*
* Finally, each node also provides a list of state change resets: state changes bringing a property
* back to its default. Tasks are generated also these unless they'd be redundant. A state change
* reset is redundant if the next node needs to set that property to a non-default value.
*
* It should be noted that nodes are skipped if they are disabled.
*
*/
public final class RenderTaskListGenerator {
/**
* Instances of this class are intended to be inserted in the Render Task List.
*
* If the content of the task list is printed out by the logger, instances of this class
* visually separate the tasks releated to a node from those of the previous one.
*/
private class MarkerTask implements RenderPipelineTask {
private String message;
/**
* Instantiate a MarkerTask.
*
* @param message A string used by the toString() method.
*/
private MarkerTask(String message) {
this.message = message;
}
@Override
public void execute() { }
/**
* Returns a string description of the instance.
*
* @return A string in the form: "----- <message>",
* where <message> is the string passed to the constructor.
*/
public String toString() {
return String.format("----- %s", message);
}
}
private static final Logger logger = LoggerFactory.getLogger(RenderTaskListGenerator.class);
private List<RenderPipelineTask> taskList;
private List<Node> nodeList;
public RenderTaskListGenerator() {
taskList = Lists.newArrayList();
}
private void logIntermediateRendererListForDebugging(List<Node> orderedNodes) {
for (Node node : orderedNodes) {
if (node.isEnabled()) {
// printing out node name
logger.info(String.format(("----- %s"), node.getClass().getSimpleName()));
// printing out individual desired state changes
for (StateChange desiredStateChange : node.getDesiredStateChanges()) {
logger.info(desiredStateChange.toString());
}
// printing out process() statement
logger.info(String.format("%s: process()", node.toString()));
// printing out individual state resets
for (StateChange desiredStateReset : node.getDesiredStateResets()) {
logger.info(desiredStateReset.toString());
}
}
}
}
/**
* See the RenderTaskListGenerator class Javadoc for an overview of what this method does.
*
* @param orderedNodes a list of Node instances, ordered to reflect the dependencies between them,
* i.e. Node A must be processed before Node B to work correctly.
* @return an optimized list of RenderPipelineTask instances,
* ready to be iterated over to execute a frame worth of rendering
*/
public List<RenderPipelineTask> generateFrom(List<Node> orderedNodes) {
long startTimeInNanoSeconds = System.nanoTime();
// TODO: Optimization task: verify if we can avoid clearing the whole list
// TODO: whenever changes in the render graph or in the intermediate list arise
// TODO: think about refactoring (a heavy method)
nodeList = orderedNodes;
taskList.clear();
Node nextEnabledNode;
StateChange persistentStateChange;
Map<Class<?>, StateChange> persistentStateChanges = Maps.newHashMap(); // assuming we can't make it a private field for the time being
int enabledNodes = 0;
int potentialTasks = 0;
int currentIndex = 0;
for (Node node : orderedNodes) {
if (node.isEnabled()) {
if (logger.isInfoEnabled()) {
// Marker tasks just add a dividing line to the logger output
taskList.add(new MarkerTask(node.getClass().getSimpleName()));
enabledNodes++; // we count them only for statistical purposes
potentialTasks += node.getDesiredStateChanges().size() + 1 + node.getDesiredStateResets().size();
}
// generating tasks for the desired state changes
for (StateChange currentStateChange : node.getDesiredStateChanges()) {
// State changes persist beyond the node that request them if following nodes
// require the exact same change.
persistentStateChange = persistentStateChanges.get(currentStateChange.getClass());
// for a state change to be necessary there can't be an identical one already persisting
if (persistentStateChange == null || !currentStateChange.equals(persistentStateChange)) {
// no persistent state change of this subType found: the state change is necessary (not redundant)
taskList.add(currentStateChange.generateTask());
persistentStateChanges.put(currentStateChange.getClass(), currentStateChange);
} // else: the state change is redundant - we don't generate a task for it
}
// task executing the node.process() method
taskList.add(node.generateTask());
// generating tasks for the desired state resets
nextEnabledNode = findNextEnabledNode(orderedNodes, currentIndex + 1);
if (nextEnabledNode != null) {
// if there is one enabled node after this one we must check its desired state changes,
// to make sure we don't reset to default something it would set again.
// For example: no point binding the display (the default FBO) if the next enabled node
// will bind another FBO.
for (StateChange currentStateReset : node.getDesiredStateResets()) {
if (sameClassStateChangeNotFoundInThe(nextEnabledNode, currentStateReset)) {
persistentStateChange = persistentStateChanges.get(currentStateReset.getClass());
// A reset state change is necessary only if an identical state change isn't already persisting.
if (persistentStateChange == null || !currentStateReset.equals(persistentStateChange)) {
taskList.add(currentStateReset.generateTask());
// This makes sure that the reset state change persists so that future state changes
// which are identical to the reset state change are not added.
persistentStateChanges.put(currentStateReset.getClass(), currentStateReset);
}
}
}
} else {
// there are no enabled nodes after this one: we must generate all the tasks necessary
// to reset the effects of any persisting state change.
for (StateChange currentStateReset : node.getDesiredStateResets()) {
taskList.add(currentStateReset.generateTask());
persistentStateChanges.remove(currentStateReset.getClass());
}
}
}
currentIndex++;
}
long endTimeInNanoSeconds = System.nanoTime();
if (logger.isInfoEnabled()) {
logger.info("===== INTERMEDIATE RENDERER LIST =========================");
logIntermediateRendererListForDebugging(orderedNodes);
logger.info("===== RENDERER TASK LIST =================================");
logList(taskList);
logger.info("----------------------------------------------------------");
logger.info(String.format("Task list generated in %.3f ms", (endTimeInNanoSeconds - startTimeInNanoSeconds) / 1000000f));
logger.info(String.format("%s nodes, %s enabled - %s tasks (excluding marker tasks) out of %s potential tasks.",
nodeList.size(), enabledNodes, taskList.size() - enabledNodes, potentialTasks));
logger.info("----------------------------------------------------------");
}
return taskList;
}
private boolean sameClassStateChangeNotFoundInThe(Node nextEnabledNode, StateChange stateChangeReset) {
for (StateChange stateChange : nextEnabledNode.getDesiredStateChanges()) {
if (stateChange.getClass() == stateChangeReset.getClass()) {
return false; // we did find it! And yes, returning false is correct, see method name.
// note: we don't worry about the details of the two state changes (i.e. if they are value-identical)
// as this is something that will be checked while iterating over next node's desired state changes,
// in the first part of method generateTaskList.
}
}
return true; // we didn't find a state change of the same class
}
private Node findNextEnabledNode(List<Node> orderedNodeList, int startIndex) {
for (int index = startIndex; index < orderedNodeList.size(); index++) {
Node currentNode = orderedNodeList.get(index);
if (currentNode.isEnabled()) {
return currentNode;
}
}
return null;
}
private void logList(List<?> list) {
for (Object object : list) {
logger.info(object.toString());
}
}
/**
* Forces a refresh of the task list using the latest node list provided to the generateFrom method.
*
* A refresh is useful when one of the nodes has been enabled or disabled, as the tasks associated
* with it need to be added to the task list or removed from it. Tasks "downstream" of the change
* need to be re-evaluated then, as they might have become redundant.
*
* At this stage the refresh uses a brute-force approach: the whole task list is cleared and regenerated.
* Eventually it will be useful to make sure that only tasks affected by a change are regenerated.
*/
public void refresh() {
generateFrom(nodeList);
}
}