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