Home | History | Annotate | Download | only in transforms
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.gltrace.state.transforms;
     18 
     19 import com.android.ide.eclipse.gltrace.FileUtils;
     20 import com.android.ide.eclipse.gltrace.GLEnum;
     21 import com.android.ide.eclipse.gltrace.state.GLStringProperty;
     22 import com.android.ide.eclipse.gltrace.state.IGLProperty;
     23 import com.google.common.io.Files;
     24 import com.google.common.primitives.UnsignedBytes;
     25 
     26 import java.awt.image.BufferedImage;
     27 import java.io.File;
     28 import java.io.IOException;
     29 import java.nio.ByteBuffer;
     30 import java.nio.FloatBuffer;
     31 
     32 import javax.imageio.ImageIO;
     33 
     34 /**
     35  * {@link TexImageTransform} transforms the state to reflect the effect of a
     36  * glTexImage2D or glTexSubImage2D GL call.
     37  */
     38 public class TexImageTransform implements IStateTransform {
     39     private static final String PNG_IMAGE_FORMAT = "PNG";
     40     private static final String TEXTURE_FILE_PREFIX = "tex";
     41     private static final String TEXTURE_FILE_SUFFIX = ".png";
     42 
     43     private final IGLPropertyAccessor mAccessor;
     44     private final File mTextureDataFile;
     45 
     46     private final int mxOffset;
     47     private final int myOffset;
     48     private final int mWidth;
     49     private final int mHeight;
     50 
     51     private String mOldValue;
     52     private String mNewValue;
     53     private GLEnum mFormat;
     54     private GLEnum mType;
     55 
     56     /**
     57      * Construct a texture image transformation.
     58      * @param accessor accessor to obtain the GL state variable to modify
     59      * @param textureData texture data passed in by the call. Could be null.
     60      * @param format format of the source texture data
     61      * @param xOffset x offset for the source data (used only in glTexSubImage2D)
     62      * @param yOffset y offset for the source data (used only in glTexSubImage2D)
     63      * @param width width of the texture
     64      * @param height height of the texture
     65      */
     66     public TexImageTransform(IGLPropertyAccessor accessor, File textureData, GLEnum format,
     67             GLEnum type, int xOffset, int yOffset, int width, int height) {
     68         mAccessor = accessor;
     69         mTextureDataFile = textureData;
     70         mFormat = format;
     71         mType = type;
     72 
     73         mxOffset = xOffset;
     74         myOffset = yOffset;
     75         mWidth = width;
     76         mHeight = height;
     77     }
     78 
     79     @Override
     80     public void apply(IGLProperty currentState) {
     81         assert mOldValue == null : "Transform cannot be applied multiple times"; //$NON-NLS-1$
     82 
     83         IGLProperty property = mAccessor.getProperty(currentState);
     84         if (!(property instanceof GLStringProperty)) {
     85             return;
     86         }
     87 
     88         GLStringProperty prop = (GLStringProperty) property;
     89         mOldValue = prop.getStringValue();
     90 
     91         // Applying texture transformations is a heavy weight process. So we perform
     92         // it only once and save the result in a temporary file. The property is actually
     93         // the path to the file.
     94         if (mNewValue == null) {
     95             try {
     96                 if (mOldValue == null) {
     97                     mNewValue = createTexture(mTextureDataFile, mWidth, mHeight);
     98                 } else {
     99                     mNewValue = updateTextureData(mOldValue, mTextureDataFile, mxOffset, myOffset,
    100                             mWidth, mHeight);
    101                 }
    102             } catch (IOException e) {
    103                 throw new RuntimeException(e);
    104             } catch (RuntimeException e) {
    105                 throw e;
    106             }
    107         }
    108 
    109         prop.setValue(mNewValue);
    110     }
    111 
    112     @Override
    113     public void revert(IGLProperty state) {
    114         if (mOldValue != null) {
    115             IGLProperty property = mAccessor.getProperty(state);
    116             property.setValue(mOldValue);
    117             mOldValue = null;
    118         }
    119     }
    120 
    121     @Override
    122     public IGLProperty getChangedProperty(IGLProperty state) {
    123         return mAccessor.getProperty(state);
    124     }
    125 
    126     /**
    127      * Creates a texture of provided width and height. If the texture data file is provided,
    128      * then the texture is initialized with the contents of that file, otherwise an empty
    129      * image is created.
    130      * @param textureDataFile path to texture data, could be null.
    131      * @param width width of texture
    132      * @param height height of texture
    133      * @return path to cached texture
    134      */
    135     private String createTexture(File textureDataFile, int width, int height) throws IOException {
    136         File f = FileUtils.createTempFile(TEXTURE_FILE_PREFIX, TEXTURE_FILE_SUFFIX);
    137 
    138         BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    139 
    140         if (textureDataFile != null) {
    141             byte[] initialData = Files.toByteArray(textureDataFile);
    142             img.getRaster().setDataElements(0, 0, width, height,
    143                     formatSourceData(initialData, width, height));
    144         }
    145 
    146         ImageIO.write(img, PNG_IMAGE_FORMAT, f);
    147 
    148         return f.getAbsolutePath();
    149     }
    150 
    151     /**
    152      * Update part of an existing texture.
    153      * @param currentImagePath current texture image.
    154      * @param textureDataFile new data to update the current texture with
    155      * @param xOffset x offset for the update region
    156      * @param yOffset y offset for the update region
    157      * @param width width of the update region
    158      * @param height height of the update region
    159      * @return path to the updated texture
    160      */
    161     private String updateTextureData(String currentImagePath, File textureDataFile,
    162             int xOffset, int yOffset, int width, int height) throws IOException {
    163         assert currentImagePath != null : "Attempt to update a null texture";
    164 
    165         if (textureDataFile == null) {
    166             // Do not perform any updates if we don't have the actual data.
    167             return currentImagePath;
    168         }
    169 
    170         File f = FileUtils.createTempFile(TEXTURE_FILE_PREFIX, TEXTURE_FILE_SUFFIX);
    171         BufferedImage image = null;
    172         image = ImageIO.read(new File(currentImagePath));
    173 
    174         byte[] subImageData = Files.toByteArray(textureDataFile);
    175         image.getRaster().setDataElements(xOffset, yOffset, width, height,
    176                 formatSourceData(subImageData, width, height));
    177         ImageIO.write(image, PNG_IMAGE_FORMAT, f);
    178 
    179         return f.getAbsolutePath();
    180     }
    181 
    182     private byte[] formatSourceData(byte[] subImageData, int width, int height) {
    183         if (mType != GLEnum.GL_UNSIGNED_BYTE) {
    184             subImageData = unpackData(subImageData, mType);
    185         }
    186 
    187         switch (mFormat) {
    188             case GL_RGBA:
    189                 // no conversions necessary
    190                 return subImageData;
    191             case GL_RGB:
    192                 return addAlphaChannel(subImageData, width, height);
    193             case GL_RED:
    194             case GL_GREEN:
    195             case GL_BLUE:
    196                 // GL_RED, GL_GREEN and GL_BLUE are all supposed to fill those respective
    197                 // channels, but we assume that the programmers intent was to use GL_ALPHA in order
    198                 // to overcome the issue that GL_ALPHA cannot be used with float data.
    199                 if (mType != GLEnum.GL_FLOAT) {
    200                     throw new RuntimeException();
    201                 } else {
    202                     // fall through - assume that it is GL_ALPHA
    203                 }
    204                 //$FALL-THROUGH$
    205             case GL_ALPHA:
    206                 return addRGBChannels(subImageData, width, height);
    207             case GL_LUMINANCE:
    208                 return createRGBAFromLuminance(subImageData, width, height);
    209             case GL_LUMINANCE_ALPHA:
    210                 return createRGBAFromLuminanceAlpha(subImageData, width, height);
    211             default:
    212                 throw new RuntimeException();
    213         }
    214     }
    215 
    216     private byte[] unpackData(byte[] data, GLEnum type) {
    217         switch (type) {
    218             case GL_UNSIGNED_BYTE:
    219                 return data;
    220             case GL_UNSIGNED_SHORT_4_4_4_4:
    221                 return convertShortToUnsigned(data, 0xf000, 12, 0x0f00, 8, 0x00f0, 4, 0x000f, 0,
    222                         true);
    223             case GL_UNSIGNED_SHORT_5_6_5:
    224                 return convertShortToUnsigned(data, 0xf800, 11, 0x07e0, 5, 0x001f, 0, 0, 0,
    225                         false);
    226             case GL_UNSIGNED_SHORT_5_5_5_1:
    227                 return convertShortToUnsigned(data, 0xf800, 11, 0x07c0, 6, 0x003e, 1, 0x1, 0,
    228                         true);
    229             case GL_FLOAT:
    230                 return convertFloatToUnsigned(data);
    231             default:
    232                 return data;
    233         }
    234     }
    235 
    236    private byte[] convertFloatToUnsigned(byte[] data) {
    237        byte[] unsignedData = new byte[data.length];
    238        ByteBuffer floatBuffer = ByteBuffer.wrap(data);
    239        for (int i = 0; i < data.length / 4; i++) {
    240            float v = floatBuffer.getFloat(i);
    241            byte alpha = (byte)(v * 255);
    242            unsignedData[i*4 + 3] = alpha;
    243        }
    244        return unsignedData;
    245    }
    246 
    247    private byte[] convertShortToUnsigned(byte[] shortData,
    248            int rmask, int rshift,
    249            int gmask, int gshift,
    250            int bmask, int bshift,
    251            int amask, int ashift,
    252            boolean includeAlpha) {
    253        int numChannels = includeAlpha ? 4 : 3;
    254        byte[] unsignedData = new byte[(shortData.length/2) * numChannels];
    255 
    256        for (int i = 0; i < (shortData.length / 2); i++) {
    257            int hi = UnsignedBytes.toInt(shortData[i*2 + 0]);
    258            int lo = UnsignedBytes.toInt(shortData[i*2 + 1]);
    259 
    260            int x = hi << 8 | lo;
    261 
    262            int r = (x & rmask) >>> rshift;
    263            int g = (x & gmask) >>> gshift;
    264            int b = (x & bmask) >>> bshift;
    265            int a = (x & amask) >>> ashift;
    266 
    267            unsignedData[i * numChannels + 0] = UnsignedBytes.checkedCast(r);
    268            unsignedData[i * numChannels + 1] = UnsignedBytes.checkedCast(g);
    269            unsignedData[i * numChannels + 2] = UnsignedBytes.checkedCast(b);
    270 
    271            if (includeAlpha) {
    272                unsignedData[i * numChannels + 3] = UnsignedBytes.checkedCast(a);
    273            }
    274        }
    275 
    276        return unsignedData;
    277    }
    278 
    279    private byte[] addAlphaChannel(byte[] sourceData, int width, int height) {
    280         assert sourceData.length == 3 * width * height; // should have R, G & B channels
    281 
    282         byte[] data = new byte[4 * width * height];
    283 
    284         for (int src = 0, dst = 0; src < sourceData.length; src += 3, dst += 4) {
    285             data[dst + 0] = sourceData[src + 0]; // copy R byte
    286             data[dst + 1] = sourceData[src + 1]; // copy G byte
    287             data[dst + 2] = sourceData[src + 2]; // copy B byte
    288             data[dst + 3] = 1; // add alpha = 1
    289         }
    290 
    291         return data;
    292     }
    293 
    294     private byte[] addRGBChannels(byte[] sourceData, int width, int height) {
    295         assert sourceData.length == width * height; // should have a single alpha channel
    296 
    297         byte[] data = new byte[4 * width * height];
    298 
    299         for (int src = 0, dst = 0; src < sourceData.length; src++, dst += 4) {
    300             data[dst + 0] = data[dst + 1] = data[dst + 2] = 0; // set R = G = B = 0
    301             data[dst + 3] = sourceData[src];                 // copy over alpha
    302         }
    303 
    304         return data;
    305     }
    306 
    307     private byte[] createRGBAFromLuminance(byte[] sourceData, int width, int height) {
    308         assert sourceData.length == width * height; // should have a single luminance channel
    309 
    310         byte[] data = new byte[4 * width * height];
    311 
    312         for (int src = 0, dst = 0; src < sourceData.length; src++, dst += 4) {
    313             int l = sourceData[src] * 3;
    314             if (l > 255) { // clamp to 255
    315                 l = 255;
    316             }
    317 
    318             data[dst + 0] = data[dst + 1] = data[dst + 2] = (byte) l; // set R = G = B = L * 3
    319             data[dst + 3] = 1;                                        // set alpha = 1
    320         }
    321 
    322         return data;
    323     }
    324 
    325     private byte[] createRGBAFromLuminanceAlpha(byte[] sourceData, int width, int height) {
    326         assert sourceData.length == 2 * width * height; // should have luminance & alpha channels
    327 
    328         byte[] data = new byte[4 * width * height];
    329 
    330         for (int src = 0, dst = 0; src < sourceData.length; src += 2, dst += 4) {
    331             int l = sourceData[src] * 3;
    332             if (l > 255) { // clamp to 255
    333                 l = 255;
    334             }
    335 
    336             data[dst + 0] = data[dst + 1] = data[dst + 2] = (byte) l; // set R = G = B = L * 3
    337             data[dst + 3] = sourceData[src + 1];                    // copy over alpha
    338         }
    339 
    340         return data;
    341     }
    342 }
    343