blob: dde89eae62b8b8b37d35d75848e9d9eaec9bced6 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* 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.
*/
package com.android.ide.eclipse.gltrace.state.transforms;
import com.android.ide.eclipse.gltrace.FileUtils;
import com.android.ide.eclipse.gltrace.GLEnum;
import com.android.ide.eclipse.gltrace.state.GLStringProperty;
import com.android.ide.eclipse.gltrace.state.IGLProperty;
import com.google.common.io.Files;
import com.google.common.primitives.UnsignedBytes;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import javax.imageio.ImageIO;
/**
* {@link TexImageTransform} transforms the state to reflect the effect of a
* glTexImage2D or glTexSubImage2D GL call.
*/
public class TexImageTransform implements IStateTransform {
private static final String PNG_IMAGE_FORMAT = "PNG";
private static final String TEXTURE_FILE_PREFIX = "tex";
private static final String TEXTURE_FILE_SUFFIX = ".png";
private final IGLPropertyAccessor mAccessor;
private final File mTextureDataFile;
private final int mxOffset;
private final int myOffset;
private final int mWidth;
private final int mHeight;
private String mOldValue;
private String mNewValue;
private GLEnum mFormat;
private GLEnum mType;
/**
* Construct a texture image transformation.
* @param accessor accessor to obtain the GL state variable to modify
* @param textureData texture data passed in by the call. Could be null.
* @param format format of the source texture data
* @param xOffset x offset for the source data (used only in glTexSubImage2D)
* @param yOffset y offset for the source data (used only in glTexSubImage2D)
* @param width width of the texture
* @param height height of the texture
*/
public TexImageTransform(IGLPropertyAccessor accessor, File textureData, GLEnum format,
GLEnum type, int xOffset, int yOffset, int width, int height) {
mAccessor = accessor;
mTextureDataFile = textureData;
mFormat = format;
mType = type;
mxOffset = xOffset;
myOffset = yOffset;
mWidth = width;
mHeight = height;
}
@Override
public void apply(IGLProperty currentState) {
assert mOldValue == null : "Transform cannot be applied multiple times"; //$NON-NLS-1$
IGLProperty property = mAccessor.getProperty(currentState);
if (!(property instanceof GLStringProperty)) {
return;
}
GLStringProperty prop = (GLStringProperty) property;
mOldValue = prop.getStringValue();
// Applying texture transformations is a heavy weight process. So we perform
// it only once and save the result in a temporary file. The property is actually
// the path to the file.
if (mNewValue == null) {
try {
if (mOldValue == null) {
mNewValue = createTexture(mTextureDataFile, mWidth, mHeight);
} else {
mNewValue = updateTextureData(mOldValue, mTextureDataFile, mxOffset, myOffset,
mWidth, mHeight);
}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (RuntimeException e) {
throw e;
}
}
prop.setValue(mNewValue);
}
@Override
public void revert(IGLProperty state) {
if (mOldValue != null) {
IGLProperty property = mAccessor.getProperty(state);
property.setValue(mOldValue);
mOldValue = null;
}
}
@Override
public IGLProperty getChangedProperty(IGLProperty state) {
return mAccessor.getProperty(state);
}
/**
* Creates a texture of provided width and height. If the texture data file is provided,
* then the texture is initialized with the contents of that file, otherwise an empty
* image is created.
* @param textureDataFile path to texture data, could be null.
* @param width width of texture
* @param height height of texture
* @return path to cached texture
*/
private String createTexture(File textureDataFile, int width, int height) throws IOException {
File f = FileUtils.createTempFile(TEXTURE_FILE_PREFIX, TEXTURE_FILE_SUFFIX);
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
if (textureDataFile != null) {
byte[] initialData = Files.toByteArray(textureDataFile);
img.getRaster().setDataElements(0, 0, width, height,
formatSourceData(initialData, width, height));
}
ImageIO.write(img, PNG_IMAGE_FORMAT, f);
return f.getAbsolutePath();
}
/**
* Update part of an existing texture.
* @param currentImagePath current texture image.
* @param textureDataFile new data to update the current texture with
* @param xOffset x offset for the update region
* @param yOffset y offset for the update region
* @param width width of the update region
* @param height height of the update region
* @return path to the updated texture
*/
private String updateTextureData(String currentImagePath, File textureDataFile,
int xOffset, int yOffset, int width, int height) throws IOException {
assert currentImagePath != null : "Attempt to update a null texture";
if (textureDataFile == null) {
// Do not perform any updates if we don't have the actual data.
return currentImagePath;
}
File f = FileUtils.createTempFile(TEXTURE_FILE_PREFIX, TEXTURE_FILE_SUFFIX);
BufferedImage image = null;
image = ImageIO.read(new File(currentImagePath));
byte[] subImageData = Files.toByteArray(textureDataFile);
image.getRaster().setDataElements(xOffset, yOffset, width, height,
formatSourceData(subImageData, width, height));
ImageIO.write(image, PNG_IMAGE_FORMAT, f);
return f.getAbsolutePath();
}
private byte[] formatSourceData(byte[] subImageData, int width, int height) {
if (mType != GLEnum.GL_UNSIGNED_BYTE) {
subImageData = unpackData(subImageData, mType);
}
switch (mFormat) {
case GL_RGBA:
// no conversions necessary
return subImageData;
case GL_RGB:
return addAlphaChannel(subImageData, width, height);
case GL_RED:
case GL_GREEN:
case GL_BLUE:
// GL_RED, GL_GREEN and GL_BLUE are all supposed to fill those respective
// channels, but we assume that the programmers intent was to use GL_ALPHA in order
// to overcome the issue that GL_ALPHA cannot be used with float data.
if (mType != GLEnum.GL_FLOAT) {
throw new RuntimeException();
} else {
// fall through - assume that it is GL_ALPHA
}
//$FALL-THROUGH$
case GL_ALPHA:
return addRGBChannels(subImageData, width, height);
case GL_LUMINANCE:
return createRGBAFromLuminance(subImageData, width, height);
case GL_LUMINANCE_ALPHA:
return createRGBAFromLuminanceAlpha(subImageData, width, height);
default:
throw new RuntimeException();
}
}
private byte[] unpackData(byte[] data, GLEnum type) {
switch (type) {
case GL_UNSIGNED_BYTE:
return data;
case GL_UNSIGNED_SHORT_4_4_4_4:
return convertShortToUnsigned(data, 0xf000, 12, 0x0f00, 8, 0x00f0, 4, 0x000f, 0,
true);
case GL_UNSIGNED_SHORT_5_6_5:
return convertShortToUnsigned(data, 0xf800, 11, 0x07e0, 5, 0x001f, 0, 0, 0,
false);
case GL_UNSIGNED_SHORT_5_5_5_1:
return convertShortToUnsigned(data, 0xf800, 11, 0x07c0, 6, 0x003e, 1, 0x1, 0,
true);
case GL_FLOAT:
return convertFloatToUnsigned(data);
default:
return data;
}
}
private byte[] convertFloatToUnsigned(byte[] data) {
byte[] unsignedData = new byte[data.length];
ByteBuffer floatBuffer = ByteBuffer.wrap(data);
for (int i = 0; i < data.length / 4; i++) {
float v = floatBuffer.getFloat(i);
byte alpha = (byte)(v * 255);
unsignedData[i*4 + 3] = alpha;
}
return unsignedData;
}
private byte[] convertShortToUnsigned(byte[] shortData,
int rmask, int rshift,
int gmask, int gshift,
int bmask, int bshift,
int amask, int ashift,
boolean includeAlpha) {
int numChannels = includeAlpha ? 4 : 3;
byte[] unsignedData = new byte[(shortData.length/2) * numChannels];
for (int i = 0; i < (shortData.length / 2); i++) {
int hi = UnsignedBytes.toInt(shortData[i*2 + 0]);
int lo = UnsignedBytes.toInt(shortData[i*2 + 1]);
int x = hi << 8 | lo;
int r = (x & rmask) >>> rshift;
int g = (x & gmask) >>> gshift;
int b = (x & bmask) >>> bshift;
int a = (x & amask) >>> ashift;
unsignedData[i * numChannels + 0] = UnsignedBytes.checkedCast(r);
unsignedData[i * numChannels + 1] = UnsignedBytes.checkedCast(g);
unsignedData[i * numChannels + 2] = UnsignedBytes.checkedCast(b);
if (includeAlpha) {
unsignedData[i * numChannels + 3] = UnsignedBytes.checkedCast(a);
}
}
return unsignedData;
}
private byte[] addAlphaChannel(byte[] sourceData, int width, int height) {
assert sourceData.length == 3 * width * height; // should have R, G & B channels
byte[] data = new byte[4 * width * height];
for (int src = 0, dst = 0; src < sourceData.length; src += 3, dst += 4) {
data[dst + 0] = sourceData[src + 0]; // copy R byte
data[dst + 1] = sourceData[src + 1]; // copy G byte
data[dst + 2] = sourceData[src + 2]; // copy B byte
data[dst + 3] = 1; // add alpha = 1
}
return data;
}
private byte[] addRGBChannels(byte[] sourceData, int width, int height) {
assert sourceData.length == width * height; // should have a single alpha channel
byte[] data = new byte[4 * width * height];
for (int src = 0, dst = 0; src < sourceData.length; src++, dst += 4) {
data[dst + 0] = data[dst + 1] = data[dst + 2] = 0; // set R = G = B = 0
data[dst + 3] = sourceData[src]; // copy over alpha
}
return data;
}
private byte[] createRGBAFromLuminance(byte[] sourceData, int width, int height) {
assert sourceData.length == width * height; // should have a single luminance channel
byte[] data = new byte[4 * width * height];
for (int src = 0, dst = 0; src < sourceData.length; src++, dst += 4) {
int l = sourceData[src] * 3;
if (l > 255) { // clamp to 255
l = 255;
}
data[dst + 0] = data[dst + 1] = data[dst + 2] = (byte) l; // set R = G = B = L * 3
data[dst + 3] = 1; // set alpha = 1
}
return data;
}
private byte[] createRGBAFromLuminanceAlpha(byte[] sourceData, int width, int height) {
assert sourceData.length == 2 * width * height; // should have luminance & alpha channels
byte[] data = new byte[4 * width * height];
for (int src = 0, dst = 0; src < sourceData.length; src += 2, dst += 4) {
int l = sourceData[src] * 3;
if (l > 255) { // clamp to 255
l = 255;
}
data[dst + 0] = data[dst + 1] = data[dst + 2] = (byte) l; // set R = G = B = L * 3
data[dst + 3] = sourceData[src + 1]; // copy over alpha
}
return data;
}
}