/* * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javafx.sg.prism; import com.sun.javafx.geom.Rectangle; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import com.sun.javafx.logging.PulseLogger; import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGER; import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; import com.sun.prism.RTTexture; import com.sun.prism.ResourceFactory; import com.sun.prism.Texture; import com.sun.prism.impl.packrect.RectanglePacker; import com.sun.prism.impl.packrect.TexturesPacker; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.IntBuffer; import java.util.logging.Level; import java.util.logging.Logger; import javax.imageio.ImageIO; import sun.awt.image.IntegerComponentRaster; /** * RegionImageCache - A fixed pixel count sized cache of Images keyed by arbitrary set of arguments. All images are held with * SoftReferences so they will be dropped by the GC if heap memory gets tight. When our size hits max pixel count least * recently requested images are removed first. * */ class RegionImageCache { // Ordered Map keyed by args hash, ordered by most recent accessed entry. private final LinkedHashMap map = new LinkedHashMap(16, 0.75f, true); // Maximum number of pixels to cache, this is used if maxCount private final int maxPixelCount; // Maximum cached image size in pixels private final int maxSingleImagePixelSize; // The current number of pixels stored in the cache private int currentPixelCount = 0; // Lock for concurrent access to map private ReadWriteLock lock = new ReentrantReadWriteLock(); // Reference queue for tracking lost soft references to images in the cache private ReferenceQueue referenceQueue = new ReferenceQueue(); RegionImageCache() { this.maxPixelCount = (8 * 1024 * 1024) / 4; // 8Mb of pixels this.maxSingleImagePixelSize = 300 * 300; } /** Clear the cache */ void flush() { lock.readLock().lock(); try { map.clear(); } finally { lock.readLock().unlock(); } } /** * Check if the image size is to big to be stored in the cache * * @param w The image width * @param h The image height * @return True if the image size is less than max */ boolean isImageCachable(int w, int h) { return w > 0 && h > 0 && (w * h) < maxSingleImagePixelSize; } /** * Get the cached image for given keys * * @param config The Screen used with the target Graphics. Used as part of cache key * @param rect {@code rect.width} and {@code rect.height} are image width and height, * used as part of cache key; x and y are set inside this method to * indicate part of the returning texture occupied by this image * @param args Other arguments to use as part of the cache key * @return Returns the cached Image, or null there is no cached image for key */ RTTexture getImage(Object config, Rectangle rect, Object... args) { lock.readLock().lock(); try { int w = rect.width; int h = rect.height; final int hash = hash(config, w, h, args); PixelCountSoftReference ref = map.get(hash); // Check reference has not been lost and the key truly matches, in case of false positive hash match if (ref != null && ref.equals(config, w, h, args)) { RTTexture image = ref.get(); // If the image surface is lost, then we will remove it from the cache and report back // to the caller that we have no cached image. if (image != null && image.isSurfaceLost()) { if (PulseLogger.PULSE_LOGGING_ENABLED) { PulseLogger.PULSE_LOGGER.renderIncrementCounter("Region RTTexture surface lost"); } currentPixelCount -= ref.pixelCount; map.remove(hash); return null; } rect.x = ref.x; rect.y = ref.y; return ref.get(); } else { return null; } } finally { lock.readLock().unlock(); } } /** * Sets the cached image for the specified constraints. * * @param image Texture with the image from the cache * @param config The Screen used with the target Graphics. Used as part of cache key * @param rect Position and size of the image in the texture, * width and height used as part of cache key * @param args Other arguments to use as part of the cache key * @return true if the image could be cached or false if the image is too big */ private boolean setImage(RTTexture image, Object config, Rectangle rect, Object... args) { int w = rect.width; int h = rect.height; if (!isImageCachable(w, h)) return false; int hash = hash(config, w, h, args); lock.writeLock().lock(); try { PixelCountSoftReference ref = map.get(hash); // check if currently in map if (ref != null && ref.get() == image) { return true; } // clear out old if (ref != null) { if (PulseLogger.PULSE_LOGGING_ENABLED) { PulseLogger.PULSE_LOGGER.renderIncrementCounter("Region RTTexture replaced in RegionImageCache"); } currentPixelCount -= ref.pixelCount; map.remove(hash); } // add new image to pixel count int newPixelCount = w * h; currentPixelCount += newPixelCount; // clean out lost references if not enough space if (currentPixelCount > maxPixelCount) { while ((ref = (PixelCountSoftReference)referenceQueue.poll()) != null){ //reference lost map.remove(ref.hash); currentPixelCount -= ref.pixelCount; } } // remove old items till there is enough free space if (currentPixelCount > maxPixelCount) { Iterator> mapIter = map.entrySet().iterator(); while ((currentPixelCount > maxPixelCount) && mapIter.hasNext()) { Map.Entry entry = mapIter.next(); mapIter.remove(); RTTexture img = entry.getValue().get(); // if (img != null) img.flush(); currentPixelCount -= entry.getValue().pixelCount; } } // finally put new in map map.put(hash, new PixelCountSoftReference(image, referenceQueue, newPixelCount,hash, config, rect.x, rect.y, w, h, args)); return true; } finally { lock.writeLock().unlock(); } } /** Create a unique hash from all the input */ private int hash(Object config, int w, int h, Object ... args) { int hash; hash = (config != null ? config.hashCode() : 0); hash = 31 * hash + w; hash = 31 * hash + h; hash = 31 * hash + Arrays.deepHashCode(args); return hash; } /** Extended SoftReference that stores the pixel count even after the image is lost */ private static class PixelCountSoftReference extends SoftReference { private final int pixelCount; private final int hash; // key parts private final Object config; private final int x; private final int y; private final int w; private final int h; private final Object[] args; public PixelCountSoftReference(RTTexture referent, ReferenceQueue q, int pixelCount, int hash, Object config, int x, int y, int w, int h, Object[] args) { super(referent, q); this.pixelCount = pixelCount; this.hash = hash; this.config = config; this.x = x; this.y = y; this.w = w; this.h = h; this.args = args; } public boolean equals (Object config, int w, int h, Object[] args){ return config == this.config && w == this.w && h == this.h && Arrays.equals(args, this.args); } } // Packing cache part // REMIND: For a less powerful device, the size of this cache // is likely something we'd want to tune as they may have much less // VRAM and are less likely to be used for apps that have huge // text demands. // 2048 pixels introduced very noticeable pauses when trying // to free 1/4 of the glyphs, which for spiral text also amounts // to 1/4 of the strikes. private static final int WIDTH = 1024; // in pixels private static final int HEIGHT = 1024; // in pixels private static final int H_HEIGHT = 256; private static final int V_HEIGHT = HEIGHT - H_HEIGHT; private static final int H_MAX_HEIGHT = 16; private static final int V_MAX_WIDTH = 24; private static final boolean dontPack = Boolean.getBoolean("prism.disableRegionCachePacking"); private RTTexture packedTexture; private TexturesPacker vPacker, hPacker; public void dump() { System.err.print("Dumping " + dumpIndex + "..."); packedTexture.lock(); try { int iw = (int) WIDTH; int ih = (int) HEIGHT; BufferedImage bimg = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB_PRE); IntegerComponentRaster icr = (IntegerComponentRaster) bimg.getRaster(); IntBuffer buf = IntBuffer.wrap(icr.getDataStorage()); packedTexture.readPixels(buf); ImageIO.write(bimg, "png", new File("dump" + dumpIndex + ".png")); System.err.println(" succesful"); } catch (IOException ex) { System.err.println(" failed"); Logger.getLogger(RegionImageCache.class.getName()).log(Level.SEVERE, null, ex); } finally { packedTexture.unlock(); dumpIndex++; } } public boolean canBeCached(int w, int h) { return !(dontPack || w > WIDTH || h > V_HEIGHT); } /** * Returns texture and coordinates where the image of given size to be rendered. * This is used as part of texture packing algorithm so the returned texture * already contains some images and it is important to render the image * only to the area specified by {@code rect}. * * @param resourceFactory resource factory to be used to create new texture * @param config The Screen used with the target Graphics. Used as part of cache key * @param rect {@code rect.width} and {@code rect.height} are image width and height, * used as part of cache key; x and y are set inside this method to * indicate part of the returning texture occupied by this image * @param args Other arguments to use as part of the cache key * @return texture where the image of given size to be rendered */ RTTexture cache(ResourceFactory resourceFactory, Object config, Rectangle rect, Object... args) { lock.writeLock().lock(); try { int w = rect.width; int h = rect.height; if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("RegionImageCache.cache() method invoked"); if (dontPack || w > WIDTH || h > V_HEIGHT) { if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("RegionImageCache: Packed Region Image Cache not used for big texture"); RTTexture cached = resourceFactory.createRTTexture(w, h, Texture.WrapMode.CLAMP_TO_ZERO); cached.contentsUseful(); rect.x = 0; rect.y = 0; setImage(cached, config, rect, args); return cached; } if (vPacker == null) { createCache(resourceFactory); } // pack vertically final boolean packVertically = !(h <= H_MAX_HEIGHT && w > V_MAX_WIDTH); // final boolean packVertically = h > V_MAX_HEIGHT || w < V_MAX_WIDTH || w < h; final TexturesPacker packer; if (packVertically) { packer = vPacker; rect.width = h; rect.height = w; } else { packer = hPacker; } if (!packer.add(rect)) { if (PULSE_LOGGING_ENABLED) { PULSE_LOGGER.renderMessage(String.format( "RegionImageCache: Swithing onto next packed cache " + "image as %dx%d doesn't fit\n", w, h)); PULSE_LOGGER.renderIncrementCounter("RegionImageCache: Packed Region Image Cache done"); } // If add fails, create new cache createCache(resourceFactory); packer.add(rect); } if (packVertically) { final int tmp = rect.x; rect.x = rect.y; rect.y = tmp; rect.width = w; rect.height = h; } else { rect.y += V_HEIGHT; } setImage(packedTexture, config, rect, args); if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("RegionImageCache: New image added to packed Region Image Cache texture"); packedTexture.lock(); return packedTexture; } finally { lock.writeLock().unlock(); } } private int dumpIndex; private void createCache(ResourceFactory resourceFactory) { if (packedTexture != null) { dump(); // TODO: Remove this } packedTexture = resourceFactory.createRTTexture(WIDTH, HEIGHT, Texture.WrapMode.CLAMP_TO_ZERO); packedTexture.contentsUseful(); packedTexture.unlock(); // width and height are swaped for vPacker vPacker = new TexturesPacker(packedTexture, V_HEIGHT, WIDTH); hPacker = new TexturesPacker(packedTexture, WIDTH, H_HEIGHT); if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.renderIncrementCounter("RegionImageCache: Packed Region Image Cache created"); } }