opsu-dance/src/itdelatrisu/opsu/beatmap/ImageLoader.java
Jeffrey Han 3214916d60 Load beatmap background images in a new thread.
This eliminates the game-wide lag (100-200ms on my computer) when switching song nodes. Attempted to mask the loading time with a fade-in effect.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
2015-09-03 19:24:07 -05:00

180 lines
4.9 KiB
Java

/*
* opsu! - an open-source osu! client
* Copyright (C) 2014, 2015 Jeffrey Han
*
* opsu! 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.
*
* opsu! 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 opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.beatmap;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.opengl.ImageData;
import org.newdawn.slick.opengl.ImageDataFactory;
import org.newdawn.slick.opengl.LoadableImageData;
import org.newdawn.slick.util.Log;
/**
* Simple threaded image loader for a single image file.
*/
public class ImageLoader {
/** The image file. */
private final File file;
/** The loaded image. */
private Image image;
/** The image data. */
private LoadedImageData data;
/** The image loader thread. */
private Thread loaderThread;
/** ImageData wrapper, needed because {@code ImageIOImageData} doesn't implement {@code getImageBufferData()}. */
private class LoadedImageData implements ImageData {
/** The image data implementation. */
private final ImageData imageData;
/** The stored image. */
private final ByteBuffer buffer;
/**
* Constructor.
* @param imageData the class holding the image properties
* @param buffer the stored image
*/
public LoadedImageData(ImageData imageData, ByteBuffer buffer) {
this.imageData = imageData;
this.buffer = buffer;
}
@Override public int getDepth() { return imageData.getDepth(); }
@Override public int getWidth() { return imageData.getWidth(); }
@Override public int getHeight() { return imageData.getHeight();}
@Override public int getTexWidth() { return imageData.getTexWidth(); }
@Override public int getTexHeight() { return imageData.getTexHeight(); }
@Override public ByteBuffer getImageBufferData() { return buffer; }
}
/** Image loading thread. */
private class ImageLoaderThread extends Thread {
/** The image file input stream. */
private BufferedInputStream in;
@Override
public void interrupt() {
super.interrupt();
if (in != null) {
try {
in.close(); // interrupt I/O
} catch (IOException e) {}
}
}
@Override
public void run() {
// load image data into a ByteBuffer to use constructor Image(ImageData)
LoadableImageData imageData = ImageDataFactory.getImageDataFor(file.getAbsolutePath());
try (BufferedInputStream in = this.in = new BufferedInputStream(new FileInputStream(file))) {
ByteBuffer textureBuffer = imageData.loadImage(in, false, null);
if (!isInterrupted())
data = new LoadedImageData(imageData, textureBuffer);
} catch (IOException e) {
if (!isInterrupted())
Log.warn(String.format("Failed to load background image '%s'.", file), e);
}
this.in = null;
}
}
/**
* Constructor. Call {@link ImageLoader#load(boolean)} to load the image.
* @param file the image file
*/
public ImageLoader(File file) {
this.file = file;
}
/**
* Loads the image.
* @param threaded true to load the image data in a new thread
*/
public void load(boolean threaded) {
if (!file.isFile())
return;
if (threaded) {
if (loaderThread != null && loaderThread.isAlive())
loaderThread.interrupt();
loaderThread = new ImageLoaderThread();
loaderThread.start();
} else {
try {
image = new Image(file.getAbsolutePath());
} catch (SlickException e) {
Log.warn(String.format("Failed to load background image '%s'.", file), e);
}
}
}
/**
* Returns the image.
* @return the loaded image, or null if not loaded
*/
public Image getImage() {
if (image == null && data != null) {
image = new Image(data);
data = null;
}
return image;
}
/**
* Returns whether an image is currently loading in another thread.
* @return true if loading, false otherwise
*/
public boolean isLoading() { return (loaderThread != null && loaderThread.isAlive()); }
/**
* Interrupts the image loader, if running.
*/
public void interrupt() {
if (isLoading())
loaderThread.interrupt();
}
/**
* Releases all resources.
*/
public void destroy() {
interrupt();
loaderThread = null;
if (image != null && !image.isDestroyed()) {
try {
image.destroy();
} catch (SlickException e) {
Log.warn(String.format("Failed to destroy image '%s'.", image.getResourceReference()), e);
}
image = null;
}
data = null;
}
}