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>
This commit is contained in:
parent
e767800702
commit
3214916d60
|
@ -27,7 +27,6 @@ import java.util.Map;
|
|||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
|
@ -39,20 +38,19 @@ public class Beatmap implements Comparable<Beatmap> {
|
|||
|
||||
/** Background image cache. */
|
||||
@SuppressWarnings("serial")
|
||||
private static final LRUCache<File, Image> bgImageCache = new LRUCache<File, Image>(10) {
|
||||
private static final LRUCache<File, ImageLoader> bgImageCache = new LRUCache<File, ImageLoader>(10) {
|
||||
@Override
|
||||
public void eldestRemoved(Map.Entry<File, Image> eldest) {
|
||||
Image img = eldest.getValue();
|
||||
if (img != null && !img.isDestroyed()) {
|
||||
try {
|
||||
img.destroy(); // destroy the removed image
|
||||
} catch (SlickException e) {
|
||||
Log.warn(String.format("Failed to destroy image '%s'.", img.getResourceReference()), e);
|
||||
}
|
||||
}
|
||||
public void eldestRemoved(Map.Entry<File, ImageLoader> eldest) {
|
||||
if (eldest.getKey() == lastBG)
|
||||
lastBG = null;
|
||||
ImageLoader imageLoader = eldest.getValue();
|
||||
imageLoader.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
/** The last background image loaded. */
|
||||
private static File lastBG;
|
||||
|
||||
/**
|
||||
* Clears the background image cache.
|
||||
* <p>
|
||||
|
@ -276,24 +274,55 @@ public class Beatmap implements Comparable<Beatmap> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Draws the beatmap background.
|
||||
* Loads the beatmap background image.
|
||||
*/
|
||||
public void loadBackground() {
|
||||
if (bg == null || bgImageCache.containsKey(bg) || !bg.isFile())
|
||||
return;
|
||||
|
||||
if (lastBG != null) {
|
||||
ImageLoader lastImageLoader = bgImageCache.get(lastBG);
|
||||
if (lastImageLoader != null && lastImageLoader.isLoading()) {
|
||||
lastImageLoader.interrupt(); // only allow loading one image at a time
|
||||
bgImageCache.remove(lastBG);
|
||||
}
|
||||
}
|
||||
ImageLoader imageLoader = new ImageLoader(bg);
|
||||
bgImageCache.put(bg, imageLoader);
|
||||
imageLoader.load(true);
|
||||
lastBG = bg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the beatmap background image is currently loading.
|
||||
* @return true if loading
|
||||
*/
|
||||
public boolean isBackgroundLoading() {
|
||||
if (bg == null)
|
||||
return false;
|
||||
ImageLoader imageLoader = bgImageCache.get(bg);
|
||||
return (imageLoader != null && imageLoader.isLoading());
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the beatmap background image.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
* @param alpha the alpha value
|
||||
* @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio
|
||||
* @return true if successful, false if any errors were produced
|
||||
*/
|
||||
public boolean drawBG(int width, int height, float alpha, boolean stretch) {
|
||||
public boolean drawBackground(int width, int height, float alpha, boolean stretch) {
|
||||
if (bg == null)
|
||||
return false;
|
||||
try {
|
||||
Image bgImage = bgImageCache.get(this);
|
||||
if (bgImage == null) {
|
||||
if (!bg.isFile())
|
||||
|
||||
ImageLoader imageLoader = bgImageCache.get(bg);
|
||||
if (imageLoader == null)
|
||||
return false;
|
||||
bgImage = new Image(bg.getAbsolutePath());
|
||||
bgImageCache.put(bg, bgImage);
|
||||
}
|
||||
|
||||
Image bgImage = imageLoader.getImage();
|
||||
if (bgImage == null)
|
||||
return true;
|
||||
|
||||
int swidth = width;
|
||||
int sheight = height;
|
||||
|
@ -311,14 +340,8 @@ public class Beatmap implements Comparable<Beatmap> {
|
|||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
}
|
||||
bgImage = bgImage.getScaledCopy(swidth, sheight);
|
||||
|
||||
bgImage.setAlpha(alpha);
|
||||
bgImage.drawCentered(width / 2, height / 2);
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to get background image '%s'.", bg), e);
|
||||
bg = null; // don't try to load the file again until a restart
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
179
src/itdelatrisu/opsu/beatmap/ImageLoader.java
Normal file
179
src/itdelatrisu/opsu/beatmap/ImageLoader.java
Normal file
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -305,7 +305,7 @@ public class Game extends BasicGameState {
|
|||
else
|
||||
dimLevel = 1f;
|
||||
}
|
||||
if (Options.isDefaultPlayfieldForced() || !beatmap.drawBG(width, height, dimLevel, false)) {
|
||||
if (Options.isDefaultPlayfieldForced() || !beatmap.drawBackground(width, height, dimLevel, false)) {
|
||||
Image playfield = GameImage.PLAYFIELD.getImage();
|
||||
playfield.setAlpha(dimLevel);
|
||||
playfield.draw();
|
||||
|
|
|
@ -106,7 +106,7 @@ public class GameRanking extends BasicGameState {
|
|||
Beatmap beatmap = MusicController.getBeatmap();
|
||||
|
||||
// background
|
||||
if (!beatmap.drawBG(width, height, 0.7f, true))
|
||||
if (!beatmap.drawBackground(width, height, 0.7f, true))
|
||||
GameImage.PLAYFIELD.getImage().draw(0,0);
|
||||
|
||||
// ranking screen elements
|
||||
|
|
|
@ -234,7 +234,7 @@ public class MainMenu extends BasicGameState {
|
|||
// draw background
|
||||
Beatmap beatmap = MusicController.getBeatmap();
|
||||
if (Options.isDynamicBackgroundEnabled() &&
|
||||
beatmap != null && beatmap.drawBG(width, height, bgAlpha.getValue(), true))
|
||||
beatmap != null && beatmap.drawBackground(width, height, bgAlpha.getValue(), true))
|
||||
;
|
||||
else {
|
||||
Image bg = GameImage.MENU_BG.getImage();
|
||||
|
@ -349,6 +349,8 @@ public class MainMenu extends BasicGameState {
|
|||
MusicController.toggleTrackDimmed(0.33f);
|
||||
|
||||
// fade in background
|
||||
Beatmap beatmap = MusicController.getBeatmap();
|
||||
if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
|
||||
bgAlpha.update(delta);
|
||||
|
||||
// buttons
|
||||
|
|
|
@ -216,6 +216,12 @@ public class SongMenu extends BasicGameState {
|
|||
/** Whether the song folder changed (notified via the watch service). */
|
||||
private boolean songFolderChanged = false;
|
||||
|
||||
/** The last background image. */
|
||||
private File lastBackgroundImage;
|
||||
|
||||
/** Background alpha level (for fade-in effect). */
|
||||
private AnimatedValue bgAlpha = new AnimatedValue(800, 0f, 1f, AnimationEquation.OUT_QUAD);
|
||||
|
||||
/**
|
||||
* Beatmaps whose difficulties were recently computed (if flag is non-null).
|
||||
* Unless the Boolean flag is null, then upon removal, the beatmap's objects will
|
||||
|
@ -344,7 +350,7 @@ public class SongMenu extends BasicGameState {
|
|||
// background
|
||||
if (focusNode != null) {
|
||||
Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
|
||||
if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true))
|
||||
if (!focusNodeBeatmap.drawBackground(width, height, bgAlpha.getValue(), true))
|
||||
GameImage.PLAYFIELD.getImage().draw();
|
||||
}
|
||||
|
||||
|
@ -545,6 +551,13 @@ public class SongMenu extends BasicGameState {
|
|||
}
|
||||
}
|
||||
|
||||
// fade in background
|
||||
if (focusNode != null) {
|
||||
Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
|
||||
if (!focusNodeBeatmap.isBackgroundLoading())
|
||||
bgAlpha.update(delta);
|
||||
}
|
||||
|
||||
// search
|
||||
search.setFocus(true);
|
||||
searchTimer += delta;
|
||||
|
@ -996,6 +1009,7 @@ public class SongMenu extends BasicGameState {
|
|||
beatmapMenuTimer = -1;
|
||||
searchTransitionTimer = SEARCH_TRANSITION_TIME;
|
||||
songInfo = null;
|
||||
bgAlpha.setTime(bgAlpha.getDuration());
|
||||
|
||||
// reset song stack
|
||||
randomStack = new Stack<SongNode>();
|
||||
|
@ -1253,6 +1267,14 @@ public class SongMenu extends BasicGameState {
|
|||
if (startNode.index == focusNode.index && startNode.beatmapIndex == -1)
|
||||
changeIndex(1);
|
||||
|
||||
// load background image
|
||||
beatmap.loadBackground();
|
||||
boolean isBgNull = lastBackgroundImage == null || beatmap.bg == null;
|
||||
if ((isBgNull && lastBackgroundImage != beatmap.bg) || (!isBgNull && !beatmap.bg.equals(lastBackgroundImage))) {
|
||||
bgAlpha.setTime(0);
|
||||
lastBackgroundImage = beatmap.bg;
|
||||
}
|
||||
|
||||
return oldFocus;
|
||||
}
|
||||
|
||||
|
@ -1352,6 +1374,7 @@ public class SongMenu extends BasicGameState {
|
|||
searchTimer = SEARCH_DELAY;
|
||||
searchTransitionTimer = SEARCH_TRANSITION_TIME;
|
||||
searchResultString = null;
|
||||
lastBackgroundImage = null;
|
||||
|
||||
// reload songs in new thread
|
||||
reloadThread = new Thread() {
|
||||
|
|
|
@ -77,6 +77,11 @@ public class AnimatedValue {
|
|||
updateValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total animation duration, in milliseconds.
|
||||
*/
|
||||
public int getDuration() { return duration; }
|
||||
|
||||
/**
|
||||
* Sets the animation duration.
|
||||
* @param duration the new duration, in milliseconds
|
||||
|
|
Loading…
Reference in New Issue
Block a user