/* MipzipLoader.java * * Copyright (c) 2003, : Tuomas J. Lukka * * This file is part of Gzz. * * Gzz is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Gzz 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 Lesser General * Public License for more details. * * You should have received a copy of the GNU Lesser General * Public License along with Gzz; if not, write to the Free * Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA * * */ /* * Written by : Tuomas J. Lukka */ package org.nongnu.libvob.gl; import org.nongnu.libvob.*; import org.nongnu.libvob.util.Background; import java.awt.Dimension; import java.io.*; import java.util.*; import java.util.zip.*; /** A loader for Mipzip files: files of zipped, * compressed mipmap levels. *

* RESPONSIBILITIES: Manage the combination of * a single texture and a mipzip file, creating a "virtualized" texture * of which better-quality versions can be loaded and discarded at will. */ public class MipzipLoader { public static boolean dbg = false; final static void pa(String s) { System.out.println(s); } /** I have a hunch that zlib might not be re-entrant * and that that would be causing our problems, * since loading a level synchronously * at the beginning mixed everything. */ private static Object zlibLock = new Object(); private int goalBaseLevel = -15; private int currentBaseLevel = 1000; /** The base level will never be set higher than this. * Apparently, the NV driver prefers to have more than one * mipmap level loaded in the beginning, possibly * because of the array shape? */ private int maxBaseLevel; private Background bg = null; private float priority = 0; /** The file in which the mipmaps are stored. */ private final File mipzipFile; /** The texture. */ private final GL.Texture tex; /** The compressed format. */ private final String texFormat; private final boolean compressedFormat; private int bitsPerTexel; /** The opened Zip file. At any given * time, may be null or open. * Use getZipFile() to get it inside these classes. */ private ZipFile zip; private final Level[] levels; /** Open (if not already open) and return the ZipFile. */ synchronized private ZipFile getZipFile() throws IOException { if(zip == null) zip = new ZipFile(mipzipFile); return zip; } synchronized private byte[] readEntry(String name) throws IOException { synchronized(zlibLock) { ZipFile f = getZipFile(); ZipEntry e = f.getEntry(name); byte[] loadedData = new byte[(int)e.getSize()]; InputStream i = f.getInputStream(e); int offs = 0; while(offs < loadedData.length) { int res = i.read(loadedData, offs, loadedData.length - offs); if(res < 0) throw new IOException("EOF"); offs += res; } i.close(); return loadedData; } } private static final int STATE_NONE = 1, STATE_DATALOADED = 2, STATE_INTEXTURE = 3; /** A single mipmap level. * Responsibilities: Know if loaded, load data, teximage data, discard. */ private class Level implements Runnable { /** Whether this level is loaded into * the texture. */ int state = STATE_NONE; /** The index of this level. */ int level; /** Sizes of the mipmap levels. */ Dimension size; /** The data loaded from the disk. */ byte[] loadedData; /** Load the data for this mipmap level * synchronously into memory. This method * does not cause TexImage to be called * and can therefore be called in any * thread. */ synchronized void loadData() throws IOException { if(dbg) pa("LoadData "+MipzipLoader.this+" "+level+ " "+state); if(state != STATE_NONE) return; if(loadedData == null) loadedData = readEntry(""+level); state = STATE_DATALOADED; } synchronized public void run() { texImage(); } /** Call TexImage and afterwards discard the loaded data. * Must be called in GL thread. */ synchronized void texImage() { if(dbg) pa("TexImage "+MipzipLoader.this+" "+level+ " "+state); if(state != STATE_DATALOADED) return; if(compressedFormat) tex.compressedTexImage(level, texFormat, size.width, size.height, 0, loadedData); else tex.texImage2D(level, "RGB", size.width, size.height, 0, texFormat, "BYTE", loadedData); state = STATE_INTEXTURE; loadedData = null; AbstractUpdateManager.doWhenIdle( r_runGL, priority); } /** Discard the texture level from the GL side. * Done by loading an 1x1 image in its place. * Hope this really works. */ synchronized void discard() { if(dbg) pa("Discard "+MipzipLoader.this+" "+level+ " "+state); switch(state) { case STATE_INTEXTURE: /* tex.loadNull2D(level, texFormat, 4, 2, 0, "RGB", "FLOAT"); */ case STATE_DATALOADED: loadedData = null; } state = STATE_NONE; } /** The texture level from the GL side was discarded. * Because discard() doesn't work on NV drivers, * do it this way. */ synchronized void wasDiscarded() { if(dbg) pa("Discarded "+MipzipLoader.this+" "+level+ " "+state); loadedData = null; state = STATE_NONE; } /** Create a level. * @param l The mipmap level index * @param s The size string from the comment in the file, e.g."64x128" */ Level(int l, String s) throws IOException { this.level = l; int i = s.indexOf('x'); if(i < 0) throw new IOException("Invalid size string"); int w = Integer.parseInt(s.substring(0,i)); int h = Integer.parseInt(s.substring(i+1)); size = new Dimension(w, h); } } /** Set the texture parameter "BASE_LEVEL" to the given value. */ synchronized private void setBaseLevel(int level) { if(level < 0) level = 0; if(level >= maxBaseLevel) level = maxBaseLevel; tex.setTexParameter("TEXTURE_2D", "TEXTURE_BASE_LEVEL", level); tex.setTexParameter("TEXTURE_2D", "TEXTURE_MIN_LOD", level); currentBaseLevel = level; } /** Load to the base level synchronously: no discards. * @param level The base level: the lowest-detail level to be loaded */ synchronized public void loadToBaseLevelSynch(int level) throws IOException { int cur = currentBaseLevel; setBaseLevel(level); if(currentBaseLevel > cur) { for(int j=0; j= maxBaseLevel) level = maxBaseLevel; if(level != goalBaseLevel || level != currentBaseLevel) { if(dbg) pa("MipzipLoader "+this+" goal "+level+ " now at "+currentBaseLevel); this.goalBaseLevel = level; this.bg = bg; this.priority = priority; bg.addTask(r_runBg, priority); } } private Runnable r_runBg = new Runnable() { public void run() { runBg(); } }; private Runnable r_runGL = new Runnable() { public void run() { runGL(); } }; synchronized private void runBg() { if(dbg) pa("MipzipLoader "+this+" runBg!!"); // See what data is missing and load it for(int i=levels.length-1; i >= goalBaseLevel && i >= 0; i--) { if(levels[i].state == STATE_NONE) { // Ah, data missing -> load now // and reschedule try { levels[i].loadData(); AbstractUpdateManager.doWhenIdle( levels[i], priority - 10 - .1f * i); priority += .05; bg.addTask(r_runBg, priority); } catch(IOException e) { pa("Exception while loading mipzip data "+e); e.printStackTrace(); } break; } } // Schedule runGL always AbstractUpdateManager.doWhenIdle( r_runGL, priority - 5); } // See if any too large mipmaps still here // If yes, discard and reschedule. // Then, set base level and return. synchronized private void runGL() { if(dbg) pa("MipzipLoader "+this+" runGL!! "+ goalBaseLevel); for(int i=0; i= goalBaseLevel && i >= 0; i--) { if(levels[i].state != STATE_INTEXTURE) break; } if(dbg) pa("MipzipLoader "+this+" firstNotIn!! "+ i); if(currentBaseLevel != i+1) { boolean wasGreater = currentBaseLevel > i+1; setBaseLevel(i+1); if(wasGreater) AbstractUpdateManager.chgAfter(100); return; } } /** Get the OpenGL texture for this MipzipLoader. * The only operations on the texture should be * drawing it, or setting the following OpenGL parameters: *

* Texture base level and max level and the actual texture * images should not be touched. */ public GL.Texture getTexture() { return tex; } /** Get the amount of memory currently used. * @return Memory, in bytes */ public int getMemory() { return getMemory(currentBaseLevel); } /** Get the amount of memory used if given level is loaded. * @return Memory, in bytes */ public int getMemory(int level) { if(level < 0) level = 0; if(level > levels.length-1) level = levels.length-1; int bytesForLevel = levels[level].size.width * levels[level].size.height * bitsPerTexel / 8; int totalBytes = (bytesForLevel * 4) / 3; return totalBytes; } /** Get the level that uses at most given amount of memory. * @return The level index. */ public int getLevelForBytes(int memory) { for(int i = 0; i