| 1 | // tImageASTC.cpp  |
| 2 | //  |
| 3 | // This class knows how to load and save ARM's Adaptive Scalable Texture Compression (.astc) files. The pixel data is  |
| 4 | // stored in a tLayer. If decode was requested the layer will store raw pixel data. The layer may be 'stolen'. IF it  |
| 5 | // is the tImageASTC is invalid afterwards. This is purely for performance.  |
| 6 | //  |
| 7 | // Copyright (c) 2022-2024 Tristan Grimmer.  |
| 8 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
| 9 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
| 10 | //  |
| 11 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
| 12 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
| 13 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
| 14 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
| 15 | // PERFORMANCE OF THIS SOFTWARE.  |
| 16 |   |
| 17 | #include <System/tFile.h>  |
| 18 | #include <System/tMachine.h>  |
| 19 | #include "Image/tImageASTC.h"  |
| 20 | #include "Image/tPixelUtil.h"  |
| 21 | #include "Image/tPicture.h"  |
| 22 | #include "astcenc.h"  |
| 23 | namespace tImage  |
| 24 | {  |
| 25 |   |
| 26 |   |
| 27 | namespace tASTC  |
| 28 | {  |
| 29 | struct   |
| 30 | {  |
| 31 | uint8_t [4];  |
| 32 | uint8_t ;  |
| 33 | uint8_t ;  |
| 34 | uint8_t ;  |
| 35 | uint8_t [3];  |
| 36 | uint8_t [3];  |
| 37 | uint8_t [3];  |
| 38 | };  |
| 39 | tStaticAssert(sizeof(Header) == 16);  |
| 40 |   |
| 41 | tPixelFormat GetFormatFromBlockDimensions(int blockW, int blockH);  |
| 42 | };  |
| 43 |   |
| 44 |   |
| 45 | tPixelFormat tASTC::GetFormatFromBlockDimensions(int blockW, int blockH)  |
| 46 | {  |
| 47 | int pixelsPerBlock = blockW * blockH;  |
| 48 | switch (pixelsPerBlock)  |
| 49 | {  |
| 50 | case 16: return tPixelFormat::ASTC4X4;  |
| 51 | case 20: return tPixelFormat::ASTC5X4;  |
| 52 | case 25: return tPixelFormat::ASTC5X5;  |
| 53 | case 30: return tPixelFormat::ASTC6X5;  |
| 54 | case 36: return tPixelFormat::ASTC6X6;  |
| 55 | case 40: return tPixelFormat::ASTC8X5;  |
| 56 | case 48: return tPixelFormat::ASTC8X6;  |
| 57 | case 50: return tPixelFormat::ASTC10X5;  |
| 58 | case 60: return tPixelFormat::ASTC10X6;  |
| 59 | case 64: return tPixelFormat::ASTC8X8;  |
| 60 | case 80: return tPixelFormat::ASTC10X8;  |
| 61 | case 100: return tPixelFormat::ASTC10X10;  |
| 62 | case 120: return tPixelFormat::ASTC12X10;  |
| 63 | case 144: return tPixelFormat::ASTC12X12;  |
| 64 | }  |
| 65 | return tPixelFormat::Invalid;  |
| 66 | }  |
| 67 |   |
| 68 |   |
| 69 | bool tImageASTC::Load(const tString& astcFile, const LoadParams& params)  |
| 70 | {  |
| 71 | Clear();  |
| 72 |   |
| 73 | if (tSystem::tGetFileType(file: astcFile) != tSystem::tFileType::ASTC)  |
| 74 | return false;  |
| 75 |   |
| 76 | if (!tSystem::tFileExists(file: astcFile))  |
| 77 | return false;  |
| 78 |   |
| 79 | int numBytes = 0;  |
| 80 | uint8* astcFileInMemory = tSystem::tLoadFile(file: astcFile, buffer: nullptr, fileSize: &numBytes);  |
| 81 | bool success = Load(astcFileInMemory, numBytes, params);  |
| 82 | delete[] astcFileInMemory;  |
| 83 |   |
| 84 | return success;  |
| 85 | }  |
| 86 |   |
| 87 |   |
| 88 | bool tImageASTC::Load(const uint8* astcInMemory, int numBytes, const LoadParams& paramsIn)  |
| 89 | {  |
| 90 | Clear();  |
| 91 |   |
| 92 | // This will deal with zero-sized files properly as well. Basically we need  |
| 93 | // the header and at least one block of data. All ASTC blocks are 16 bytes.  |
| 94 | if (!astcInMemory || (numBytes < (sizeof(tASTC::Header)+16)))  |
| 95 | return false;  |
| 96 |   |
| 97 | LoadParams params(paramsIn);  |
| 98 | const uint8* astcCurr = astcInMemory;  |
| 99 | tASTC::Header& = *((tASTC::Header*)astcInMemory);  |
| 100 |   |
| 101 | // Check the magic.  |
| 102 | if ((header.Magic[0] != 0x13) || (header.Magic[1] != 0xAB) || (header.Magic[2] != 0xA1) || (header.Magic[3] != 0x5C))  |
| 103 | return false;  |
| 104 |   |
| 105 | int blockW = header.BlockW;  |
| 106 | int blockH = header.BlockH;  |
| 107 | int blockD = header.BlockD;  |
| 108 |   |
| 109 | // We only support block depth of 1 (2D blocks) -- for now.  |
| 110 | if (blockD != 1)  |
| 111 | return false;  |
| 112 |   |
| 113 | int width = header.DimX[0] + (header.DimX[1] << 8) + (header.DimX[2] << 16);  |
| 114 | int height = header.DimY[0] + (header.DimY[1] << 8) + (header.DimY[2] << 16);  |
| 115 | int depth = header.DimZ[0] + (header.DimZ[1] << 8) + (header.DimZ[2] << 16);  |
| 116 |   |
| 117 | // We only support depth of 1 (2D images) -- for now.  |
| 118 | if ((width <= 0) || (height <= 0) || (depth > 1))  |
| 119 | return false;  |
| 120 |   |
| 121 | tPixelFormat format = tASTC::GetFormatFromBlockDimensions(blockW, blockH);  |
| 122 | if (!tIsASTCFormat(format))  |
| 123 | return false;  |
| 124 | PixelFormat = format;  |
| 125 | PixelFormatSrc = format;  |
| 126 |   |
| 127 | const uint8* astcData = astcInMemory + sizeof(tASTC::Header);  |
| 128 | int astcDataSize = numBytes - sizeof(tASTC::Header);  |
| 129 | tAssert(!Layer);  |
| 130 |   |
| 131 | // Update the colour profiles.  |
| 132 | ColourProfileSrc = (params.Profile != tColourProfile::Unspecified) ? params.Profile : tColourProfile::sRGB;  |
| 133 | ColourProfile = (params.Profile != tColourProfile::Unspecified) ? params.Profile : tColourProfile::sRGB;  |
| 134 |   |
| 135 | // If we were not asked to decode we just get the data over to the Layer and we're done.  |
| 136 | if (!(params.Flags & LoadFlag_Decode))  |
| 137 | {  |
| 138 | Layer = new tLayer(PixelFormat, width, height, (uint8*)astcData);  |
| 139 | return true;  |
| 140 | }  |
| 141 |   |
| 142 | // The gamma-compression load flags only apply when decoding. If the gamma mode is auto, we determine here  |
| 143 | // whether to apply sRGB compression. If the space is linear and a format that often encodes colours, we apply it.  |
| 144 | if (params.Flags & LoadFlag_AutoGamma)  |
| 145 | {  |
| 146 | // Clear all related flags.  |
| 147 | params.Flags &= ~(LoadFlag_AutoGamma | LoadFlag_SRGBCompression | LoadFlag_GammaCompression);  |
| 148 | if (tMath::tIsProfileLinearInRGB(profile: params.Profile))  |
| 149 | params.Flags |= LoadFlag_SRGBCompression;  |
| 150 | }  |
| 151 |   |
| 152 | tColour4f* decoded4f = nullptr;  |
| 153 | tColour4b* decoded4b = nullptr;  |
| 154 |   |
| 155 | // ASTC data is always decoded into a float buffer.  |
| 156 | DecodeResult result = tImage::DecodePixelData_ASTC(format, data: astcData, dataSize: astcDataSize, w: width, h: height, decoded4f);  |
| 157 | if (result != DecodeResult::Success)  |
| 158 | return false;  |
| 159 |   |
| 160 | // Apply any decode flags.  |
| 161 | tAssert(decoded4f);  |
| 162 | bool flagTone = (params.Flags & tImageASTC::LoadFlag_ToneMapExposure) ? true : false;  |
| 163 | bool flagSRGB = (params.Flags & tImageASTC::LoadFlag_SRGBCompression) ? true : false;  |
| 164 | bool flagGama = (params.Flags & tImageASTC::LoadFlag_GammaCompression)? true : false;  |
| 165 | if (flagTone || flagSRGB || flagGama)  |
| 166 | {  |
| 167 | for (int p = 0; p < width*height; p++)  |
| 168 | {  |
| 169 | tColour4f& colour = decoded4f[p];  |
| 170 | if (flagTone)  |
| 171 | colour.TonemapExposure(exposure: params.Exposure, chans: tCompBit_RGB);  |
| 172 | if (flagSRGB)  |
| 173 | colour.LinearToSRGB(chans: tCompBit_RGB);  |
| 174 | if (flagGama)  |
| 175 | colour.LinearToGamma(gamma: params.Gamma, chans: tCompBit_RGB);  |
| 176 | }  |
| 177 | }  |
| 178 |   |
| 179 | // We're decoded. Need to update the current colour profile.  |
| 180 | if (flagSRGB) ColourProfile = tColourProfile::sRGB;  |
| 181 | if (flagGama) ColourProfile = tColourProfile::gRGB;  |
| 182 |   |
| 183 | // Converts to RGBA32 into the decoded4b array. Cleans up the float buffer.  |
| 184 | tAssert(!decoded4b);  |
| 185 | decoded4b = new tColour4b[width*height];  |
| 186 | for (int p = 0; p < width*height; p++)  |
| 187 | decoded4b[p].Set(decoded4f[p]);  |
| 188 | delete[] decoded4f;  |
| 189 |   |
| 190 | // Give decoded4b to layer.  |
| 191 | tAssert(!Layer);  |
| 192 | Layer = new tLayer(tPixelFormat::R8G8B8A8, width, height, (uint8*)decoded4b, true);  |
| 193 | tAssert(Layer->OwnsData);  |
| 194 |   |
| 195 | // We've got one more chance to reverse the rows here (if we still need to) because we were asked to decode.  |
| 196 | if (params.Flags & tImageASTC::LoadFlag_ReverseRowOrder)  |
| 197 | {  |
| 198 | // This shouldn't ever fail. Too easy to reverse RGBA 32-bit.  |
| 199 | uint8* reversedRowData = tImage::CreateReversedRowData(pixelData: Layer->Data, pixelDataFormat: Layer->PixelFormat, numBlocksW: width, numBlocksH: height);  |
| 200 | tAssert(reversedRowData);  |
| 201 | delete[] Layer->Data;  |
| 202 | Layer->Data = reversedRowData;  |
| 203 | }  |
| 204 |   |
| 205 | // Update the current pixel format -- but not the source format.  |
| 206 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 207 |   |
| 208 | tAssert(IsValid());  |
| 209 | return true;  |
| 210 | }  |
| 211 |   |
| 212 |   |
| 213 | bool tImageASTC::Set(tPixel4b* pixels, int width, int height, bool steal)  |
| 214 | {  |
| 215 | Clear();  |
| 216 | if (!pixels || (width <= 0) || (height <= 0))  |
| 217 | return false;  |
| 218 |   |
| 219 | Layer = new tLayer(tPixelFormat::R8G8B8A8, width, height, (uint8*)pixels, steal);  |
| 220 |   |
| 221 | PixelFormatSrc = tPixelFormat::R8G8B8A8;  |
| 222 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 223 | ColourProfileSrc = tColourProfile::sRGB; // We assume pixels must be sRGB.  |
| 224 | ColourProfile = tColourProfile::sRGB;  |
| 225 |   |
| 226 | return true;  |
| 227 | }  |
| 228 |   |
| 229 |   |
| 230 | bool tImageASTC::Set(tFrame* frame, bool steal)  |
| 231 | {  |
| 232 | Clear();  |
| 233 | if (!frame || !frame->IsValid())  |
| 234 | return false;  |
| 235 |   |
| 236 | PixelFormatSrc = frame->PixelFormatSrc;  |
| 237 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 238 | ColourProfileSrc = tColourProfile::sRGB; // We assume frame must be sRGB.  |
| 239 | ColourProfile = tColourProfile::sRGB;  |
| 240 |   |
| 241 | tPixel4b* pixels = frame->GetPixels(steal);  |
| 242 | Set(pixels, width: frame->Width, height: frame->Height, steal);  |
| 243 | if (steal)  |
| 244 | delete frame;  |
| 245 |   |
| 246 | return true;  |
| 247 | }  |
| 248 |   |
| 249 |   |
| 250 | bool tImageASTC::Set(tPicture& picture, bool steal)  |
| 251 | {  |
| 252 | Clear();  |
| 253 | if (!picture.IsValid())  |
| 254 | return false;  |
| 255 |   |
| 256 | PixelFormatSrc = picture.PixelFormatSrc;  |
| 257 | PixelFormat = tPixelFormat::R8G8B8A8;  |
| 258 | // We don't know colour profile of tPicture.  |
| 259 |   |
| 260 | // This is worth some explanation. If steal is true the picture becomes invalid and the  |
| 261 | // 'set' call will steal the stolen pixels. If steal is false GetPixels is called and the  |
| 262 | // 'set' call will memcpy them out... which makes sure the picture is still valid after and  |
| 263 | // no-one is sharing the pixel buffer. We don't check the success of 'set' because it must  |
| 264 | // succeed if picture was valid.  |
| 265 | tPixel4b* pixels = steal ? picture.StealPixels() : picture.GetPixels();  |
| 266 | bool success = Set(pixels, width: picture.GetWidth(), height: picture.GetHeight(), steal);  |
| 267 | tAssert(success);  |
| 268 | return true;  |
| 269 | }  |
| 270 |   |
| 271 |   |
| 272 | tFrame* tImageASTC::GetFrame(bool steal)  |
| 273 | {  |
| 274 | // Data must be decoded for this to work.  |
| 275 | if (!IsValid() || (PixelFormat != tPixelFormat::R8G8B8A8))  |
| 276 | return nullptr;  |
| 277 |   |
| 278 | tFrame* frame = new tFrame();  |
| 279 | frame->Width = Layer->Width;  |
| 280 | frame->Height = Layer->Height;  |
| 281 | frame->PixelFormatSrc = PixelFormatSrc;  |
| 282 |   |
| 283 | if (steal)  |
| 284 | {  |
| 285 | frame->Pixels = (tPixel4b*)Layer->StealData();  |
| 286 | delete Layer;  |
| 287 | Layer = nullptr;  |
| 288 | }  |
| 289 | else  |
| 290 | {  |
| 291 | frame->Pixels = new tPixel4b[frame->Width * frame->Height];  |
| 292 | tStd::tMemcpy(dest: frame->Pixels, src: (tPixel4b*)Layer->Data, numBytes: frame->Width * frame->Height * sizeof(tPixel4b));  |
| 293 | }  |
| 294 |   |
| 295 | return frame;  |
| 296 | }  |
| 297 |   |
| 298 |   |
| 299 | bool tImageASTC::IsOpaque() const  |
| 300 | {  |
| 301 | if (!IsValid())  |
| 302 | return false;  |
| 303 |   |
| 304 | if (Layer->PixelFormat == tPixelFormat::R8G8B8A8)  |
| 305 | {  |
| 306 | tPixel4b* pixels = (tPixel4b*)Layer->Data;  |
| 307 | for (int p = 0; p < (Layer->Width * Layer->Height); p++)  |
| 308 | {  |
| 309 | if (pixels[p].A < 255)  |
| 310 | return false;  |
| 311 | }  |
| 312 | }  |
| 313 |   |
| 314 | return false;  |
| 315 | }  |
| 316 |   |
| 317 |   |
| 318 | }  |
| 319 | |