From 92f4a5176d09d82cfd481e4de0db9abbb2265185 Mon Sep 17 00:00:00 2001 From: Jeffrey Han Date: Tue, 9 Jun 2015 03:10:44 -0400 Subject: [PATCH] Switch to a LRU cache for beatmap background images. Created a separate BeatmapImageCache class to handle cache operations. The cache now uses File objects as keys, rather than Beatmap objects (which was buggy). Also renamed "OsuHitObjectResult" helper class to "HitObjectResult". Signed-off-by: Jeffrey Han --- src/itdelatrisu/opsu/Container.java | 2 +- src/itdelatrisu/opsu/GameData.java | 20 ++--- src/itdelatrisu/opsu/beatmap/Beatmap.java | 52 +++-------- .../opsu/beatmap/BeatmapImageCache.java | 86 +++++++++++++++++++ .../opsu/beatmap/BeatmapParser.java | 2 +- src/itdelatrisu/opsu/db/BeatmapDB.java | 4 +- 6 files changed, 113 insertions(+), 53 deletions(-) create mode 100644 src/itdelatrisu/opsu/beatmap/BeatmapImageCache.java diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java index 05cb3282..7e789160 100644 --- a/src/itdelatrisu/opsu/Container.java +++ b/src/itdelatrisu/opsu/Container.java @@ -124,7 +124,7 @@ public class Container extends AppGameContainer { // reset image references GameImage.clearReferences(); GameData.Grade.clearReferences(); - Beatmap.resetImageCache(); + Beatmap.getBackgroundImageCache().clear(); // prevent loading tracks from re-initializing OpenAL MusicController.reset(); diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index e0ef0ece..309533ed 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -191,7 +191,7 @@ public class GameData { private int[] hitResultOffset; /** List of hit result objects associated with hit objects. */ - private LinkedBlockingDeque hitResultList; + private LinkedBlockingDeque hitResultList; /** * Class to store hit error information. @@ -230,7 +230,7 @@ public class GameData { public enum HitObjectType { CIRCLE, SLIDERTICK, SLIDER_FIRST, SLIDER_LAST, SPINNER } /** Hit result helper class. */ - private class OsuHitObjectResult { + private class HitObjectResult { /** Object start time. */ public int time; @@ -265,7 +265,7 @@ public class GameData { * @param curve the slider curve (or null if not applicable) * @param expand whether or not the hit result animation should expand (if applicable) */ - public OsuHitObjectResult(int time, int result, float x, float y, Color color, + public HitObjectResult(int time, int result, float x, float y, Color color, HitObjectType hitResultType, Curve curve, boolean expand) { this.time = time; this.result = result; @@ -375,12 +375,12 @@ public class GameData { healthDisplay = 100f; hitResultCount = new int[HIT_MAX]; if (hitResultList != null) { - for (OsuHitObjectResult hitResult : hitResultList) { + for (HitObjectResult hitResult : hitResultList) { if (hitResult.curve != null) hitResult.curve.discardCache(); } } - hitResultList = new LinkedBlockingDeque(); + hitResultList = new LinkedBlockingDeque(); hitErrorList = new LinkedBlockingDeque(); fullObjectCount = 0; combo = 0; @@ -872,9 +872,9 @@ public class GameData { * @param trackPosition the current track position (in ms) */ public void drawHitResults(int trackPosition) { - Iterator iter = hitResultList.iterator(); + Iterator iter = hitResultList.iterator(); while (iter.hasNext()) { - OsuHitObjectResult hitResult = iter.next(); + HitObjectResult hitResult = iter.next(); if (hitResult.time + HITRESULT_TIME > trackPosition) { // spinner if (hitResult.hitResultType == HitObjectType.SPINNER && hitResult.result != HIT_MISS) { @@ -1217,7 +1217,7 @@ public class GameData { if (!Options.isPerfectHitBurstEnabled()) ; // hide perfect hit results else - hitResultList.add(new OsuHitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false)); + hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false)); } } @@ -1338,14 +1338,14 @@ public class GameData { else if (result == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) ; // "relax" and "autopilot" mods: hide misses else { - hitResultList.add(new OsuHitObjectResult(time, result, x, y, color, hitResultType, curve, expand)); + hitResultList.add(new HitObjectResult(time, result, x, y, color, hitResultType, curve, expand)); // sliders: add the other curve endpoint for the hit animation if (curve != null) { boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST); float[] p = curve.pointAt((isFirst) ? 1f : 0f); HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST; - hitResultList.add(new OsuHitObjectResult(time, result, p[0], p[1], color, type, null, expand)); + hitResultList.add(new HitObjectResult(time, result, p[0], p[1], color, type, null, expand)); } } } diff --git a/src/itdelatrisu/opsu/beatmap/Beatmap.java b/src/itdelatrisu/opsu/beatmap/Beatmap.java index 14847b23..ad8b5476 100644 --- a/src/itdelatrisu/opsu/beatmap/Beatmap.java +++ b/src/itdelatrisu/opsu/beatmap/Beatmap.java @@ -22,12 +22,10 @@ import itdelatrisu.opsu.Options; import java.io.File; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedList; import org.newdawn.slick.Color; import org.newdawn.slick.Image; -import org.newdawn.slick.SlickException; import org.newdawn.slick.util.Log; /** @@ -37,11 +35,13 @@ public class Beatmap implements Comparable { /** Game modes. */ public static final byte MODE_OSU = 0, MODE_TAIKO = 1, MODE_CTB = 2, MODE_MANIA = 3; - /** Map of all loaded background images. */ - private static HashMap bgImageMap = new HashMap(); + /** Background image cache. */ + private static final BeatmapImageCache bgImageCache = new BeatmapImageCache(); - /** Maximum number of cached images before all get erased. */ - private static final int MAX_CACHE_SIZE = 10; + /** + * Returns the background image cache. + */ + public static BeatmapImageCache getBackgroundImageCache() { return bgImageCache; } /** The OSU File object associated with this beatmap. */ private File file; @@ -156,11 +156,11 @@ public class Beatmap implements Comparable { * [Events] */ - /** Background image file name. */ - public String bg; + /** Background image file. */ + public File bg; - /** Background video file name. */ -// public String video; + /** Background video file. */ +// public File video; /** All break periods (start time, end time, ...). */ public ArrayList breaks; @@ -201,30 +201,6 @@ public class Beatmap implements Comparable { /** Last object end time (in ms). */ public int endTime = -1; - /** - * Destroys all cached background images and resets the cache. - */ - public static void clearImageCache() { - for (Image img : bgImageMap.values()) { - if (img != null && !img.isDestroyed()) { - try { - img.destroy(); - } catch (SlickException e) { - Log.warn(String.format("Failed to destroy image '%s'.", img.getResourceReference()), e); - } - } - } - resetImageCache(); - } - - /** - * Resets the image cache. - * This does NOT destroy images, so be careful of memory leaks! - */ - public static void resetImageCache() { - bgImageMap = new HashMap(); - } - /** * Constructor. * @param file the file associated with this beatmap @@ -287,12 +263,10 @@ public class Beatmap implements Comparable { if (bg == null) return false; try { - Image bgImage = bgImageMap.get(this); + Image bgImage = bgImageCache.get(this); if (bgImage == null) { - if (bgImageMap.size() > MAX_CACHE_SIZE) - clearImageCache(); - bgImage = new Image(new File(file.getParentFile(), bg).getAbsolutePath()); - bgImageMap.put(this, bgImage); + bgImage = new Image(bg.getAbsolutePath()); + bgImageCache.put(this, bgImage); } int swidth = width; diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapImageCache.java b/src/itdelatrisu/opsu/beatmap/BeatmapImageCache.java new file mode 100644 index 00000000..95e05c85 --- /dev/null +++ b/src/itdelatrisu/opsu/beatmap/BeatmapImageCache.java @@ -0,0 +1,86 @@ +/* + * 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 . + */ + +package itdelatrisu.opsu.beatmap; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.newdawn.slick.Image; +import org.newdawn.slick.SlickException; +import org.newdawn.slick.util.Log; + +/** + * LRU cache for beatmap background images. + */ +public class BeatmapImageCache { + /** Maximum number of cached images. */ + private static final int MAX_CACHE_SIZE = 10; + + /** Map of all loaded background images. */ + private LinkedHashMap cache; + + /** + * Constructor. + */ + @SuppressWarnings("serial") + public BeatmapImageCache() { + this.cache = new LinkedHashMap(MAX_CACHE_SIZE + 1, 1.1f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > MAX_CACHE_SIZE) { + // destroy the eldest image + Image img = eldest.getValue(); + if (img != null && !img.isDestroyed()) { + try { + img.destroy(); + } catch (SlickException e) { + Log.warn(String.format("Failed to destroy image '%s'.", img.getResourceReference()), e); + } + } + return true; + } + return false; + } + }; + } + + /** + * Returns the image mapped to the specified beatmap. + * @param beatmap the Beatmap + * @return the Image, or {@code null} if no such mapping exists + */ + public Image get(Beatmap beatmap) { return cache.get(beatmap.bg); } + + /** + * Creates a mapping from the specified beatmap to the given image. + * @param beatmap the Beatmap + * @param image the Image + * @return the previously mapped Image, or {@code null} if no such mapping existed + */ + public Image put(Beatmap beatmap, Image image) { return cache.put(beatmap.bg, image); } + + /** + * Removes all entries from the cache. + *

+ * NOTE: This does NOT destroy the images in the cache, and will cause + * memory leaks if all images have not been destroyed. + */ + public void clear() { cache.clear(); } +} diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java index 58a2100b..369ea19e 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapParser.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapParser.java @@ -440,7 +440,7 @@ public class BeatmapParser { tokens[2] = tokens[2].replaceAll("^\"|\"$", ""); String ext = BeatmapParser.getExtension(tokens[2]); if (ext.equals("jpg") || ext.equals("png")) - beatmap.bg = getDBString(tokens[2]); + beatmap.bg = new File(dir, getDBString(tokens[2])); break; case "2": // break periods try { diff --git a/src/itdelatrisu/opsu/db/BeatmapDB.java b/src/itdelatrisu/opsu/db/BeatmapDB.java index cb4b817d..fc7a160c 100644 --- a/src/itdelatrisu/opsu/db/BeatmapDB.java +++ b/src/itdelatrisu/opsu/db/BeatmapDB.java @@ -335,7 +335,7 @@ public class BeatmapDB { stmt.setBoolean(33, beatmap.letterboxInBreaks); stmt.setBoolean(34, beatmap.widescreenStoryboard); stmt.setBoolean(35, beatmap.epilepsyWarning); - stmt.setString(36, beatmap.bg); + stmt.setString(36, beatmap.bg.getName()); stmt.setString(37, beatmap.sliderBorderToString()); stmt.setString(38, beatmap.timingPointsToString()); stmt.setString(39, beatmap.breaksToString()); @@ -476,7 +476,7 @@ public class BeatmapDB { beatmap.letterboxInBreaks = rs.getBoolean(33); beatmap.widescreenStoryboard = rs.getBoolean(34); beatmap.epilepsyWarning = rs.getBoolean(35); - beatmap.bg = BeatmapParser.getDBString(rs.getString(36)); + beatmap.bg = new File(beatmap.getFile().getParentFile(), BeatmapParser.getDBString(rs.getString(36))); beatmap.sliderBorderFromString(rs.getString(37)); } catch (SQLException e) { throw e;