/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion 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.
*/
package org.illarion.engine.backend.shared;
import illarion.common.util.PoolThreadFactory;
import illarion.common.util.ProgressMonitor;
import org.illarion.engine.assets.TextureManager;
import org.illarion.engine.graphic.Texture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* This is the shared code of the texture manager that is used by all backend implementations in a similar way.
*
* @author Martin Karing >nitram@illarion.org<
*/
public abstract class AbstractTextureManager<T> implements TextureManager {
private static final Logger log = LoggerFactory.getLogger(AbstractTextureManager.class);
/**
* These are the progress monitors for each directory.
*/
@Nonnull
private final List<ProgressMonitor> directoryMonitors;
/**
* The list of known root directories. This list is used to locate
*/
@Nonnull
private final List<String> rootDirectories;
/**
* This stores the values if a directory is done loading or currently loading.
*/
@Nonnull
private final List<Boolean> directoriesLoaded;
/**
* The textures that are known to this manager.
*/
@Nonnull
private final Map<String, Texture> textures;
/**
* This is the progress monitor that can be used to keep track of the texture atlas loading.
*/
@Nonnull
private final ProgressMonitor progressMonitor;
/**
* This executor takes care for the tasks required to load the textures properly.
*/
@Nullable
private ExecutorService loadingExecutor;
/**
* This is a list of loading tasks. Once all entries in this list are cleared, the loading is considered done.
*/
@Nullable
private Deque<TextureAtlasTask> loadingTasks;
/**
* These are the tasks that are progressed in the graphics context.
*/
@Nullable
private Queue<Runnable> updateTasks;
private boolean loadingStarted;
/**
* Creates a new texture loader.
*/
protected AbstractTextureManager() {
directoryMonitors = new ArrayList<>();
rootDirectories = new ArrayList<>();
textures = new HashMap<>();
progressMonitor = new ProgressMonitor();
directoriesLoaded = new ArrayList<>();
}
@Override
public void startLoading() {
if (isLoadingDone()) {
return;
}
if (loadingExecutor != null) {
log.warn("Trying to load texture files while loading already in progress.");
return;
}
loadingStarted = true;
loadingTasks = new ConcurrentLinkedDeque<>();
updateTasks = new ConcurrentLinkedQueue<>();
// Prepare the parser factory for processing the XML files
XmlPullParserFactory parserFactory;
try {
parserFactory = XmlPullParserFactory.newInstance();
} catch (XmlPullParserException e) {
throw new IllegalStateException(e);
}
parserFactory.setNamespaceAware(false);
parserFactory.setValidating(false);
// Loading starts here. Firing up the executor.
loadingExecutor = Executors.newFixedThreadPool(2, new PoolThreadFactory("TextureLoading", false));
int directoryCount = rootDirectories.size();
for (int i = 0; i < directoryCount; i++) {
if (directoriesLoaded.get(i)) {
continue;
}
String directoryName = rootDirectories.get(i);
TextureAtlasListXmlLoadingTask<T> task =
new TextureAtlasListXmlLoadingTask<>(parserFactory, directoryName, this,
directoryMonitors.get(i), loadingExecutor);
loadingExecutor.execute(task);
loadingTasks.addFirst(task);
directoriesLoaded.set(i, Boolean.TRUE);
}
}
public void update() {
if (isLoadingDone()) {
return;
}
if (updateTasks == null) {
return;
}
long startTime = System.currentTimeMillis();
do {
Runnable task = updateTasks.poll();
if (task == null) {
return;
}
task.run();
} while ((System.currentTimeMillis() - startTime) < 100);
}
void addLoadingTask(@Nonnull TextureAtlasTask task) {
if (loadingTasks != null) {
loadingTasks.add(task);
}
}
void addUpdateTask(@Nonnull Runnable task) {
if (updateTasks != null) {
updateTasks.add(task);
}
}
@Override
@Nonnull
public ProgressMonitor getProgress() {
return progressMonitor;
}
@Nonnull
private static String cleanTextureName(@Nonnull String name) {
if (name.endsWith(".png")) {
return name.substring(0, name.length() - 4);
}
return name;
}
/**
* Combine the path from a directory and the name.
*
* @param directory the directory
* @param name the name of the new path element
* @return the full path, properly merged
*/
@Nonnull
private static String mergePath(@Nonnull String directory, @Nonnull String name) {
if (directory.endsWith("/")) {
return directory + name;
}
return directory + '/' + name;
}
/**
* This function is supposed to load the texture data as far as possible outside of the graphics context thread.
* Its very likely that the thread calling this function does not have the graphics context. The data prepared here
* will be provided along with the final texture loading call.
*
* @param textureName the name of the texture file
* @return the texture data
*/
@Nullable
protected abstract T loadTextureData(@Nonnull String textureName);
@Override
public final void addTextureDirectory(@Nonnull String directory) {
rootDirectories.add(directory);
ProgressMonitor dirProgressMonitor = new ProgressMonitor();
directoryMonitors.add(dirProgressMonitor);
progressMonitor.addChild(dirProgressMonitor);
directoriesLoaded.add(Boolean.FALSE);
}
/**
* Get the index of a root directory.
*
* @param directory the directory
* @return the index of the root directory or {@code -1} in case the directory could not be assigned to a root
* directory
*/
private int getDirectoryIndex(@Nonnull String directory) {
if (directory.endsWith("/")) {
return rootDirectories.indexOf(directory.substring(0, directory.length() - 1));
}
return rootDirectories.indexOf(directory);
}
/**
* Get the directory index for a file. The expected file name should start with the name of the directory.
*
* @param name the name of the file
* @return the index of the directory or {@code -1} in case there is not matching directory
*/
protected int getFileDirectoryIndex(@Nonnull String name) {
for (int i = 0; i < rootDirectories.size(); i++) {
if (name.startsWith(rootDirectories.get(i))) {
return i;
}
}
return -1;
}
@Nullable
@Override
public Texture getTexture(@Nonnull String name) {
int directoryIndex = getFileDirectoryIndex(name);
if (directoryIndex >= 0) {
return getTexture(directoryIndex, name);
}
return null;
}
@Nullable
public Texture getTexture(int directoryIndex, @Nonnull String name) {
if (directoryIndex == -1) {
return null;
}
if (log.isTraceEnabled()) {
log.trace("Texture {} requested from directory: {}", name, rootDirectories.get(directoryIndex));
}
String cleanName = cleanTextureName(name);
// Checking if the texture is among already loaded textures.
@Nullable Texture loadedTexture = textures.get(cleanName);
if (loadedTexture != null) {
log.trace("Found texture {} among the already loaded texture.", cleanName);
return loadedTexture;
}
// Checking if the texture is located on a separated file.
@Nullable T preLoadTextureData = loadTextureData(cleanName + ".png");
if (preLoadTextureData != null) {
@Nullable Texture directTexture = loadTexture(cleanName + ".png", preLoadTextureData);
if (directTexture != null) {
log.trace("Fetched texture {} by direct name.", cleanName);
textures.put(cleanName, directTexture);
return directTexture;
}
}
// We reached a point where everything just turns to be crappy. The file appears to be on a texture atlas that
// is not yet loaded.
if (!directoriesLoaded.get(directoryIndex)) {
String directoryName = rootDirectories.get(directoryIndex);
log.trace("Start loading directory: {}", directoryName);
XmlPullParserFactory parserFactory;
try {
parserFactory = XmlPullParserFactory.newInstance();
} catch (XmlPullParserException e) {
log.error("Failed to create parser factory.", e);
return null;
}
parserFactory.setNamespaceAware(false);
parserFactory.setValidating(false);
TextureAtlasListXmlLoadingTask<T> task =
new TextureAtlasListXmlLoadingTask<>(parserFactory, directoryName, this,
directoryMonitors.get(directoryIndex), null);
if (loadingTasks == null) {
loadingTasks = new ConcurrentLinkedDeque<>();
updateTasks = new ConcurrentLinkedQueue<>();
}
loadingTasks.add(task);
task.run();
directoriesLoaded.set(directoryIndex, Boolean.TRUE);
loadingStarted = true;
while (!isLoadingDone()) {
update();
}
loadingStarted = false;
log.trace("Loading of directory {} is done.", directoryName);
Texture result = textures.get(cleanName);
if (result == null) {
log.error("Failed to load texture: {} from directory: {}", cleanName, directoryName);
}
return result;
}
return null;
}
@Nullable
@Override
public Texture getTexture(@Nonnull String directory, @Nonnull String name) {
return getTexture(getDirectoryIndex(directory), mergePath(directory, name));
}
@Override
public boolean isLoadingDone() {
if (!loadingStarted) {
return false;
}
if (loadingTasks == null) {
return true;
}
while (!loadingTasks.isEmpty()) {
TextureAtlasTask task = loadingTasks.peekFirst();
if (task.isDone()) {
loadingTasks.removeFirst();
} else {
return false;
}
}
for (@Nonnull ProgressMonitor dirMonitor : directoryMonitors) {
dirMonitor.setProgress(1.f);
}
if (loadingExecutor != null) {
loadingExecutor.shutdown();
}
loadingExecutor = null;
loadingTasks = null;
return true;
}
/**
* Load the texture from a specific resource.
*
* @param resource the path to the resource
* @return the texture loaded or {@code null} in case loading is impossible
*/
@Nullable
protected abstract Texture loadTexture(@Nonnull String resource, @Nonnull T preLoadData);
protected void addTexture(@Nonnull String textureName, @Nonnull Texture texture) {
textures.put(textureName, texture);
}
}