EDIT:
Don’t do this! It has some severe problems which can cause your program to grind into a halt after mapping a lot of files. See the below discussion for more details! I recommend using FileChannels with a temporary direct buffer, not to map them!
Hello.
Today I tried out something interesting. I was working on making it possible to source my streamable textures from multiple sources (asset files, texture packs and raw files in a directory) when I stumbled upon an interesting function. FileChannels allow you to get a MappedByteBuffer for a certain range of a file on your harddrive. This byte buffer is direct, meaning that it’s possible to pass it directly into OpenGL functions like glTexImage2D() and glBufferData(). For texture data, this can be pretty worthless. Most of the time the data is stored in some kind of image format (PNG or even JPG) which needs to be decompressed before it can be passed into glTexImage2D(), but for my streaming system this decompression was way too slow. My streamable texture files contain the raw image data compressed using S3TC or BPTC which I simply dump into glCompressedTexImage2D(). To test out the potential gains of using mapped files, I’ve developed a small test program which compares the CPU performance of 3 different ways of loading raw texture data from a file.
The first way of loading stuff is with old-school input streams. This requires a lot of copies, since FileInputStreams work with byte[]s, not ByteBuffers. We have to read the texture data into a byte[], copy it to a direct ByteBuffer and then pass it to glTexImage2D().
private static byte[] bytes = new byte[DATA_LENGTH];
private static ByteBuffer buffer = BufferUtils.createByteBuffer(DATA_LENGTH);
private static long loadStream() throws Exception {
long startTime = System.nanoTime();
FileInputStream fis = new FileInputStream(RAW_FILE);
int read = 0;
while(read < DATA_LENGTH){
int r = fis.read(bytes, read, DATA_LENGTH-read);
if(r == -1){
throw new IOException();
}
read += r;
}
buffer.put(bytes).flip();
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 512, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
fis.close();
return System.nanoTime() - startTime;
}
The second way is to use NIO and FileChannels. FileChannels work on ByteBuffers directly, so we can use a direct ByteBuffer from the start!
private static ByteBuffer buffer = BufferUtils.createByteBuffer(DATA_LENGTH);
private static long loadChannel() throws Exception{
long startTime = System.nanoTime();
FileInputStream fis = new FileInputStream(RAW_FILE);
FileChannel fc = fis.getChannel();
while(buffer.hasRemaining()){
fc.read(buffer);
}
buffer.flip();
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 512, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
fc.close();
fis.close();
return System.nanoTime() - startTime;
}
The last most awesome way is to use NIO and FileChannels to map part of the file as a MappedByteBuffer. This is just so magical and simple.
private static long loadMapped() throws Exception {
long startTime = System.nanoTime();
FileInputStream fis = new FileInputStream(RAW_FILE);
FileChannel fc = fis.getChannel();
MappedByteBuffer mbb = fc.map(MapMode.READ_ONLY, 0, fc.size());
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 512, 512, 0, GL_RGBA, GL_UNSIGNED_BYTE, mbb);
fc.close();
fis.close();
return System.nanoTime() - startTime;
}
As you can see, there’s some timing code in each function. The average time taken to do these operations over a few thousand runs in a loop (all reading the same file over and over again, so it’s not representative for IO performance, only CPU performance) is listed below:
Stream: 0.657057 ms
Channel: 0.207856 ms
Mapped: 0.169004 ms
So it’s not only simple as hell to do, it’s faster as well and doesn’t require any temporary memory!