Source: cache/GpuResourceCache.js

/*
 * Copyright 2015-2017 WorldWind Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/**
 * @exports GpuResourceCache
 */
define([
        '../util/AbsentResourceList',
        '../error/ArgumentError',
        '../util/ImageSource',
        '../util/Logger',
        '../cache/MemoryCache',
        '../render/Texture'
    ],
    function (AbsentResourceList,
              ArgumentError,
              ImageSource,
              Logger,
              MemoryCache,
              Texture) {
        "use strict";

        /**
         * Constructs a GPU resource cache for a specified size and low-water value.
         * @alias GpuResourceCache
         * @constructor
         * @classdesc Maintains a cache of GPU resources such as textures and GLSL programs.
         * Applications typically do not interact with this class unless they create their own shapes.
         * @param {Number} capacity The cache capacity, in bytes.
         * @param {Number} lowWater The number of bytes to clear the cache to when it exceeds its capacity.
         * @throws {ArgumentError} If the specified capacity is undefined, 0 or negative or the low-water value is
         * undefined, negative or not less than the capacity.
         */
        var GpuResourceCache = function (capacity, lowWater) {
            if (!capacity || capacity < 1) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "constructor",
                        "Specified cache capacity is undefined, 0 or negative."));
            }

            if (!lowWater || lowWater < 0 || lowWater >= capacity) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "constructor",
                        "Specified cache low-water value is undefined, negative or not less than the capacity."));
            }

            // Private. Holds the actual cache entries.
            this.entries = new MemoryCache(capacity, lowWater);

            // Private. Counter for generating cache keys.
            this.cacheKeyPool = 0;

            // Private. List of retrievals currently in progress.
            this.currentRetrievals = {};

            // Private. Identifies requested resources that whose retrieval failed.
            this.absentResourceList = new AbsentResourceList(3, 60e3);
        };

        Object.defineProperties(GpuResourceCache.prototype, {
            /**
             * Indicates the capacity of this cache in bytes.
             * @type {Number}
             * @readonly
             * @memberof GpuResourceCache.prototype
             */
            capacity: {
                get: function () {
                    return this.entries.capacity;
                }
            },

            /**
             * Indicates the low-water value for this cache in bytes, the size this cache is cleared to when it
             * exceeds its capacity.
             * @type {Number}
             * @readonly
             * @memberof GpuResourceCache.prototype
             */
            lowWater: {
                get: function () {
                    return this.entries.lowWater;
                }
            },

            /**
             * Indicates the number of bytes currently used by this cache.
             * @type {Number}
             * @readonly
             * @memberof GpuResourceCache.prototype
             */
            usedCapacity: {
                get: function () {
                    return this.entries.usedCapacity;
                }
            },

            /**
             * Indicates the number of free bytes in this cache.
             * @type {Number}
             * @readonly
             * @memberof GpuResourceCache.prototype
             */
            freeCapacity: {
                get: function () {
                    return this.entries.freeCapacity;
                }
            }
        });

        /**
         * Creates a cache key unique to this cache, typically for a resource about to be added to this cache.
         * @returns {String} The generated cache key.
         */
        GpuResourceCache.prototype.generateCacheKey = function () {
            return "GpuResourceCache " + ++this.cacheKeyPool;
        };

        /**
         * Adds a specified resource to this cache. Replaces the existing resource for the specified key if the
         * cache currently contains a resource for that key.
         * @param {String|ImageSource} key The key or image source of the resource to add.
         * @param {Object} resource The resource to add to the cache.
         * @param {Number} size The resource's size in bytes. Must be greater than 0.
         * @throws {ArgumentError} If either the key or resource arguments is null or undefined
         * or if the specified size is less than 1.
         */
        GpuResourceCache.prototype.putResource = function (key, resource, size) {
            if (!key) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "putResource", "missingKey."));
            }

            if (!resource) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "putResource", "missingResource."));
            }

            if (!size || size < 1) {
                throw new ArgumentError(
                    Logger.logMessage(Logger.LEVEL_SEVERE, "GpuResourceCache", "putResource",
                        "The specified resource size is undefined or less than 1."));
            }

            var entry = {
                resource: resource
            };

            this.entries.putEntry(key instanceof ImageSource ? key.key : key, entry, size);
        };

        /**
         * Returns the resource associated with a specified key.
         * @param {String|ImageSource} key The key or image source of the resource to find.
         * @returns {Object} The resource associated with the specified key, or null if the resource is not in
         * this cache or the specified key is null or undefined.
         */
        GpuResourceCache.prototype.resourceForKey = function (key) {
            var entry = (key instanceof ImageSource)
                ? this.entries.entryForKey(key.key) : this.entries.entryForKey(key);

            var resource = entry ? entry.resource : null;

            // This is faster than checking if the resource is a texture using instanceof.
            if (resource !== null && typeof resource.clearTexParameters === "function") {
                resource.clearTexParameters();
            }

            return resource;
        };
        
        /**
         * Sets a resource's aging factor (multiplier).
         * @param {String} key The key of the resource to modify. If null or undefined, the resource's cache entry is not modified.
         * @param {Number} agingFactor A multiplier applied to the age of the resource.
         */
        GpuResourceCache.prototype.setResourceAgingFactor = function (key, agingFactor) {
            this.entries.setEntryAgingFactor(key, agingFactor);
        };

        /**
         * Indicates whether a specified resource is in this cache.
         * @param {String|ImageSource} key The key or image source of the resource to find.
         * @returns {Boolean} true If the resource is in this cache, false if the resource
         * is not in this cache or the specified key is null or undefined.
         */
        GpuResourceCache.prototype.containsResource = function (key) {
            return this.entries.containsKey(key instanceof ImageSource ? key.key : key);
        };

        /**
         * Removes the specified resource from this cache. The cache is not modified if the specified key is null or
         * undefined or does not correspond to an entry in the cache.
         * @param {String|ImageSource} key The key or image source of the resource to remove.
         */
        GpuResourceCache.prototype.removeResource = function (key) {
            this.entries.removeEntry(key instanceof ImageSource ? key.key : key);
        };

        /**
         * Removes all resources from this cache.
         */
        GpuResourceCache.prototype.clear = function () {
            this.entries.clear(false);
        };

        /**
         * Retrieves an image and adds it to this cache when it arrives. If the specified image source is a URL, a
         * retrieval request for the image is made and this method returns immediately with a value of null. A redraw
         * event is generated when the image subsequently arrives and is added to this cache. If the image source is an
         * {@link ImageSource}, the image is used immediately and this method returns the {@link Texture} created and
         * cached for the image. No redraw event is generated in this case.
         * @param {WebGLRenderingContext} gl The current WebGL context.
         * @param {String|ImageSource} imageSource The image source, either a {@link ImageSource} or a String
         * giving the URL of the image.
         * @param {GLenum} wrapMode Optional. Specifies the wrap mode of the texture. Defaults to gl.CLAMP_TO_EDGE
         * @returns {Texture} The {@link Texture} created for the image if the specified image source is an
         * {@link ImageSource}, otherwise null.
         */
        GpuResourceCache.prototype.retrieveTexture = function (gl, imageSource, wrapMode) {
            if (!imageSource) {
                return null;
            }

            if (imageSource instanceof ImageSource) {
                var t = new Texture(gl, imageSource.image, wrapMode);
                this.putResource(imageSource.key, t, t.size);
                return t;
            }

            if (this.currentRetrievals[imageSource] || this.absentResourceList.isResourceAbsent(imageSource)) {
                return null;
            }

            var cache = this,
                image = new Image();

            image.onload = function () {
                Logger.log(Logger.LEVEL_INFO, "Image retrieval succeeded: " + imageSource);

                var texture = new Texture(gl, image, wrapMode);

                cache.putResource(imageSource, texture, texture.size);

                delete cache.currentRetrievals[imageSource];
                cache.absentResourceList.unmarkResourceAbsent(imageSource);

                // Send an event to request a redraw.
                var e = document.createEvent('Event');
                e.initEvent(WorldWind.REDRAW_EVENT_TYPE, true, true);
                window.dispatchEvent(e);
            };

            image.onerror = function () {
                delete cache.currentRetrievals[imageSource];
                cache.absentResourceList.markResourceAbsent(imageSource);
                Logger.log(Logger.LEVEL_WARNING, "Image retrieval failed: " + imageSource);
            };

            this.currentRetrievals[imageSource] = imageSource;
            image.crossOrigin = 'anonymous';
            image.src = imageSource;

            return null;
        };

        return GpuResourceCache;
    });