1// tTexture.cpp 
2// 
3// A tTexture is a 'hardware-ready' format. tTextures contain functionality for creating mipmap layers in a variety of 
4// block-compressed and uncompressed formats. A tTexture stores each mipmap layer in a tLayer. A tTexture can be 
5// created from either a tPicture or a dds file. The purpose of a dds file is so that content-creators have control 
6// over the authoring of each mipmap level and the exact pixel format used. Basically if you've created a dds file, 
7// you're saying you want the final hardware to use the image data unchanged and as authored -- same mip levels, same 
8// pixel format, same dimensions. For this reason, dds files should not be loaded into tPictures where image 
9// manipulation occurs and possibly lossy block-compressed dds images would be decompressed. A dds file may contain more 
10// than one image if it is a cubemap, but a tTexture only ever represents a single image. The tTexture dds constructor 
11// allows you to decide which one gets loaded. tTextures can save and load to a tChunk-based format, and are therefore 
12// useful at both pipeline and for runtime loading. To save to a tChunk file format a tTexture will call the Save 
13// method of all the tLayers. 
14// 
15// Copyright (c) 2006, 2016, 2017, 2020, 2023, 2024 Tristan Grimmer. 
16// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby 
17// granted, provided that the above copyright notice and this permission notice appear in all copies. 
18// 
19// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 
20// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 
21// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 
22// AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 
23// PERFORMANCE OF THIS SOFTWARE. 
24 
25#include <Image/tTexture.h> 
26#define RGBCX_IMPLEMENTATION 
27#include <BC7Enc/rgbcx.h> 
28namespace tImage 
29
30 
31 
32bool tTexture::BC7EncInitialized = false
33 
34 
35bool tTexture::Set(tList<tLayer>& layers
36
37 Clear(); 
38 if (layers.GetNumItems() == 0
39 return false
40 
41 while (!layers.IsEmpty()) 
42 Layers.Append(item: layers.Remove()); 
43 
44 Opaque = Layers.First()->IsOpaqueFormat(); 
45 return true
46
47 
48 
49bool tTexture::Load(const tString& ddsFile, tFaceIndex face, bool correctRowOrder
50
51 Clear(); 
52 if ((tSystem::tGetFileType(file: ddsFile) != tSystem::tFileType::DDS) || !tSystem::tFileExists(file: ddsFile)) 
53 return false
54 tImageDDS::LoadParams params
55 params.Flags = tImageDDS::LoadFlag_ReverseRowOrder
56 tImageDDS dds(ddsFile, params); 
57 if (!dds.IsValid()) 
58 return false
59 
60 return Set(ddsObject&: dds, face); 
61
62 
63 
64bool tTexture::Set(tImageDDS& dds, tFaceIndex face
65
66 Clear(); 
67 if (!dds.IsValid()) 
68 return false
69 
70 if (!dds.IsCubemap()) 
71
72 dds.StealLayers(Layers); 
73
74 else 
75
76 tList<tLayer> layerSets[tFaceIndex_NumFaces]; 
77 dds.StealCubemapLayers(layers: layerSets); 
78 while (!layerSets[face].IsEmpty()) 
79 Layers.Append(item: layerSets[face].Remove()); 
80
81 
82 if (Layers.GetNumItems() == 0
83 return false
84 
85 Opaque = Layers.First()->IsOpaqueFormat(); 
86 
87 tLayer* mainLayer = Layers.First(); 
88 int width = mainLayer->Width
89 int height = mainLayer->Height
90 
91 if 
92
93 !tMath::tInRange(val: width, min: tLayer::MinLayerDimension, max: tLayer::MaxLayerDimension) || 
94 !tMath::tInRange(val: height, min: tLayer::MinLayerDimension, max: tLayer::MaxLayerDimension
95
96
97 Clear(); 
98 return false
99
100 
101 return true
102
103 
104 
105/* 
106bool tTexture::Load(const tString& imageFile, bool generateMipMaps, tPixelFormat format, tQuality quality, int forceWidth, int forceHeight) 
107{ 
108 Clear(); 
109 tPicture image(imageFile); 
110 if (!image.IsValid()) 
111 return false; 
112 
113 return Set(image, generateMipMaps, format, quality, forceWidth, forceHeight); 
114} 
115*/ 
116 
117 
118bool tTexture::Set(tPicture& image, bool generateMipmaps, tPixelFormat pixelFormat, tQuality quality, int forceWidth, int forceHeight
119
120 Clear(); 
121 
122 // Sanity check force arguments. 
123 if (forceWidth && !tMath::tIsPower2(v: forceWidth)) 
124 throw tError("Texture forceWidth was specified but is not a power of 2."); 
125 
126 if (forceHeight && !tMath::tIsPower2(v: forceHeight)) 
127 throw tError("Texture forceHeight was specified but is not a power of 2."); 
128 
129 // If the dimensions are incorrect we choose the closest power of 2 to resample to. Eg. If the value is 54 we can 
130 // choose from 32 and 64, but since 32 is 22 away and 64 is only 10, we choose 64. 
131 int origWidth = image.GetWidth(); 
132 int newWidth = forceWidth ? forceWidth : tMath::tClosestPower2(v: origWidth); 
133 tMath::tiClamp(val&: newWidth, min: tLayer::MinLayerDimension, max: tLayer::MaxLayerDimension); 
134 
135 int origHeight = image.GetHeight(); 
136 int newHeight = forceHeight ? forceHeight : tMath::tClosestPower2(v: origHeight); 
137 tMath::tiClamp(val&: newHeight, min: tLayer::MinLayerDimension, max: tLayer::MaxLayerDimension); 
138 
139 if ((origWidth != newWidth) || (origHeight != newHeight)) 
140
141 // Might want to let user know that we're resampling here. This resize happens when the artist didn't submit 
142 // proper power-of-2-sized images or if dimensions were forced. 
143 bool ok = image.Resize(width: newWidth, height: newHeight, filter: DetermineFilter(quality)); 
144 if (!ok
145 throw tError("Problem resampling texture '%s' to %dx%d.", tSystem::tGetFileBaseName(file: image.Filename).Pod(), newWidth, newHeight); 
146
147 
148 // This must be set before AutoDeterminePixelFormat is called. 
149 Opaque = image.IsOpaque(); 
150 
151 // Are we supposed to automatically determine the pixel format? 
152 if (pixelFormat == tPixelFormat::Auto
153 pixelFormat = DeterminePixelFormat(image); 
154 
155 switch (pixelFormat
156
157 case tPixelFormat::R8G8B8
158 case tPixelFormat::R8G8B8A8
159 ProcessImageTo_R8G8B8_Or_R8G8B8A8(image, pixelFormat, generateMipmaps, quality); 
160 break
161 
162 case tPixelFormat::G3B5R5G3
163 ProcessImageTo_G3B5R5G3(image, generateMipmaps, quality); 
164 break
165 
166 case tPixelFormat::BC1DXT1A
167 case tPixelFormat::BC1DXT1
168 case tPixelFormat::BC2DXT2DXT3
169 case tPixelFormat::BC3DXT4DXT5
170 ProcessImageTo_BCTC(image, pixelFormat, generateMipmaps, quality); 
171 break
172 
173 default
174 throw tError("Conversion of image to pixel format %d failed.", int(pixelFormat)); 
175
176 
177 // Since the convert functions may or may not modify the source tPicture image, we guarantee invalidness here. 
178 image.Clear(); 
179 return true
180
181 
182 
183void tTexture::ProcessImageTo_R8G8B8_Or_R8G8B8A8(tPicture& image, tPixelFormat format, bool generateMipmaps, tQuality quality
184
185 tAssert((format == tPixelFormat::R8G8B8) || (format == tPixelFormat::R8G8B8A8)); 
186 int width = image.GetWidth(); 
187 int height = image.GetHeight(); 
188 int bytesPerPixel = (format == tPixelFormat::R8G8B8) ? 3 : 4
189 tResampleFilter filter = DetermineFilter(quality); 
190 
191 // This loop resamples (reduces) the image multiple times for mipmap generation. In general we should start with 
192 // the original image every time so that we're not applying interpolations to interpolations (better quality). 
193 // However, since we are only using a box-filter (pixel averaging) there is no benefit to having a fresh src 
194 // image each time. The math is equivalent: (a+b/2 + c+d/2)/2 = (a+b+c+d)/4. For now we are saving the extra 
195 // effort to start with an original every time. If we ever use a more advanced filter we'll need to change this 
196 // behaviour. Note: we're now using bilinear as the lower quality filter. Should probably make the change. 
197 while (1
198
199 int numDataBytes = width*height*bytesPerPixel
200 uint8* layerData = new uint8[numDataBytes]; 
201 
202 // We can just extract the data out directly from RGBA to either RGB or RGBA. 
203 uint8* srcPixel = (uint8*)image.GetPixelPointer(); 
204 uint8* dstPixel = layerData
205 for (int p = 0; p < width*height; p++) 
206
207 tStd::tMemcpy(dest: dstPixel, src: srcPixel, numBytes: bytesPerPixel); 
208 srcPixel += 4; // Src is always RGBA. 
209 dstPixel += bytesPerPixel; // Dst is RGB or RGBA. 
210
211 
212 tLayer* layer = new tLayer(format, width, height, layerData, true); 
213 tAssert(numDataBytes == layer->GetDataSize()); 
214 Layers.Append(item: layer); 
215 
216 // Was this the last one? 
217 if (((width == 1) && (height == 1)) || !generateMipmaps
218 break
219 
220 // Remember, width and height are not necessarily the same. As soon as one reaches 1 it needs to stay there until 
221 // the other gets there too. 
222 if (width != 1
223 width >>= 1
224 
225 if (height != 1
226 height >>= 1
227 
228 image.Resize(width, height, filter); 
229
230
231 
232 
233void tTexture::ProcessImageTo_G3B5R5G3(tPicture& image, bool generateMipmaps, tQuality quality
234
235 int width = image.GetWidth(); 
236 int height = image.GetHeight(); 
237 int bytesPerPixel = 2
238 tResampleFilter filter = DetermineFilter(quality); 
239 
240 // This loop resamples (reduces) the image multiple times for mipmap generation. In general we should start with 
241 // the original image every time so that we're not applying interpolations to interpolations (better quality). 
242 // However, since we are only using a box-filter (pixel averaging) there is no benefit to having a fresh src 
243 // image each time. The math is equivalent: (a+b/2 + c+d/2)/2 = (a+b+c+d)/4. For now we are saving the extra 
244 // effort to start with an original every time. If we ever use a more advanced filter we'll need to change this 
245 // behaviour. Note: we're now using bilinear as the lower quality filter. Should probably make the change. 
246 while (1
247
248 int numDataBytes = width*height*bytesPerPixel
249 uint8* layerData = new uint8[numDataBytes]; 
250 
251 // We need to change the src data (RGBA) into 16bits. 
252 tPixel4b* srcPixel = image.GetPixelPointer(); 
253 uint8* dstPixel = layerData
254 for (int p = 0; p < width*height; p++) 
255
256 // In memory. Each letter a bit: GGGBBBBB RRRRRGGG 
257 dstPixel[0] = (srcPixel->G & 0x1C << 3) | (srcPixel->B >> 3); 
258 dstPixel[1] = (srcPixel->R & 0xF8) | (srcPixel->G >> 5); 
259 srcPixel++; 
260 dstPixel += bytesPerPixel
261
262 
263 tLayer* layer = new tLayer(tPixelFormat::G3B5R5G3, width, height, layerData, true); 
264 tAssert(numDataBytes == layer->GetDataSize()); 
265 Layers.Append(item: layer); 
266 
267 // Was this the last one? 
268 if (((width == 1) && (height == 1)) || !generateMipmaps
269 break
270 
271 // Remember, width and height are not necessarily the same. As soon as one reaches 1 it needs to stay there until 
272 // the other gets there too. 
273 if (width != 1
274 width >>= 1
275 
276 if (height != 1
277 height >>= 1
278 
279 image.Resize(width, height, filter); 
280
281
282 
283 
284void tTexture::ProcessImageTo_BCTC(tPicture& image, tPixelFormat pixelFormat, bool generateMipmaps, tQuality quality
285
286 int width = image.GetWidth(); 
287 int height = image.GetHeight(); 
288 tResampleFilter filter = DetermineFilter(quality); 
289 if (!tMath::tIsPower2(v: width) || !tMath::tIsPower2(v: height)) 
290 throw tError("Texture must be power-of-2 to be compressed to a BC format."); 
291 
292 if (!BC7EncInitialized
293
294 rgbcx::init(mode: rgbcx::bc1_approx_mode::cBC1Ideal); 
295 BC7EncInitialized = true
296
297 
298 // This loop resamples (reduces) the image multiple times for mipmap generation. In general we should start with 
299 // the original image every time so that we're not applying interpolations to interpolations (better quality). 
300 // However, since we are only using a box-filter (pixel averaging) there is no benefit to having a fresh src 
301 // image each time. The math is equivalent: (a+b/2 + c+d/2)/2 = (a+b+c+d)/4. For now we are saving the extra 
302 // effort to start with an original every time. If we ever use a more advanced filter we'll need to change this 
303 // behaviour. Note: we're now using bilinear as the lower quality filter. Should probably make the change. 
304 while (1
305
306 // Setup the layer data to receive the compressed data. 
307 int numBlocks = tMath::tMax(a: 1, b: width/4) * tMath::tMax(a: 1, b: height/4); 
308 int blockSize = (pixelFormat == tPixelFormat::BC1DXT1) ? 8 : 16
309 int outputSize = numBlocks * blockSize
310 uint8* outputData = new uint8[outputSize]; 
311 
312 int encoderQualityLevel = DetermineBlockEncodeQualityLevel(quality); 
313 bool allow3colour = true
314 bool useTransparentTexelsForBlack = false
315 
316 uint8* blockDest = outputData
317 uint8* pixelSrc = (uint8*)image.GetPixelPointer(); 
318 for (int block = 0; block < numBlocks; block++) 
319
320 switch (pixelFormat
321
322 case tPixelFormat::BC1DXT1
323 rgbcx::encode_bc1(level: encoderQualityLevel, pDst: blockDest, pPixels: pixelSrc, allow_3color: allow3colour, allow_transparent_texels_for_black: useTransparentTexelsForBlack); 
324 break
325 
326 case tPixelFormat::BC3DXT4DXT5
327 rgbcx::encode_bc3(level: encoderQualityLevel, pDst: blockDest, pPixels: pixelSrc); 
328 break
329 
330 default
331 throw tError("Unsupported BC pixel format %d.", int(pixelFormat)); 
332
333 blockDest += blockSize
334 pixelSrc += sizeof(tPixel4b); 
335
336 
337 // The last true in this call allows the layer constructor to steal the outputData pointer. Avoids extra memcpys. 
338 tLayer* layer = new tLayer(pixelFormat, width, height, outputData, true); 
339 tAssert(layer->GetDataSize() == outputSize); 
340 Layers.Append(item: layer); 
341 
342 // Was this the last one? 
343 if (((width == 1) && (height == 1)) || !generateMipmaps
344 break
345 
346 if (width != 1
347 width >>= 1
348 
349 if (height != 1
350 height >>= 1
351 
352 // When using BC compression we don't ever want to scale lower than 4x4 as that is the individual block size. 
353 // we need at least that much data so the compressor can do it's job. Consider a 128x4 texture: Ideally we want 
354 // that to rescale to 64x4, rather than 64x2. So it's reasonable to just stop once either dimension reaches 4 
355 // because otherwise non-uniform scale issues come into play. In short, we either have to deal with this 
356 // distortion, or the cropping issue of just stopping. We do the latter because it's just easier. 
357 // 
358 // Just because we stop downscaling doesn't mean that we don't generate all the mipmap levels! We still 
359 // generate all the way to 1x1. It's only the src data that stops being down-sampled. 
360 if ((image.GetWidth() >= 8) && (image.GetHeight() >= 8)) 
361
362 // This code scales by half using the correct quality filter. 
363 int newWidth = image.GetWidth() / 2
364 int newHeight = image.GetHeight() / 2
365 image.Resize(width: newWidth, height: newHeight, filter); 
366
367
368
369 
370 
371int tTexture::ComputeMaxNumberOfMipmaps() const 
372
373 if (!IsValid()) 
374 return 0
375 
376 int maxDim = tMath::tMax(a: GetWidth(), b: GetHeight()); 
377 int count = 0
378 while (maxDim > 0
379
380 maxDim >>= 1
381 count++; 
382
383 
384 return count
385
386 
387 
388void tTexture::Save(tChunkWriter& chunk) const 
389
390 chunk.Begin(chunkID: tChunkID::Image_Texture); 
391
392 chunk.Begin(chunkID: tChunkID::Image_TextureProperties); 
393
394 chunk.Write(b: Opaque); 
395
396 chunk.End(); 
397 
398 chunk.Begin(chunkID: tChunkID::Image_TextureLayers); 
399
400 for (tLayer* layer = Layers.First(); layer; layer = layer->Next()) 
401 layer->Save(chunk); 
402
403 chunk.End(); 
404
405 chunk.End(); 
406
407 
408 
409void tTexture::Load(const tChunk& chunk
410
411 Clear(); 
412 if (chunk.ID() != tChunkID::Image_Texture
413 return
414 
415 int numLayers = 0
416 for (tChunk ch = chunk.First(); ch.IsValid(); ch = ch.Next()) 
417
418 switch (ch.ID()) 
419
420 case tChunkID::Image_TextureProperties
421
422 ch.GetItem(item&: Opaque); 
423 break
424
425 
426 case tChunkID::Image_TextureLayers
427
428 for (tChunk layerChunk = ch.First(); layerChunk.IsValid(); layerChunk = layerChunk.Next()) 
429 Layers.Append(item: new tLayer(layerChunk)); 
430 break
431
432
433
434
435 
436 
437bool tTexture::operator==(const tTexture& src) const 
438
439 if (!IsValid() || !src.IsValid()) 
440 return false
441 
442 if (Opaque != src.Opaque
443 return false
444 
445 if (Layers.GetNumItems() != Layers.GetNumItems()) 
446 return false
447 
448 tLayer* srcLayer = Layers.First(); 
449 for (tLayer* layer = Layers.First(); layer; layer = layer->Next(), srcLayer = srcLayer->Next()) 
450 if (*layer != *srcLayer
451 return false
452 
453 return true
454
455 
456 
457
458