SourcePP
Several modern C++20 libraries for sanely parsing Valve's formats.
Loading...
Searching...
No Matches
VTF.cpp
Go to the documentation of this file.
1// ReSharper disable CppDFATimeOver
2// ReSharper disable CppRedundantParentheses
3// ReSharper disable CppRedundantQualifier
4// ReSharper disable CppUseStructuredBinding
5
6#include <vtfpp/VTF.h>
7
8#include <algorithm>
9#include <cstring>
10#include <ranges>
11#include <unordered_map>
12#include <utility>
13
14#ifdef SOURCEPP_BUILD_WITH_TBB
15#include <execution>
16#endif
17
18#ifdef SOURCEPP_BUILD_WITH_THREADS
19#include <future>
20#include <thread>
21#endif
22
23#include <BufferStream.h>
24#include <miniz.h>
25#include <zstd.h>
26
29#include <vtfpp/ImageQuantize.h>
30
31using namespace sourcepp;
32using namespace vtfpp;
33
34namespace {
35
36[[nodiscard]] std::vector<std::byte> compressData(std::span<const std::byte> data, int16_t level, CompressionMethod method) {
37 switch (method) {
38 using enum CompressionMethod;
39 case DEFLATE: {
40 mz_ulong compressedSize = mz_compressBound(data.size());
41 std::vector<std::byte> out(compressedSize);
42
43 int status = MZ_OK;
44 while ((status = mz_compress2(reinterpret_cast<unsigned char*>(out.data()), &compressedSize, reinterpret_cast<const unsigned char*>(data.data()), data.size(), level)) == MZ_BUF_ERROR) {
45 compressedSize *= 2;
46 out.resize(compressedSize);
47 }
48
49 if (status != MZ_OK) {
50 return {};
51 }
52 out.resize(compressedSize);
53 return out;
54 }
55 case ZSTD: {
56 if (level < 0) {
57 level = 6;
58 }
59
60 const auto expectedSize = ZSTD_compressBound(data.size());
61 std::vector<std::byte> out(expectedSize);
62
63 const auto compressedSize = ZSTD_compress(out.data(), expectedSize, data.data(), data.size(), level);
64 if (ZSTD_isError(compressedSize)) {
65 return {};
66 }
67
68 out.resize(compressedSize);
69 return out;
70 }
71 case CONSOLE_LZMA: {
72 if (const auto out = compression::compressValveLZMA(data, level)) {
73 return *out;
74 }
75 return {};
76 }
77 }
78 return {};
79}
80
81template<std::unsigned_integral T, bool ExistingDataIsSwizzled>
82constexpr void swizzleUncompressedImageData(std::span<std::byte> inputData, std::span<std::byte> outputData, ImageFormat format, uint16_t width, uint16_t height, uint16_t depth) {
83 width *= ImageFormatDetails::bpp(format) / (sizeof(T) * 8);
84 const auto zIndex = [
85 widthL2 = static_cast<int>(math::log2ceil(width)),
86 heightL2 = static_cast<int>(math::log2ceil(height)),
87 depthL2 = static_cast<int>(math::log2ceil(depth))
88 ](uint32_t x, uint32_t y, uint32_t z) {
89 auto widthL2m = widthL2;
90 auto heightL2m = heightL2;
91 auto depthL2m = depthL2;
92 uint32_t offset = 0;
93 uint32_t shiftCount = 0;
94 do {
95 if (depthL2m --> 0) {
96 offset |= (z & 1) << shiftCount++;
97 z >>= 1;
98 }
99 if (heightL2m --> 0) {
100 offset |= (y & 1) << shiftCount++;
101 y >>= 1;
102 }
103 if (widthL2m --> 0) {
104 offset |= (x & 1) << shiftCount++;
105 x >>= 1;
106 }
107 } while (x || y || z);
108 return offset;
109 };
110
111 const auto* inputPtr = reinterpret_cast<const T*>(inputData.data());
112 auto* outputPtr = reinterpret_cast<T*>(outputData.data());
113 for (uint16_t x = 0; x < width; x++) {
114 for (uint16_t y = 0; y < height; y++) {
115 for (uint16_t z = 0; z < depth; z++) {
116 if constexpr (ExistingDataIsSwizzled) {
117 *outputPtr++ = reinterpret_cast<const T*>(inputData.data())[zIndex(x, y, z)];
118 } else {
119 reinterpret_cast<T*>(outputData.data())[zIndex(x, y, z)] = *inputPtr++;
120 }
121 }
122 }
123 }
124}
125
126template<bool ConvertingFromSource>
127void swapImageDataEndianForConsole(std::span<std::byte> imageData, ImageFormat format, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t depth, VTF::Platform platform) {
128 if (imageData.empty() || format == ImageFormat::EMPTY || platform == VTF::PLATFORM_PC) {
129 return;
130 }
131
132 if (platform == VTF::PLATFORM_X360) {
133 switch (format) {
134 using enum ImageFormat;
135 case BGRA8888:
136 case BGRX8888:
137 case UVWQ8888:
138 case UVLX8888: {
139 const auto newData = ImageConversion::convertSeveralImageDataToFormat(imageData, ARGB8888, BGRA8888, mipCount, frameCount, faceCount, width, height, depth);
140 std::ranges::copy(newData, imageData.begin());
141 break;
142 }
143 case DXT1:
145 case DXT3:
146 case DXT5:
147 case UV88: {
148 std::span dxtData{reinterpret_cast<uint16_t*>(imageData.data()), imageData.size() / sizeof(uint16_t)};
149 std::for_each(
150#ifdef SOURCEPP_BUILD_WITH_TBB
151 std::execution::par_unseq,
152#endif
153 dxtData.begin(), dxtData.end(), [](uint16_t& value) {
154 BufferStream::swap_endian(&value);
155 });
156 break;
157 }
158 default:
159 break;
160 }
161 }
162
163 // todo(vtfpp): should we enable 16-bit wide and 8-bit wide formats outside XBOX?
164 if ((platform == VTF::PLATFORM_XBOX || platform == VTF::PLATFORM_PS3_ORANGEBOX || platform == VTF::PLATFORM_PS3_PORTAL2) && !ImageFormatDetails::compressed(format) && (ImageFormatDetails::bpp(format) % 32 == 0 || (platform == VTF::PLATFORM_XBOX && (ImageFormatDetails::bpp(format) % 16 == 0 || ImageFormatDetails::bpp(format) % 8 == 0)))) {
165 const bool compressed = ImageFormatDetails::compressed(format);
166 std::vector<std::byte> out(imageData.size());
167 for(int mip = mipCount - 1; mip >= 0; mip--) {
168 const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(mip, width, height, depth, compressed);
169 for (int frame = 0; frame < frameCount; frame++) {
170 for (int face = 0; face < faceCount; face++) {
171 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, format, mip, mipCount, frame, frameCount, face, faceCount, width, height)) {
172 std::span imageDataSpan{imageData.data() + offset, length * mipDepth};
173 std::span outSpan{out.data() + offset, length * mipDepth};
174 if (ImageFormatDetails::bpp(format) % 32 == 0) {
175 ::swizzleUncompressedImageData<uint32_t, ConvertingFromSource>(imageDataSpan, outSpan, format, mipWidth, mipHeight, mipDepth);
176 } else if (ImageFormatDetails::bpp(format) % 16 == 0) {
177 ::swizzleUncompressedImageData<uint16_t, ConvertingFromSource>(imageDataSpan, outSpan, format, mipWidth, mipHeight, mipDepth);
178 } else /*if (ImageFormatDetails::bpp(format) % 8 == 0)*/ {
179 ::swizzleUncompressedImageData<uint8_t, ConvertingFromSource>(imageDataSpan, outSpan, format, mipWidth, mipHeight, mipDepth);
180 }
181 }
182 }
183 }
184 }
185 std::memcpy(imageData.data(), out.data(), out.size());
186 }
187}
188
189template<bool ConvertingFromDDS>
190[[nodiscard]] std::vector<std::byte> convertBetweenDDSAndVTFMipOrderForXBOX(bool padded, std::span<const std::byte> imageData, ImageFormat format, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t depth, bool& ok) {
191 std::vector<std::byte> reorderedImageData;
192 reorderedImageData.resize(ImageFormatDetails::getDataLengthXBOX(padded, format, mipCount, frameCount, faceCount, width, height, depth));
193 BufferStream reorderedStream{reorderedImageData};
194
195 if constexpr (ConvertingFromDDS) {
196 for (int i = mipCount - 1; i >= 0; i--) {
197 const auto mipDepth = ImageDimensions::getMipDim(i, depth);
198 for (int j = 0; j < frameCount; j++) {
199 for (int k = 0; k < faceCount; k++) {
200 for (int l = 0; l < mipDepth; l++) {
201 uint32_t oldOffset, length;
202 if (!ImageFormatDetails::getDataPositionXbox(oldOffset, length, padded, format, i, mipCount, j, frameCount, k, faceCount, width, height, l, depth)) {
203 ok = false;
204 return {};
205 }
206 reorderedStream << imageData.subspan(oldOffset, length);
207 }
208 }
209 }
210 }
211 } else {
212 for (int j = 0; j < frameCount; j++) {
213 for (int k = 0; k < faceCount; k++) {
214 for (int i = 0; i < mipCount; i++) {
215 const auto mipDepth = ImageDimensions::getMipDim(i, depth);
216 for (int l = 0; l < mipDepth; l++) {
217 uint32_t oldOffset, length;
218 if (!ImageFormatDetails::getDataPosition(oldOffset, length, format, i, mipCount, j, frameCount, k, faceCount, width, height, l, depth)) {
219 ok = false;
220 return {};
221 }
222 reorderedStream << imageData.subspan(oldOffset, length);
223 }
224 }
225 }
226 if (padded && j + 1 != frameCount && reorderedStream.tell() > 512) {
227 reorderedStream.pad(math::paddingForAlignment(512, reorderedStream.tell()));
228 }
229 }
230 }
231
232 ok = true;
233 return reorderedImageData;
234}
235
236} // namespace
237
239 switch (this->type) {
241 if (this->data.size() <= sizeof(uint32_t)) {
242 return {};
243 }
244 return SHT{{reinterpret_cast<const std::byte*>(this->data.data()) + sizeof(uint32_t), *reinterpret_cast<const uint32_t*>(this->data.data())}};
245 case TYPE_CRC:
247 if (this->data.size() != sizeof(uint32_t)) {
248 return {};
249 }
250 return *reinterpret_cast<const uint32_t*>(this->data.data());
252 if (this->data.size() != sizeof(uint32_t)) {
253 return {};
254 }
255 return std::make_tuple(
256 *(reinterpret_cast<const uint8_t*>(this->data.data()) + 0),
257 *(reinterpret_cast<const uint8_t*>(this->data.data()) + 1),
258 *(reinterpret_cast<const uint8_t*>(this->data.data()) + 2),
259 *(reinterpret_cast<const uint8_t*>(this->data.data()) + 3));
261 if (this->data.size() <= sizeof(uint32_t)) {
262 return "";
263 }
264 return std::string(reinterpret_cast<const char*>(this->data.data()) + sizeof(uint32_t), *reinterpret_cast<const uint32_t*>(this->data.data()));
266 if (this->data.size() <= sizeof(uint32_t)) {
267 return {};
268 }
269 return HOT{{reinterpret_cast<const std::byte*>(this->data.data()) + sizeof(uint32_t), *reinterpret_cast<const uint32_t*>(this->data.data())}};
270 default:
271 break;
272 }
273 return {};
274}
275
277 this->opened = true;
278}
279
280VTF::VTF(std::vector<std::byte>&& vtfData, bool parseHeaderOnly)
281 : data(std::move(vtfData)) {
282 BufferStreamReadOnly stream{this->data};
283
284 if (const auto signature = stream.read<uint32_t>(); signature == VTF_SIGNATURE) {
285 stream >> this->platform;
286 if (this->platform != PLATFORM_PC) {
287 return;
288 }
289 stream >> this->version;
290 if (this->version > 6) {
291 return;
292 }
293 } else if (signature == VTFX_SIGNATURE || signature == VTF3_SIGNATURE) {
294 stream.set_big_endian(true);
295 stream >> this->platform;
296 if (this->platform != PLATFORM_X360 && this->platform != PLATFORM_PS3_ORANGEBOX && this->platform != PLATFORM_PS3_PORTAL2) {
297 return;
298 }
299 stream >> this->version;
300 if (this->version != 8) {
301 return;
302 }
303 // Now fix up the actual version as it would be on PC
304 if (signature == VTF3_SIGNATURE) {
305 this->platform = PLATFORM_PS3_PORTAL2;
306 this->version = 5;
307 } else {
308 this->version = 4;
309 }
310 } else if (signature == XTF_SIGNATURE) {
311 stream >> this->platform;
312 if (this->platform != PLATFORM_XBOX) {
313 return;
314 }
315 stream >> this->version;
316 if (this->version != 0) {
317 return;
318 }
319 // Now fix up the actual version as it would be on PC
320 this->version = 2;
321 } else {
322 return;
323 }
324
325 const auto headerSize = stream.read<uint32_t>();
326
327 const auto readResources = [this, &stream](uint32_t resourceCount) {
328 // Read resource header info
329 this->resources.reserve(resourceCount);
330 for (int i = 0; i < resourceCount; i++) {
331 auto& [type, flags_, data_] = this->resources.emplace_back();
332
333 auto typeAndFlags = stream.read<uint32_t>();
334 if (stream.is_big_endian()) {
335 // This field is little-endian
336 BufferStream::swap_endian(&typeAndFlags);
337 }
338 type = static_cast<Resource::Type>(typeAndFlags & 0xffffff); // last 3 bytes
339 flags_ = static_cast<Resource::Flags>(typeAndFlags >> 24); // first byte
340 data_ = stream.read_span<std::byte>(4);
341
342 if (stream.is_big_endian() && !(flags_ & Resource::FLAG_LOCAL_DATA)) {
343 BufferStream::swap_endian(reinterpret_cast<uint32_t*>(data_.data()));
344 }
345 }
346
347 // Sort resources by their offset, in case certain VTFs are written
348 // weirdly and have resource data written out of order. So far I have
349 // found only one VTF in an official Valve game where this is the case.
350 // UPDATE: We do this intentionally to write image data at the end now!
351 // It fixes mip skipping issues, and should still work fine in every branch.
352 std::ranges::sort(this->resources, [](const Resource& lhs, const Resource& rhs) {
354 return lhs.type < rhs.type;
355 }
357 return true;
358 }
360 return false;
361 }
362 return *reinterpret_cast<uint32_t*>(lhs.data.data()) < *reinterpret_cast<uint32_t*>(rhs.data.data());
363 });
364
365 // Fix up data spans to point to the actual data
366 Resource* lastResource = nullptr;
367 for (auto& resource : this->resources) {
368 if (!(resource.flags & Resource::FLAG_LOCAL_DATA)) {
369 if (lastResource) {
370 const auto lastOffset = *reinterpret_cast<uint32_t*>(lastResource->data.data());
371 const auto currentOffset = *reinterpret_cast<uint32_t*>(resource.data.data());
372 const auto curPos = stream.tell();
373 stream.seek(lastOffset);
374 lastResource->data = stream.read_span<std::byte>(currentOffset - lastOffset);
375 stream.seek(static_cast<int64_t>(curPos));
376 }
377 lastResource = &resource;
378 }
379 }
380 if (lastResource) {
381 const auto offset = *reinterpret_cast<uint32_t*>(lastResource->data.data());
382 const auto curPos = stream.tell();
383 stream.seek(offset);
384 lastResource->data = stream.read_span<std::byte>(stream.size() - offset);
385 stream.seek(static_cast<int64_t>(curPos));
386 }
387 };
388
389 // HACK: a couple tweaks to fix engine bugs or branch differences
390 const auto postReadTransform = [this] {
391 // Change the format to DXT1_ONE_BIT_ALPHA to get compressonator to recognize it.
392 // No source game recognizes this format, so we will do additional transform in bake back to DXT1.
393 // We also need to check MULTI_BIT_ALPHA flag because stupid third party tools will sometimes set it???
394 if (this->format == ImageFormat::DXT1 && this->flags & (FLAG_V0_ONE_BIT_ALPHA | FLAG_V0_MULTI_BIT_ALPHA)) {
395 this->format = ImageFormat::DXT1_ONE_BIT_ALPHA;
396 }
397 // If the version is below 7.5, NV_NULL / ATI2N / ATI1N are at different positions in the enum.
398 // It is safe to do this because these formats didn't exist before v7.5.
399 if (this->version < 5 && (this->format == ImageFormat::RGBA1010102 || this->format == ImageFormat::BGRA1010102 || this->format == ImageFormat::R16F)) {
400 this->format = static_cast<ImageFormat>(static_cast<int32_t>(this->format) - 3);
401 }
402 // We need to apply this transform because of the broken Borealis skybox in HL2.
403 // Thanks Valve! If this transform isn't applied it breaks conversion to console formats.
404 if (this->flags & FLAG_V0_NO_MIP && this->mipCount > 1) {
405 this->removeFlags(FLAG_V0_NO_MIP);
406 }
407 };
408
409 switch (this->platform) {
410 case PLATFORM_UNKNOWN:
411 return;
412 case PLATFORM_PC: {
413 stream
414 .read(this->width)
415 .read(this->height)
416 .read(this->flags)
417 .read(this->frameCount)
418 .read(this->startFrame)
419 .skip(4)
420 .read(this->reflectivity[0])
421 .read(this->reflectivity[1])
422 .read(this->reflectivity[2])
423 .skip(4)
424 .read(this->bumpMapScale)
425 .read(this->format)
426 .read(this->mipCount);
427
428 postReadTransform();
429
430 // This will always be DXT1
431 stream.skip<ImageFormat>();
432 stream >> this->thumbnailWidth >> this->thumbnailHeight;
433 if (this->thumbnailWidth == 0 || this->thumbnailHeight == 0) {
434 this->thumbnailFormat = ImageFormat::EMPTY;
435 } else {
436 this->thumbnailFormat = ImageFormat::DXT1;
437 }
438
439 if (this->version < 2) {
440 this->depth = 1;
441 } else {
442 stream.read(this->depth);
443 }
444
445 if (parseHeaderOnly) {
446 this->opened = true;
447 return;
448 }
449
450 if (this->version >= 3) {
451 stream.skip(3);
452 auto resourceCount = stream.read<uint32_t>();
453 stream.skip(8);
454 readResources(resourceCount);
455
456 this->opened = stream.tell() == headerSize;
457
458 if (this->opened && this->version >= 6) {
459 const auto* auxResource = this->getResource(Resource::TYPE_AUX_COMPRESSION);
460 const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA);
461 if (auxResource && imageResource) {
462 if (auxResource->getDataAsAuxCompressionLevel() != 0) {
463 const auto faceCount = this->getFaceCount();
464 std::vector<std::byte> decompressedImageData(ImageFormatDetails::getDataLength(this->format, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->depth));
465 uint32_t oldOffset = 0;
466 for (int i = this->mipCount - 1; i >= 0; i--) {
467 for (int j = 0; j < this->frameCount; j++) {
468 for (int k = 0; k < faceCount; k++) {
469 uint32_t oldLength = auxResource->getDataAsAuxCompressionLength(i, this->mipCount, j, this->frameCount, k, faceCount);
470 if (uint32_t newOffset, newLength; ImageFormatDetails::getDataPosition(newOffset, newLength, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, 0, this->getDepth())) {
471 // Keep in mind that slices are compressed together
472 mz_ulong decompressedImageDataSize = newLength * this->depth;
473 switch (auxResource->getDataAsAuxCompressionMethod()) {
474 using enum CompressionMethod;
475 case DEFLATE:
476 if (mz_uncompress(reinterpret_cast<unsigned char*>(decompressedImageData.data() + newOffset), &decompressedImageDataSize, reinterpret_cast<const unsigned char*>(imageResource->data.data() + oldOffset), oldLength) != MZ_OK) {
477 this->opened = false;
478 return;
479 }
480 break;
481 case ZSTD:
482 if (const auto decompressedSize = ZSTD_decompress(decompressedImageData.data() + newOffset, decompressedImageDataSize, imageResource->data.data() + oldOffset, oldLength); ZSTD_isError(decompressedSize) || decompressedSize != decompressedImageDataSize) {
483 this->opened = false;
484 return;
485 }
486 break;
487 case CONSOLE_LZMA:
488 // Shouldn't be here!
490 break;
491 }
492 }
493 oldOffset += oldLength;
494 }
495 }
496 }
497 this->setResourceInternal(Resource::TYPE_IMAGE_DATA, decompressedImageData);
498 }
499 }
500 }
501 } else {
502 stream.skip(math::paddingForAlignment(16, stream.tell()));
503 this->opened = stream.tell() == headerSize;
504
505 this->resources.reserve(2);
506
507 if (this->hasThumbnailData()) {
508 this->resources.push_back({
510 .flags = Resource::FLAG_NONE,
511 .data = stream.read_span<std::byte>(ImageFormatDetails::getDataLength(this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight)),
512 });
513 }
514 if (this->hasImageData()) {
515 this->resources.push_back({
517 .flags = Resource::FLAG_NONE,
518 .data = stream.read_span<std::byte>(stream.size() - stream.tell()),
519 });
520 }
521 }
522
523 if (const auto* resource = this->getResource(Resource::TYPE_AUX_COMPRESSION)) {
524 this->compressionLevel = resource->getDataAsAuxCompressionLevel();
525 this->compressionMethod = resource->getDataAsAuxCompressionMethod();
526 this->removeResourceInternal(Resource::TYPE_AUX_COMPRESSION);
527 }
528 return;
529 }
530 case PLATFORM_XBOX: {
531 if (this->platform == PLATFORM_XBOX) {
532 uint16_t preloadSize = 0, imageOffset = 0;
533 stream
534 .read(this->flags)
535 .read(this->width)
536 .read(this->height)
537 .read(this->depth)
538 .read(this->frameCount)
539 .read(preloadSize)
540 .read(imageOffset)
541 .read(this->reflectivity[0])
542 .read(this->reflectivity[1])
543 .read(this->reflectivity[2])
544 .read(this->bumpMapScale)
545 .read(this->format)
546 .read(this->thumbnailWidth)
547 .read(this->thumbnailHeight)
548 .read(this->fallbackWidth)
549 .read(this->fallbackHeight)
550 .read(this->consoleMipScale)
551 .skip<uint8_t>(); // padding
552
553 const bool headerSizeIsAccurate = stream.tell() == headerSize;
554
555 this->mipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth);
556 this->fallbackMipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->fallbackWidth, this->fallbackHeight);
557
558 postReadTransform();
559
560 // Can't use VTF::getFaceCount yet because there's no image data
561 const auto faceCount = (this->flags & FLAG_V0_ENVMAP) ? 6 : 1;
562
563 if (this->thumbnailWidth == 0 || this->thumbnailHeight == 0) {
564 this->thumbnailFormat = ImageFormat::EMPTY;
565 } else {
566 this->thumbnailFormat = ImageFormat::RGB888;
567 this->resources.push_back({
569 .flags = Resource::FLAG_NONE,
570 .data = stream.read_span<std::byte>(ImageFormatDetails::getDataLength(this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight)),
571 });
572 }
573
574 if (this->format == ImageFormat::P8) {
575 this->resources.push_back({
577 .flags = Resource::FLAG_NONE,
578 .data = stream.read_span<std::byte>(256 * sizeof(ImagePixel::BGRA8888) * this->frameCount),
579 });
580 }
581
582 bool ok;
583 auto fallbackSize = ImageFormatDetails::getDataLengthXBOX(false, this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight);
584 std::vector<std::byte> reorderedFallbackData;
585 if (this->hasFallbackData()) {
586 if (stream.tell() + fallbackSize != preloadSize) {
587 // A couple XTFs that shipped with HL2 are missing the NO_MIP flag. We can detect them by checking the size of the fallback
588 fallbackSize = ImageFormatDetails::getDataLengthXBOX(false, this->format, 1, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight);
589 if (stream.tell() + fallbackSize != preloadSize) {
590 this->opened = false;
591 return;
592 }
593 this->fallbackMipCount = 1;
594 this->mipCount = 1;
595 this->flags |= FLAG_V0_NO_MIP;
596 }
597 reorderedFallbackData = ::convertBetweenDDSAndVTFMipOrderForXBOX<true>(false, stream.read_span<std::byte>(fallbackSize), this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight, 1, ok);
598 if (!ok) {
599 this->opened = false;
600 return;
601 }
602 ::swapImageDataEndianForConsole<true>(reorderedFallbackData, this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight, 1, this->platform);
603 }
604
605 this->opened = headerSizeIsAccurate;
606 if (parseHeaderOnly) {
607 return;
608 }
609
610 const auto imageSize = ImageFormatDetails::getDataLengthXBOX(true, this->format, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->depth);
611 std::vector<std::byte> reorderedImageData;
612 if (this->hasImageData()) {
613 reorderedImageData = ::convertBetweenDDSAndVTFMipOrderForXBOX<true>(true, stream.seek(imageOffset).read_span<std::byte>(imageSize), this->format, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->depth, ok);
614 if (!ok) {
615 this->opened = false;
616 return;
617 }
618 ::swapImageDataEndianForConsole<true>(reorderedImageData, this->format, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->depth, this->platform);
619 }
620
621 // By this point we cannot use spans over data, it will change here
622 if (this->hasFallbackData()) {
623 this->setResourceInternal(Resource::TYPE_FALLBACK_DATA, reorderedFallbackData);
624 }
625 if (this->hasImageData()) {
626 this->setResourceInternal(Resource::TYPE_IMAGE_DATA, reorderedImageData);
627 }
628 return;
629 }
630 }
631 case PLATFORM_X360:
632 case PLATFORM_PS3_ORANGEBOX:
633 case PLATFORM_PS3_PORTAL2: {
634 uint8_t resourceCount;
635 stream
636 .read(this->flags)
637 .read(this->width)
638 .read(this->height)
639 .read(this->depth)
640 .read(this->frameCount)
641 .skip<uint16_t>() // preload
642 .read(this->consoleMipScale)
643 .read(resourceCount)
644 .read(this->reflectivity[0])
645 .read(this->reflectivity[1])
646 .read(this->reflectivity[2])
647 .read(this->bumpMapScale)
648 .read(this->format)
649 .skip<math::Vec4ui8>() // lowResImageSample (replacement for thumbnail resource, linear color pixel)
650 .skip<uint32_t>(); // compressedLength
651
652 postReadTransform();
653
654 // Align to 16 bytes
655 if (this->platform == PLATFORM_PS3_PORTAL2) {
656 stream.skip<uint32_t>();
657 }
658
659 this->mipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth);
660
661 if (parseHeaderOnly) {
662 this->opened = true;
663 return;
664 }
665
666 this->resources.reserve(resourceCount);
667 readResources(resourceCount);
668
669 this->opened = stream.tell() == headerSize;
670
671 // The resources vector isn't modified by setResourceInternal when we're not adding a new one, so this is fine
672 for (const auto& resource : this->resources) {
673 // Decompress LZMA resources
674 if (BufferStreamReadOnly rsrcStream{resource.data}; rsrcStream.read<uint32_t>() == compression::VALVE_LZMA_SIGNATURE) {
675 if (auto decompressedData = compression::decompressValveLZMA(resource.data)) {
676 this->setResourceInternal(resource.type, *decompressedData);
677
678 if (resource.type == Resource::TYPE_IMAGE_DATA) {
679 // Do this here because compressionLength in header can be garbage on PS3 orange box
680 this->compressionMethod = CompressionMethod::CONSOLE_LZMA;
681 }
682 }
683 }
684
685 if (resource.type == Resource::TYPE_THUMBNAIL_DATA) {
686 ::swapImageDataEndianForConsole<true>(resource.data, this->thumbnailFormat, 1, 1, 1, this->thumbnailWidth, this->thumbnailHeight, 1, this->platform);
687 } else if (resource.type == Resource::TYPE_IMAGE_DATA) {
688 if (this->platform == PLATFORM_PS3_ORANGEBOX) {
689 bool ok;
690 const auto reorderedFallbackData = ::convertBetweenDDSAndVTFMipOrderForXBOX<true>(false, resource.data, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->depth, ok);
691 if (!ok || reorderedFallbackData.size() != resource.data.size()) {
692 this->opened = false;
693 return;
694 }
695 std::memcpy(resource.data.data(), reorderedFallbackData.data(), resource.data.size());
696 }
697 ::swapImageDataEndianForConsole<true>(resource.data, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->depth, this->platform);
698 } else if (!(resource.flags & Resource::FLAG_LOCAL_DATA) && resource.data.size() >= sizeof(uint32_t)) {
699 BufferStream::swap_endian(reinterpret_cast<uint32_t*>(resource.data.data()));
700 }
701 }
702 return;
703 }
704 }
705}
706
707VTF::VTF(std::span<const std::byte> vtfData, bool parseHeaderOnly)
708 : VTF(std::vector<std::byte>{vtfData.begin(), vtfData.end()}, parseHeaderOnly) {}
709
710VTF::VTF(const std::filesystem::path& vtfPath, bool parseHeaderOnly)
711 : VTF(fs::readFileBuffer(vtfPath), parseHeaderOnly) {}
712
713VTF::VTF(const VTF& other) {
714 *this = other;
715}
716
717VTF& VTF::operator=(const VTF& other) {
718 this->opened = other.opened;
719 this->data = other.data;
720 this->version = other.version;
721 this->width = other.width;
722 this->height = other.height;
723 this->flags = other.flags;
724 this->frameCount = other.frameCount;
725 this->startFrame = other.startFrame;
726 this->reflectivity = other.reflectivity;
727 this->bumpMapScale = other.bumpMapScale;
728 this->format = other.format;
729 this->mipCount = other.mipCount;
730 this->thumbnailFormat = other.thumbnailFormat;
731 this->thumbnailWidth = other.thumbnailWidth;
732 this->thumbnailHeight = other.thumbnailHeight;
733 this->fallbackWidth = other.fallbackWidth;
734 this->fallbackHeight = other.fallbackHeight;
736 this->consoleMipScale = other.consoleMipScale;
737 this->depth = other.depth;
738
739 this->resources.clear();
740 for (const auto& [otherType, otherFlags, otherData] : other.resources) {
741 auto& [type, flags_, data_] = this->resources.emplace_back();
742 type = otherType;
743 flags_ = otherFlags;
744 data_ = {this->data.data() + (otherData.data() - other.data.data()), otherData.size()};
745 }
746
747 this->platform = other.platform;
752
753 return *this;
754}
755
756VTF::operator bool() const {
757 return this->opened;
758}
759
761 bool out = true;
762 if (writer.hasImageData() && (options.invertGreenChannel || options.gammaCorrection != 1.f)) {
763 for (int i = 0; i < writer.mipCount; i++) {
764 for (int j = 0; j < writer.frameCount; j++) {
765 for (int k = 0; k < writer.getFaceCount(); k++) {
766 for (int l = 0; l < writer.depth; l++) {
767 if (options.invertGreenChannel && !writer.setImage(ImageConversion::invertGreenChannelForImageData(writer.getImageDataRaw(i, j, k, l), writer.getFormat(), writer.getWidth(i), writer.getHeight(i)), writer.getFormat(), writer.getWidth(i), writer.getHeight(i), ImageConversion::ResizeFilter::DEFAULT, i, j, k, l)) {
768 out = false;
769 }
770 if (options.gammaCorrection != 1.f && !writer.setImage(ImageConversion::gammaCorrectImageData(writer.getImageDataRaw(i, j, k, l), writer.getFormat(), writer.getWidth(i), writer.getHeight(i), options.gammaCorrection), writer.getFormat(), writer.getWidth(i), writer.getHeight(i), ImageConversion::ResizeFilter::DEFAULT, i, j, k, l)) {
771 out = false;
772 }
773 }
774 }
775 }
776 }
777 }
778 writer.setPlatform(options.platform);
779 if (options.computeReflectivity) {
780 writer.computeReflectivity();
781 }
782 if (options.initialFrameCount > 1 || options.isCubeMap || options.initialDepth > 1) {
783 if (!writer.setFrameFaceAndDepth(options.initialFrameCount, options.isCubeMap, options.initialDepth)) {
784 out = false;
785 }
786 }
787 writer.setStartFrame(options.startFrame);
788 writer.setBumpMapScale(options.bumpMapScale);
789 if (options.computeThumbnail) {
790 writer.computeThumbnail();
791 }
792 if (options.outputFormat == FORMAT_UNCHANGED) {
793 options.outputFormat = writer.getFormat();
794 } else if (options.outputFormat == FORMAT_DEFAULT) {
795 options.outputFormat = VTF::getDefaultCompressedFormat(writer.getFormat(), writer.getVersion(), options.isCubeMap);
796 }
797 if (options.computeMips) {
798 if (const auto maxMipCount = ImageDimensions::getMaximumMipCount(writer.getWidth(), writer.getHeight(), writer.getDepth()); maxMipCount > 1) {
799 if (!writer.setMipCount(maxMipCount)) {
800 out = false;
801 }
802 writer.computeMips(options.filter);
803 }
804 }
806 if (options.computeTransparencyFlags) {
808 }
811 writer.setConsoleMipScale(options.consoleMipScale);
812 return out;
813}
814
815bool VTF::create(std::span<const std::byte> imageData, ImageFormat format, uint16_t width, uint16_t height, const std::filesystem::path& vtfPath, const CreationOptions& options) {
816 VTF writer;
817 writer.setVersion(options.version);
818 writer.addFlags(options.flags);
820 if (!writer.setImage(imageData, format, width, height, options.filter)) {
821 return false;
822 }
823 if (!createInternal(writer, options)) {
824 return false;
825 }
826 return writer.bake(vtfPath);
827}
828
829bool VTF::create(ImageFormat format, uint16_t width, uint16_t height, const std::filesystem::path& vtfPath, const CreationOptions& options) {
830 std::vector<std::byte> imageData;
831 imageData.resize(static_cast<uint32_t>(width) * height * ImageFormatDetails::bpp(format) / 8);
832 return create(imageData, format, width, height, vtfPath, options);
833}
834
835VTF VTF::create(std::span<const std::byte> imageData, ImageFormat format, uint16_t width, uint16_t height, const CreationOptions& options) {
836 VTF writer;
837 writer.setVersion(options.version);
838 writer.addFlags(options.flags);
840 if (!writer.setImage(imageData, format, width, height, options.filter)) {
841 writer.opened = false;
842 return writer;
843 }
844 if (!createInternal(writer, options)) {
845 writer.opened = false;
846 }
847 return writer;
848}
849
850VTF VTF::create(ImageFormat format, uint16_t width, uint16_t height, const CreationOptions& options) {
851 std::vector<std::byte> imageData;
852 imageData.resize(static_cast<uint32_t>(width) * height * ImageFormatDetails::bpp(format) / 8);
853 return create(imageData, format, width, height, options);
854}
855
856bool VTF::create(const std::filesystem::path& imagePath, const std::filesystem::path& vtfPath, const CreationOptions& options) {
857 VTF writer;
858 writer.setVersion(options.version);
859 writer.addFlags(options.flags);
861 if (!writer.setImage(imagePath, options.filter)) {
862 return false;
863 }
864 if (!createInternal(writer, options)) {
865 return false;
866 }
867 return writer.bake(vtfPath);
868}
869
870VTF VTF::create(const std::filesystem::path& imagePath, const CreationOptions& options) {
871 VTF writer;
872 writer.setVersion(options.version);
873 writer.addFlags(options.flags);
875 if (!writer.setImage(imagePath, options.filter)) {
876 writer.opened = false;
877 return writer;
878 }
879 if (!createInternal(writer, options)) {
880 writer.opened = false;
881 }
882 return writer;
883}
884
886 return this->platform;
887}
888
889void VTF::setPlatform(Platform newPlatform) {
890 if (this->platform == newPlatform) {
891 return;
892 }
893
894 // hack to allow VTF::setVersion to work
895 const auto oldPlatform = this->platform;
896 this->platform = PLATFORM_PC;
897 switch (newPlatform) {
898 case PLATFORM_UNKNOWN:
899 case PLATFORM_PC:
900 break;
901 case PLATFORM_XBOX:
902 // Have to do it this roundabout way to fix cubemaps, v7.5 has 6 faces
903 this->setVersion(5);
904 this->platform = newPlatform;
905 // It's safe to do this, and we can't use VTF::setVersion now that platform is set
906 this->version = 2;
907 break;
908 case PLATFORM_X360:
910 this->setVersion(4);
911 break;
913 this->setVersion(5);
914 break;
915 }
916 this->platform = newPlatform;
917
918 // Update flags
919 if (this->platform == PLATFORM_XBOX || newPlatform == PLATFORM_XBOX) {
921 }
922
923 // XBOX stores thumbnail as single RGB888 pixel, but we assume thumbnail is DXT1 on other platforms
924 if (this->hasThumbnailData()) {
925 if (this->platform == PLATFORM_XBOX) {
926 this->computeThumbnail();
927 } else if (oldPlatform == PLATFORM_XBOX) {
929 this->thumbnailWidth = 0;
930 this->thumbnailHeight = 0;
931 }
932 }
933
934 // Add/remove fallback data for XBOX
935 if (this->platform != PLATFORM_XBOX && this->hasFallbackData()) {
936 this->removeFallback();
937 } else if (this->platform == PLATFORM_XBOX) {
938 this->computeFallback();
939 }
940
942
943 if (this->platform != PLATFORM_PC) {
944 const auto maxMipCount = (this->flags & FLAG_V0_NO_MIP) ? 1 : ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth);
945 if (this->mipCount != maxMipCount) {
946 this->setMipCount(maxMipCount);
947 }
948 }
949}
950
951uint32_t VTF::getVersion() const {
952 return this->version;
953}
954
955void VTF::setVersion(uint32_t newVersion) {
956 if (this->platform != PLATFORM_PC) {
957 return;
958 }
959 if (this->hasImageData()) {
960 auto faceCount = this->getFaceCount();
961 if (faceCount == 7 && (newVersion < 1 || newVersion > 4)) {
962 faceCount = 6;
963 }
964 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, this->frameCount, faceCount, this->depth);
965 }
966
967 // Fix up flags
968 const bool srgb = this->isSRGB();
969 if ((this->version < 1 && newVersion >= 1) || (this->version >= 1 && newVersion < 1)) {
971 }
972 if ((this->version < 2 && newVersion >= 2) || (this->version >= 2 && newVersion < 2)) {
974 }
975 if ((this->version < 3 && newVersion >= 3) || (this->version >= 3 && newVersion < 3)) {
977 }
978 if ((this->version < 4 && newVersion >= 4) || (this->version >= 4 && newVersion < 4)) {
980 }
981 if ((this->version < 5 && newVersion >= 5) || (this->version >= 5 && newVersion < 5)) {
983 }
984 this->setSRGB(srgb);
985
986 this->version = newVersion;
987}
988
992
996
997void VTF::setImageResizeMethods(ImageConversion::ResizeMethod imageWidthResizeMethod_, ImageConversion::ResizeMethod imageHeightResizeMethod_) {
998 this->imageWidthResizeMethod = imageWidthResizeMethod_;
999 this->imageHeightResizeMethod = imageHeightResizeMethod_;
1000}
1001
1003 this->imageWidthResizeMethod = imageWidthResizeMethod_;
1004}
1005
1007 this->imageHeightResizeMethod = imageHeightResizeMethod_;
1008}
1009
1010uint16_t VTF::getWidth(uint8_t mip) const {
1011 return ImageDimensions::getMipDim(mip, this->width);
1012}
1013
1014uint16_t VTF::getPaddedWidth(uint8_t mip) const {
1015 return ImageDimensions::getMipDim(mip, this->width, ImageFormatDetails::compressed(this->format));
1016}
1017
1018uint16_t VTF::getHeight(uint8_t mip) const {
1019 return ImageDimensions::getMipDim(mip, this->height);
1020}
1021
1022uint16_t VTF::getPaddedHeight(uint8_t mip) const {
1023 return ImageDimensions::getMipDim(mip, this->height, ImageFormatDetails::compressed(this->format));
1024}
1025
1026void VTF::setSize(uint16_t newWidth, uint16_t newHeight, ImageConversion::ResizeFilter filter) {
1027 if (newWidth == 0 || newHeight == 0) {
1028 this->format = ImageFormat::EMPTY;
1029 this->width = 0;
1030 this->height = 0;
1032 return;
1033 }
1034
1036 if (this->hasImageData()) {
1037 if (this->width == newWidth && this->height == newHeight) {
1038 return;
1039 }
1040 auto newMipCount = this->mipCount;
1041 if (const auto maxMipCount = ImageDimensions::getMaximumMipCount(newWidth, newHeight, this->depth); this->platform != PLATFORM_PC || newMipCount > maxMipCount) {
1042 newMipCount = maxMipCount;
1043 }
1044 this->regenerateImageData(this->format, newWidth, newHeight, newMipCount, this->frameCount, this->getFaceCount(), this->depth, filter);
1045 } else {
1046 this->format = ImageFormat::RGBA8888;
1047 this->mipCount = 1;
1048 this->frameCount = 1;
1049 this->flags &= ~FLAG_V0_ENVMAP;
1050 this->width = newWidth;
1051 this->height = newHeight;
1052 this->depth = 1;
1053 this->setResourceInternal(Resource::TYPE_IMAGE_DATA, std::vector<std::byte>(ImageFormatDetails::getDataLength(this->format, this->mipCount, this->frameCount, 1, this->width, this->height, this->depth)));
1054 }
1055}
1056
1057uint32_t VTF::getFlags() const {
1058 return this->flags;
1059}
1060
1061void VTF::setFlags(uint32_t flags_) {
1062 this->flags = (this->flags & FLAG_MASK_INTERNAL) | (flags_ & ~FLAG_MASK_INTERNAL);
1063}
1064
1065void VTF::addFlags(uint32_t flags_) {
1066 this->flags |= flags_ & ~FLAG_MASK_INTERNAL;
1067}
1068
1069void VTF::removeFlags(uint32_t flags_) {
1070 this->flags &= ~flags_ | FLAG_MASK_INTERNAL;
1071}
1072
1073bool VTF::isSRGB() const {
1074 return !ImageFormatDetails::large(this->format) && (this->version < 4 ? false : this->version < 5 ? this->flags & FLAG_V4_SRGB : this->flags & FLAG_V5_PWL_CORRECTED || this->flags & FLAG_V5_SRGB);
1075}
1076
1077void VTF::setSRGB(bool srgb) {
1078 if (srgb) {
1079 if (this->version >= 5) {
1080 this->addFlags(FLAG_V5_SRGB);
1081 } else if (this->version == 4) {
1082 this->addFlags(FLAG_V4_SRGB);
1083 }
1084 } else {
1085 if (this->version >= 5) {
1087 } else if (this->version == 4) {
1089 }
1090 }
1091}
1092
1094 if (ImageFormatDetails::transparent(this->format)) {
1095 if (ImageFormatDetails::decompressedAlpha(this->format) > 1) {
1098 } else {
1101 }
1102 } else {
1104 }
1105}
1106
1107ImageFormat VTF::getDefaultCompressedFormat(ImageFormat inputFormat, uint32_t version, bool isCubeMap) {
1108 if (version >= 6) {
1109 if (isCubeMap) {
1110 return ImageFormat::BC6H;
1111 }
1112 return ImageFormat::BC7;
1113 }
1114 if (ImageFormatDetails::decompressedAlpha(inputFormat) > 0) {
1115 return ImageFormat::DXT5;
1116 }
1117 return ImageFormat::DXT1;
1118}
1119
1121 return this->format;
1122}
1123
1124void VTF::setFormat(ImageFormat newFormat, ImageConversion::ResizeFilter filter, float quality) {
1125 if (newFormat == FORMAT_UNCHANGED || newFormat == this->format) {
1126 return;
1127 }
1128 if (newFormat == FORMAT_DEFAULT) {
1129 newFormat = VTF::getDefaultCompressedFormat(this->format, this->version, this->getFaceCount() > 1);
1130 }
1131 if (!this->hasImageData()) {
1132 this->format = newFormat;
1133 return;
1134 }
1135 const auto oldFormat = this->format;
1136 if (ImageFormatDetails::compressed(newFormat)) {
1137 this->regenerateImageData(newFormat, this->width + math::paddingForAlignment(4, this->width), this->height + math::paddingForAlignment(4, this->height), this->mipCount, this->frameCount, this->getFaceCount(), this->depth, filter, quality);
1138 } else {
1139 this->regenerateImageData(newFormat, this->width, this->height, this->mipCount, this->frameCount, this->getFaceCount(), this->depth, filter, quality);
1140 }
1141
1142 if (const auto* fallbackResource = this->getResource(Resource::TYPE_FALLBACK_DATA)) {
1143 const auto fallbackConverted = ImageConversion::convertSeveralImageDataToFormat(fallbackResource->data, oldFormat, this->format, ImageDimensions::getMaximumMipCount(this->fallbackWidth, this->fallbackHeight), this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight, 1, quality);
1144 this->setResourceInternal(Resource::TYPE_FALLBACK_DATA, fallbackConverted);
1145 }
1146}
1147
1148uint8_t VTF::getMipCount() const {
1149 return this->mipCount;
1150}
1151
1152bool VTF::setMipCount(uint8_t newMipCount) {
1153 if (!this->hasImageData()) {
1154 return false;
1155 }
1156
1157 if (const auto maxMipCount = ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth); (this->platform != PLATFORM_PC && newMipCount > 1) || (this->platform == PLATFORM_PC && newMipCount > maxMipCount)) {
1158 newMipCount = maxMipCount;
1159 }
1160 this->regenerateImageData(this->format, this->width, this->height, newMipCount, this->frameCount, this->getFaceCount(), this->depth);
1161 return true;
1162}
1163
1165 // Something we could do here for v5 and up is check if FLAG_V5_LOAD_MOST_MIPS is set and change mip count accordingly
1166 // (eliminate anything below 32x32). Noting we can't do that check for v4 and down because TF2 branch added small mip
1167 // loading, didn't bother adding the most mips flag, and doesn't handle VTFs missing lower mips correctly. Thanks Valve!
1168 return this->setMipCount(ImageDimensions::getMaximumMipCount(this->width, this->height, this->depth));
1169}
1170
1172 auto* imageResource = this->getResourceInternal(Resource::TYPE_IMAGE_DATA);
1173 if (!imageResource || !this->hasImageData()) {
1174 return;
1175 }
1176
1177 if (this->mipCount <= 1) {
1178 if (!this->setRecommendedMipCount() || this->mipCount <= 1) {
1179 return;
1180 }
1181 }
1182
1183 auto* outputDataPtr = imageResource->data.data();
1184 const auto faceCount = this->getFaceCount();
1185
1186#ifdef SOURCEPP_BUILD_WITH_THREADS
1187 std::vector<std::future<void>> futures;
1188 futures.reserve(this->frameCount * faceCount * this->depth);
1189#endif
1190 for (int j = 0; j < this->frameCount; j++) {
1191 for (int k = 0; k < faceCount; k++) {
1192#ifdef SOURCEPP_BUILD_WITH_THREADS
1193 futures.push_back(std::async(std::launch::async, [this, filter, outputDataPtr, faceCount, j, k] {
1194#endif
1195 for (int i = 1; i < this->mipCount; i++) {
1196 const auto [mipWidth, mipHeight, mipDepth] = ImageDimensions::getMipDims(i, this->width, this->height, this->depth);
1197 const auto [mipWidthM1, mipHeightM1] = ImageDimensions::getMipDims(i - 1, this->width, this->height);
1198 for (int l = 0; l < mipDepth; l++) {
1199 auto mip = ImageConversion::resizeImageData(this->getImageDataRaw(i - 1, j, k, l), this->format, mipWidthM1, mipWidth, mipHeightM1, mipHeight, this->isSRGB(), filter);
1200 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, l, this->depth) && mip.size() == length) {
1201 std::memcpy(outputDataPtr + offset, mip.data(), length);
1202 }
1203 }
1204 }
1205#ifdef SOURCEPP_BUILD_WITH_THREADS
1206 }));
1207 if (futures.size() >= std::thread::hardware_concurrency()) {
1208 for (auto& future : futures) {
1209 future.get();
1210 }
1211 futures.clear();
1212 }
1213#endif
1214 }
1215 }
1216#ifdef SOURCEPP_BUILD_WITH_THREADS
1217 for (auto& future : futures) {
1218 future.get();
1219 }
1220#endif
1221}
1222
1223uint16_t VTF::getFrameCount() const {
1224 return this->frameCount;
1225}
1226
1227bool VTF::setFrameCount(uint16_t newFrameCount) {
1228 if (!this->hasImageData()) {
1229 return false;
1230 }
1231 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, newFrameCount, this->getFaceCount(), this->depth);
1232 return true;
1233}
1234
1235uint8_t VTF::getFaceCount() const {
1236 if (!this->hasImageData()) {
1237 return 0;
1238 }
1239 const auto* image = this->getResource(Resource::TYPE_IMAGE_DATA);
1240 if (!image) {
1241 return 0;
1242 }
1243 if (!(this->flags & FLAG_V0_ENVMAP)) {
1244 return 1;
1245 }
1246 if (this->platform == PLATFORM_XBOX || this->version >= 6) {
1247 // All v7.6 VTFs are sane, and we need this special case to fix a bug in the parser where
1248 // it won't recognize cubemaps as cubemaps because the image resource is compressed!
1249 return 6;
1250 }
1251 const auto expectedLength = ImageFormatDetails::getDataLength(this->format, this->mipCount, this->frameCount, 6, this->width, this->height, this->depth);
1252 if (this->version >= 1 && this->version <= 4 && expectedLength < image->data.size()) {
1253 return 7;
1254 }
1255 if (expectedLength == image->data.size()) {
1256 return 6;
1257 }
1258 return 1;
1259}
1260
1261bool VTF::setFaceCount(bool isCubeMap) {
1262 if (!this->hasImageData()) {
1263 return false;
1264 }
1265 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, this->frameCount, isCubeMap ? ((this->version >= 1 && this->version <= 4) ? 7 : 6) : 1, this->depth);
1266 return true;
1267}
1268
1269uint16_t VTF::getDepth(uint8_t mip) const {
1270 return ImageDimensions::getMipDim(mip, this->depth);
1271}
1272
1273bool VTF::setDepth(uint16_t newDepth) {
1274 if (!this->hasImageData()) {
1275 return false;
1276 }
1277 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, this->frameCount, this->getFaceCount(), newDepth);
1278 return true;
1279}
1280
1281bool VTF::setFrameFaceAndDepth(uint16_t newFrameCount, bool isCubeMap, uint16_t newDepth) {
1282 if (!this->hasImageData()) {
1283 return false;
1284 }
1285 this->regenerateImageData(this->format, this->width, this->height, this->mipCount, newFrameCount, isCubeMap ? ((this->version >= 1 && this->version <= 4) ? 7 : 6) : 1, newDepth);
1286 return true;
1287}
1288
1289uint16_t VTF::getStartFrame() const {
1290 return this->startFrame;
1291}
1292
1293void VTF::setStartFrame(uint16_t newStartFrame) {
1294 this->startFrame = newStartFrame;
1295}
1296
1297math::Vec3f VTF::getReflectivity() const {
1298 return this->reflectivity;
1299}
1300
1301void VTF::setReflectivity(math::Vec3f newReflectivity) {
1302 this->reflectivity = newReflectivity;
1303}
1304
1306 static constexpr auto getReflectivityForImage = [](const VTF& vtf, uint16_t frame, uint8_t face, uint16_t slice) {
1307 static constexpr auto getReflectivityForPixel = [](const ImagePixel::RGBA8888* pixel) -> math::Vec3f {
1308 // http://markjstock.org/doc/gsd_talk_11_notes.pdf page 11
1309 math::Vec3f ref{static_cast<float>(pixel->r), static_cast<float>(pixel->g), static_cast<float>(pixel->b)};
1310 // I tweaked it a bit to produce 0.85 reflectivity at pure white
1311 ref = ref / 255.f * 0.922f;
1312 ref[0] *= ref[0];
1313 ref[1] *= ref[1];
1314 ref[2] *= ref[2];
1315 return ref;
1316 };
1317
1318 auto rgba8888Data = vtf.getImageDataAsRGBA8888(0, frame, face, slice);
1319 math::Vec3f out{};
1320 for (uint64_t i = 0; i < rgba8888Data.size(); i += 4) {
1321 out += getReflectivityForPixel(reinterpret_cast<ImagePixel::RGBA8888*>(rgba8888Data.data() + i));
1322 }
1323 return out / (rgba8888Data.size() / sizeof(ImagePixel::RGBA8888));
1324 };
1325
1326 const auto faceCount = this->getFaceCount();
1327
1328#ifdef SOURCEPP_BUILD_WITH_THREADS
1329 if (this->frameCount > 1 || faceCount > 1 || this->depth > 1) {
1330 std::vector<std::future<math::Vec3f>> futures;
1331 futures.reserve(this->frameCount * faceCount * this->depth);
1332
1333 this->reflectivity = {};
1334 for (int j = 0; j < this->frameCount; j++) {
1335 for (int k = 0; k < faceCount; k++) {
1336 for (int l = 0; l < this->depth; l++) {
1337 futures.push_back(std::async(std::launch::async, [this, j, k, l] {
1338 return getReflectivityForImage(*this, j, k, l);
1339 }));
1340 if (futures.size() >= std::thread::hardware_concurrency()) {
1341 for (auto& future : futures) {
1342 this->reflectivity += future.get();
1343 }
1344 futures.clear();
1345 }
1346 }
1347 }
1348 }
1349
1350 for (auto& future : futures) {
1351 this->reflectivity += future.get();
1352 }
1353 this->reflectivity /= this->frameCount * faceCount * this->depth;
1354 } else {
1355 this->reflectivity = getReflectivityForImage(*this, 0, 0, 0);
1356 }
1357#else
1358 this->reflectivity = {};
1359 for (int j = 0; j < this->frameCount; j++) {
1360 for (int k = 0; k < faceCount; k++) {
1361 for (int l = 0; l < this->depth; l++) {
1362 this->reflectivity += getReflectivityForImage(*this, j, k, l);
1363 }
1364 }
1365 }
1366 this->reflectivity /= this->frameCount * faceCount * this->depth;
1367#endif
1368}
1369
1371 return this->bumpMapScale;
1372}
1373
1374void VTF::setBumpMapScale(float newBumpMapScale) {
1375 this->bumpMapScale = newBumpMapScale;
1376}
1377
1379 return this->thumbnailFormat;
1380}
1381
1382uint8_t VTF::getThumbnailWidth() const {
1383 return this->thumbnailWidth;
1384}
1385
1387 return this->thumbnailHeight;
1388}
1389
1390uint8_t VTF::getFallbackWidth() const {
1391 return this->fallbackWidth;
1392}
1393
1394uint8_t VTF::getFallbackHeight() const {
1395 return this->fallbackHeight;
1396}
1397
1399 return this->fallbackMipCount;
1400}
1401
1402const std::vector<Resource>& VTF::getResources() const {
1403 return this->resources;
1404}
1405
1407 for (const auto& resource : this->resources) {
1408 if (resource.type == type) {
1409 return &resource;
1410 }
1411 }
1412 return nullptr;
1413}
1414
1416 for (auto& resource : this->resources) {
1417 if (resource.type == type) {
1418 return &resource;
1419 }
1420 }
1421 return nullptr;
1422}
1423
1424void VTF::setResourceInternal(Resource::Type type, std::span<const std::byte> data_) {
1425 if (const auto* resource = this->getResource(type); resource && resource->data.size() == data_.size()) {
1426 std::memcpy(resource->data.data(), data_.data(), data_.size());
1427 return;
1428 }
1429
1430 // Store resource data
1431 std::unordered_map<Resource::Type, std::pair<std::vector<std::byte>, uint64_t>> resourceData;
1432 for (const auto& [type_, flags_, dataSpan] : this->resources) {
1433 resourceData[type_] = {std::vector<std::byte>{dataSpan.begin(), dataSpan.end()}, 0};
1434 }
1435
1436 // Set new resource
1437 if (data_.empty()) {
1438 resourceData.erase(type);
1439 } else {
1440 resourceData[type] = {{data_.begin(), data_.end()}, 0};
1441 }
1442
1443 // Save the data
1444 this->data.clear();
1445 BufferStream writer{this->data};
1446
1447 for (auto resourceType : resourceData | std::views::keys) {
1448 if (!resourceData.contains(resourceType)) {
1449 continue;
1450 }
1451 auto& [specificResourceData, offset] = resourceData[resourceType];
1452 if (resourceType == type) {
1453 Resource newResource{
1454 type,
1455 specificResourceData.size() <= sizeof(uint32_t) ? Resource::FLAG_LOCAL_DATA : Resource::FLAG_NONE,
1456 {this->data.data() + offset, specificResourceData.size()},
1457 };
1458 if (auto* resourcePtr = this->getResourceInternal(type)) {
1459 *resourcePtr = newResource;
1460 } else {
1461 this->resources.push_back(newResource);
1462 }
1463 } else if (!resourceData.contains(resourceType)) {
1464 continue;
1465 }
1466 offset = writer.tell();
1467 writer.write(specificResourceData);
1468 }
1469 this->data.resize(writer.size());
1470
1471 for (auto& [type_, flags_, dataSpan] : this->resources) {
1472 if (resourceData.contains(type_)) {
1473 const auto& [specificResourceData, offset] = resourceData[type_];
1474 dataSpan = {this->data.data() + offset, specificResourceData.size()};
1475 }
1476 }
1477}
1478
1480 std::erase_if(this->resources, [type](const Resource& resource) { return resource.type == type; });
1481}
1482
1483void VTF::regenerateImageData(ImageFormat newFormat, uint16_t newWidth, uint16_t newHeight, uint8_t newMipCount, uint16_t newFrameCount, uint8_t newFaceCount, uint16_t newDepth, ImageConversion::ResizeFilter filter, float quality) {
1484 if (!newWidth) newWidth = 1;
1485 if (!newHeight) newHeight = 1;
1486 if (!newMipCount) newMipCount = 1;
1487 if (!newFrameCount) newFrameCount = 1;
1488 if (!newFaceCount) newFaceCount = 1;
1489 if (!newDepth) newDepth = 1;
1490
1491 if (newMipCount > 1) {
1492 // Valve code doesn't like it when you have compressed mips with padding unless they're lower than 4 on a given dimension!
1493 // We could do some extra work to calculate this better, but I don't care, sorry
1494 if (ImageFormatDetails::compressed(newFormat) && ((newWidth > 4 && !std::has_single_bit(newWidth)) || (newHeight > 4 && !std::has_single_bit(newHeight)))) {
1495 newMipCount = 1;
1496 }
1497
1498 this->flags &= ~(FLAG_V0_NO_MIP | FLAG_V0_NO_LOD);
1499 } else {
1501 }
1502
1503 const auto faceCount = this->getFaceCount();
1504 if (this->format == newFormat && this->width == newWidth && this->height == newHeight && this->mipCount == newMipCount && this->frameCount == newFrameCount && faceCount == newFaceCount && this->depth == newDepth) {
1505 return;
1506 }
1507
1508 std::vector<std::byte> newImageData;
1509 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
1510 if (this->format != newFormat && this->width == newWidth && this->height == newHeight && this->mipCount == newMipCount && this->frameCount == newFrameCount && faceCount == newFaceCount && this->depth == newDepth) {
1511 newImageData = ImageConversion::convertSeveralImageDataToFormat(imageResource->data, this->format, newFormat, this->mipCount, this->frameCount, faceCount, this->width, this->height, this->depth, quality);
1512 } else {
1513 newImageData.resize(ImageFormatDetails::getDataLength(this->format, newMipCount, newFrameCount, newFaceCount, newWidth, newHeight, newDepth));
1514 for (int i = newMipCount - 1; i >= 0; i--) {
1515 const auto [newMipWidth, newMipHeight, newMipDepth] = ImageDimensions::getMipDims(i, newWidth, newHeight, newDepth);
1516
1517 int sourceMipIndex = 0;
1518 for (int mip = 0, bestScore = std::numeric_limits<int>::max(); mip < this->mipCount; mip++) {
1519 const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(mip, this->width, this->height, false);
1520 if (mipWidth == newMipWidth && mipHeight == newMipHeight) {
1521 break;
1522 }
1523 const auto widthDiff = static_cast<int>(mipWidth) - static_cast<int>(newMipWidth);
1524 const auto heightDiff = static_cast<int>(mipHeight) - static_cast<int>(newMipHeight);
1525 if (widthDiff < 0 || heightDiff < 0) {
1526 continue;
1527 }
1528 if (const auto score = widthDiff + heightDiff; score < bestScore) {
1529 bestScore = score;
1530 sourceMipIndex = mip;
1531 }
1532 }
1533 const auto [sourceMipWidth, sourceMipHeight] = ImageDimensions::getMipDims(sourceMipIndex, this->width, this->height);
1534
1535 for (int j = 0; j < newFrameCount; j++) {
1536 for (int k = 0; k < newFaceCount; k++) {
1537 for (int l = 0; l < newMipDepth; l++) {
1538 if (j < this->frameCount && k < faceCount && l < this->depth) {
1539 auto imageSpan = this->getImageDataRaw(sourceMipIndex, j, k, l);
1540 std::vector<std::byte> imageBacking;
1541 if (sourceMipWidth != newMipWidth || sourceMipHeight != newMipHeight) {
1542 imageBacking = ImageConversion::resizeImageData(imageSpan, this->format, sourceMipWidth, newMipWidth, sourceMipHeight, newMipHeight, this->isSRGB(), filter);
1543 imageSpan = imageBacking;
1544 }
1545 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, newMipCount, j, newFrameCount, k, newFaceCount, newWidth, newHeight, l, newDepth) && imageSpan.size() == length) {
1546 std::memcpy(newImageData.data() + offset, imageSpan.data(), length);
1547 }
1548 }
1549 }
1550 }
1551 }
1552 }
1553 if (this->format != newFormat) {
1554 newImageData = ImageConversion::convertSeveralImageDataToFormat(newImageData, this->format, newFormat, newMipCount, newFrameCount, newFaceCount, newWidth, newHeight, newDepth, quality);
1555 }
1556 }
1557 } else {
1558 newImageData.resize(ImageFormatDetails::getDataLength(newFormat, newMipCount, newFrameCount, newFaceCount, newWidth, newHeight, newDepth));
1559 }
1560
1561 this->format = newFormat;
1562 this->width = newWidth;
1563 this->height = newHeight;
1564 this->mipCount = newMipCount;
1565 this->frameCount = newFrameCount;
1566 if (newFaceCount > 1) {
1567 this->flags |= FLAG_V0_ENVMAP;
1568 } else {
1569 this->flags &= ~FLAG_V0_ENVMAP;
1570 }
1571 this->depth = newDepth;
1572
1574
1575 // Fix up XBOX resources that depend on image resource
1576 if (const auto* palette = this->getResource(Resource::TYPE_PALETTE_DATA)) {
1577 const auto targetSize = 256 * sizeof(ImagePixel::BGRA8888) * this->frameCount;
1578 if (palette->data.size() != targetSize) {
1579 std::vector<std::byte> paletteData{palette->data.begin(), palette->data.end()};
1580 paletteData.resize(targetSize);
1582 }
1583 }
1584 if (this->hasFallbackData()) {
1585 this->removeFallback();
1586 this->computeFallback();
1587 }
1588}
1589
1590std::vector<std::byte> VTF::getPaletteResourceFrame(uint16_t frame) const {
1591 if (const auto* palette = this->getResource(Resource::TYPE_PALETTE_DATA)) {
1592 return palette->getDataAsPalette(frame);
1593 }
1594 return {};
1595}
1596
1597std::vector<std::byte> VTF::getParticleSheetFrameDataRaw(uint16_t& spriteWidth, uint16_t& spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1598 spriteWidth = 0;
1599 spriteHeight = 0;
1600
1601 const auto shtResource = this->getResource(Resource::TYPE_PARTICLE_SHEET_DATA);
1602 if (!shtResource) {
1603 return {};
1604 }
1605
1606 auto sht = shtResource->getDataAsParticleSheet();
1607 const auto* sequence = sht.getSequenceFromID(shtSequenceID);
1608 if (!sequence || sequence->frames.size() <= shtFrame || shtBounds >= sht.getFrameBoundsCount()) {
1609 return {};
1610 }
1611
1612 // These values are likely slightly too large thanks to float magic, use a
1613 // shader that scales UVs instead of this function if precision is a concern
1614 // This will also break if any of the bounds are above 1 or below 0, but that
1615 // hasn't been observed in official textures
1616 const auto& bounds = sequence->frames[shtFrame].bounds[shtBounds];
1617 uint16_t x1 = std::clamp<uint16_t>(std::floor(bounds.x1 * static_cast<float>(this->getWidth(mip))), 0, this->getWidth(mip));
1618 uint16_t y1 = std::clamp<uint16_t>(std::ceil( bounds.y1 * static_cast<float>(this->getHeight(mip))), 0, this->getHeight(mip));
1619 uint16_t x2 = std::clamp<uint16_t>(std::ceil( bounds.x2 * static_cast<float>(this->getWidth(mip))), 0, this->getHeight(mip));
1620 uint16_t y2 = std::clamp<uint16_t>(std::floor(bounds.y2 * static_cast<float>(this->getHeight(mip))), 0, this->getWidth(mip));
1621
1622 if (x1 > x2) [[unlikely]] {
1623 std::swap(x1, x2);
1624 }
1625 if (y1 > y2) [[unlikely]] {
1626 std::swap(y1, y2);
1627 }
1628 spriteWidth = x2 - x1;
1629 spriteWidth = y2 - y1;
1630
1631 const auto out = ImageConversion::cropImageData(this->getImageDataRaw(mip, frame, face, slice), this->getFormat(), this->getWidth(mip), spriteWidth, x1, this->getHeight(mip), spriteHeight, y1);
1632 if (out.empty()) {
1633 spriteWidth = 0;
1634 spriteHeight = 0;
1635 }
1636 return out;
1637}
1638
1639std::vector<std::byte> VTF::getParticleSheetFrameDataAs(ImageFormat newFormat, uint16_t& spriteWidth, uint16_t& spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1640 return ImageConversion::convertImageDataToFormat(this->getParticleSheetFrameDataRaw(spriteWidth, spriteHeight, shtSequenceID, shtFrame, shtBounds, mip, frame, face, slice), this->getFormat(), newFormat, spriteWidth, spriteHeight);
1641}
1642
1643std::vector<std::byte> VTF::getParticleSheetFrameDataAsRGBA8888(uint16_t& spriteWidth, uint16_t& spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1644 return this->getParticleSheetFrameDataAs(ImageFormat::RGBA8888, spriteWidth, spriteHeight, shtSequenceID, shtFrame, shtBounds, mip, frame, face, slice);
1645}
1646
1648 std::vector<std::byte> particleSheetData;
1649 BufferStream writer{particleSheetData};
1650
1651 const auto bakedSheet = value.bake();
1652 writer.write<uint32_t>(bakedSheet.size()).write(bakedSheet);
1653 particleSheetData.resize(writer.size());
1654
1656}
1657
1661
1662void VTF::setCRCResource(uint32_t value) {
1663 this->setResourceInternal(Resource::TYPE_CRC, {reinterpret_cast<const std::byte*>(&value), sizeof(value)});
1664}
1665
1669
1670void VTF::setLODResource(uint8_t u, uint8_t v, uint8_t u360, uint8_t v360) {
1671 uint32_t lodData;
1672 BufferStream writer{&lodData, sizeof(lodData)};
1673
1674 writer << u << v << u360 << v360;
1675
1676 this->setResourceInternal(Resource::TYPE_LOD_CONTROL_INFO, {reinterpret_cast<const std::byte*>(&lodData), sizeof(lodData)});
1677}
1678
1682
1683void VTF::setExtendedFlagsResource(uint32_t value) {
1684 this->setResourceInternal(Resource::TYPE_EXTENDED_FLAGS, {reinterpret_cast<const std::byte*>(&value), sizeof(value)});
1685}
1686
1690
1691void VTF::setKeyValuesDataResource(std::string_view value) {
1692 std::vector<std::byte> keyValuesData;
1693 BufferStream writer{keyValuesData};
1694
1695 writer.write<uint32_t>(value.size()).write(value, false);
1696 keyValuesData.resize(writer.size());
1697
1699}
1700
1704
1706 std::vector<std::byte> hotspotData;
1707 BufferStream writer{hotspotData};
1708
1709 const auto bakedHotspotData = value.bake();
1710 writer.write<uint32_t>(bakedHotspotData.size()).write(bakedHotspotData);
1711 hotspotData.resize(writer.size());
1712
1714}
1715
1719
1721 return this->compressionLevel;
1722}
1723
1724void VTF::setCompressionLevel(int16_t newCompressionLevel) {
1725 this->compressionLevel = newCompressionLevel;
1726}
1727
1731
1733 if (newCompressionMethod == CompressionMethod::CONSOLE_LZMA && this->platform == PLATFORM_PC) {
1735 } else if (newCompressionMethod != CompressionMethod::CONSOLE_LZMA && this->platform != PLATFORM_PC) {
1737 } else {
1738 this->compressionMethod = newCompressionMethod;
1739 }
1740}
1741
1742bool VTF::hasImageData() const {
1743 return this->format != ImageFormat::EMPTY && this->width > 0 && this->height > 0;
1744}
1745
1746std::span<const std::byte> VTF::getImageDataRaw(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1747 if (const auto imageResource = this->getResource(Resource::TYPE_IMAGE_DATA)) {
1748 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, mip, this->mipCount, frame, this->frameCount, face, this->getFaceCount(), this->width, this->height, slice, this->depth)) {
1749 return imageResource->data.subspan(offset, length);
1750 }
1751 }
1752 return {};
1753}
1754
1755std::vector<std::byte> VTF::getImageDataAs(ImageFormat newFormat, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1756 const auto rawImageData = this->getImageDataRaw(mip, frame, face, slice);
1757 if (rawImageData.empty()) {
1758 return {};
1759 }
1760 if (this->format == ImageFormat::P8) {
1761 if (const auto paletteData = this->getPaletteResourceFrame(frame); !paletteData.empty()) {
1762 const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(mip, this->width, this->height);
1763 return ImageConversion::convertImageDataToFormat(ImageQuantize::convertP8ImageDataToBGRA8888(paletteData, rawImageData), ImageFormat::BGRA8888, newFormat, mipWidth, mipHeight);
1764 }
1765 }
1766 const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(mip, this->width, this->height);
1767 return ImageConversion::convertImageDataToFormat(rawImageData, this->format, newFormat, mipWidth, mipHeight);
1768}
1769
1770std::vector<std::byte> VTF::getImageDataAsRGBA8888(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice) const {
1771 return this->getImageDataAs(ImageFormat::RGBA8888, mip, frame, face, slice);
1772}
1773
1774bool VTF::setImage(std::span<const std::byte> imageData_, ImageFormat format_, uint16_t width_, uint16_t height_, ImageConversion::ResizeFilter filter, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice, float quality) {
1775 if (imageData_.empty()) {
1776 return false;
1777 }
1778
1779 if (!this->hasImageData()) {
1780 uint16_t resizedWidth = width_, resizedHeight = height_;
1781 ImageConversion::setResizedDims(resizedWidth, this->imageWidthResizeMethod, resizedHeight, this->imageHeightResizeMethod);
1782 if (ImageFormatDetails::compressed(format_)) {
1783 resizedWidth += math::paddingForAlignment(4, resizedWidth);
1784 resizedHeight += math::paddingForAlignment(4, resizedHeight);
1785 }
1786 if (const auto newMipCount = ImageDimensions::getMaximumMipCount(resizedWidth, resizedHeight, this->depth); newMipCount <= mip) {
1787 mip = newMipCount - 1;
1788 }
1789 if (face > 6 || (face == 6 && (this->platform == PLATFORM_XBOX || this->version < 1 || this->version > 4))) {
1790 return false;
1791 }
1792 this->regenerateImageData(format_, resizedWidth, resizedHeight, mip + 1, frame + 1, face ? (face < 6 ? 6 : face) : 0, slice + 1);
1793 }
1794
1795 const auto faceCount = this->getFaceCount();
1796 if (this->mipCount <= mip || this->frameCount <= frame || faceCount <= face || this->depth <= slice) {
1797 return false;
1798 }
1799
1800 const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA);
1801 if (!imageResource) {
1802 return false;
1803 }
1804 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, mip, this->mipCount, frame, this->frameCount, face, faceCount, this->width, this->height, slice, this->depth)) {
1805 std::vector<std::byte> image{imageData_.begin(), imageData_.end()};
1806 const auto [newWidth, newHeight] = ImageDimensions::getMipDims(mip, this->width, this->height);
1807 if (width_ != newWidth || height_ != newHeight) {
1808 image = ImageConversion::resizeImageData(image, format_, width_, newWidth, height_, newHeight, this->isSRGB(), filter);
1809 }
1810 if (format_ != this->format) {
1811 image = ImageConversion::convertImageDataToFormat(image, format_, this->format, newWidth, newHeight, quality);
1812 }
1813 std::memcpy(imageResource->data.data() + offset, image.data(), image.size());
1814 }
1815 return true;
1816}
1817
1818bool VTF::setImage(const std::filesystem::path& imagePath, ImageConversion::ResizeFilter filter, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice, float quality) {
1819 ImageFormat inputFormat;
1820 int inputWidth, inputHeight, inputFrameCount;
1821 auto imageData_ = ImageConversion::convertFileToImageData(fs::readFileBuffer(imagePath), inputFormat, inputWidth, inputHeight, inputFrameCount);
1822
1823 // Unable to decode file
1824 if (imageData_.empty() || inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) {
1825 return false;
1826 }
1827
1828 // One frame (normal)
1829 if (inputFrameCount == 1) {
1830 return this->setImage(imageData_, inputFormat, inputWidth, inputHeight, filter, mip, frame, face, slice, quality);
1831 }
1832
1833 // Multiple frames (GIF)
1834 bool allSuccess = true;
1835 const auto frameSize = ImageFormatDetails::getDataLength(inputFormat, inputWidth, inputHeight);
1836 for (int currentFrame = 0; currentFrame < inputFrameCount; currentFrame++) {
1837 if (!this->setImage({imageData_.data() + currentFrame * frameSize, imageData_.data() + currentFrame * frameSize + frameSize}, inputFormat, inputWidth, inputHeight, filter, mip, frame + currentFrame, face, slice, quality)) {
1838 allSuccess = false;
1839 }
1840 if (currentFrame == 0 && this->frameCount < frame + inputFrameCount) {
1841 // Call this after setting the first image, this function is a no-op if no image data is present yet
1842 this->setFrameCount(frame + inputFrameCount);
1843 }
1844 }
1845 return allSuccess;
1846}
1847
1848std::vector<std::byte> VTF::saveImageToFile(uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice, ImageConversion::FileFormat fileFormat) const {
1849 return ImageConversion::convertImageDataToFile(this->getImageDataRaw(mip, frame, face, slice), this->format, this->getWidth(mip), this->getHeight(mip), fileFormat);
1850}
1851
1852bool VTF::saveImageToFile(const std::filesystem::path& imagePath, uint8_t mip, uint16_t frame, uint8_t face, uint16_t slice, ImageConversion::FileFormat fileFormat) const {
1853 if (auto data_ = this->saveImageToFile(mip, frame, face, slice, fileFormat); !data_.empty()) {
1854 return fs::writeFileBuffer(imagePath, data_);
1855 }
1856 return false;
1857}
1858
1860 return this->thumbnailFormat != ImageFormat::EMPTY && this->thumbnailWidth > 0 && this->thumbnailHeight > 0;
1861}
1862
1863std::span<const std::byte> VTF::getThumbnailDataRaw() const {
1864 if (const auto thumbnailResource = this->getResource(Resource::TYPE_THUMBNAIL_DATA)) {
1865 return thumbnailResource->data;
1866 }
1867 return {};
1868}
1869
1870std::vector<std::byte> VTF::getThumbnailDataAs(ImageFormat newFormat) const {
1871 const auto rawThumbnailData = this->getThumbnailDataRaw();
1872 if (rawThumbnailData.empty()) {
1873 return {};
1874 }
1875 return ImageConversion::convertImageDataToFormat(rawThumbnailData, this->thumbnailFormat, newFormat, this->thumbnailWidth, this->thumbnailHeight);
1876}
1877
1878std::vector<std::byte> VTF::getThumbnailDataAsRGBA8888() const {
1880}
1881
1882void VTF::setThumbnail(std::span<const std::byte> imageData_, ImageFormat format_, uint16_t width_, uint16_t height_, float quality) {
1883 if (format_ != this->thumbnailFormat) {
1884 this->setResourceInternal(Resource::TYPE_THUMBNAIL_DATA, ImageConversion::convertImageDataToFormat(imageData_, format_, this->thumbnailFormat, width_, height_, quality));
1885 } else {
1887 }
1888 this->thumbnailWidth = width_;
1889 this->thumbnailHeight = height_;
1890}
1891
1892bool VTF::setThumbnail(const std::filesystem::path& imagePath, float quality) {
1893 ImageFormat inputFormat;
1894 int inputWidth, inputHeight, inputFrameCount;
1895 auto imageData_ = ImageConversion::convertFileToImageData(fs::readFileBuffer(imagePath), inputFormat, inputWidth, inputHeight, inputFrameCount);
1896
1897 // Unable to decode file
1898 if (imageData_.empty() || inputFormat == ImageFormat::EMPTY || !inputWidth || !inputHeight || !inputFrameCount) {
1899 return false;
1900 }
1901
1902 // One frame (normal)
1903 if (inputFrameCount == 1) {
1904 this->setThumbnail(imageData_, inputFormat, inputWidth, inputHeight, quality);
1905 return true;
1906 }
1907
1908 // Multiple frames (GIF) - we will just use the first one
1909 const auto frameSize = ImageFormatDetails::getDataLength(inputFormat, inputWidth, inputHeight);
1910 this->setThumbnail({imageData_.data(), frameSize}, inputFormat, inputWidth, inputHeight, quality);
1911 return true;
1912}
1913
1915 if (!this->hasImageData()) {
1916 return;
1917 }
1918
1919 if (this->platform == PLATFORM_XBOX) {
1921 this->thumbnailWidth = 1;
1922 this->thumbnailHeight = 1;
1923 std::array newThumbnail{
1924 static_cast<std::byte>(static_cast<uint8_t>(std::clamp(this->reflectivity[0], 0.f, 1.f) * 255.f)),
1925 static_cast<std::byte>(static_cast<uint8_t>(std::clamp(this->reflectivity[1], 0.f, 1.f) * 255.f)),
1926 static_cast<std::byte>(static_cast<uint8_t>(std::clamp(this->reflectivity[2], 0.f, 1.f) * 255.f)),
1927 };
1929 } else {
1931 this->thumbnailWidth = 16;
1932 this->thumbnailHeight = 16;
1933 this->setResourceInternal(Resource::TYPE_THUMBNAIL_DATA, ImageConversion::convertImageDataToFormat(ImageConversion::resizeImageData(this->getImageDataRaw(), this->format, this->width, this->thumbnailWidth, this->height, this->thumbnailHeight, this->isSRGB(), filter), this->format, this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight, quality));
1934 }
1935}
1936
1939 this->thumbnailWidth = 0;
1940 this->thumbnailHeight = 0;
1942}
1943
1944std::vector<std::byte> VTF::saveThumbnailToFile(ImageConversion::FileFormat fileFormat) const {
1945 return ImageConversion::convertImageDataToFile(this->getThumbnailDataRaw(), this->thumbnailFormat, this->thumbnailWidth, this->thumbnailHeight, fileFormat);
1946}
1947
1948bool VTF::saveThumbnailToFile(const std::filesystem::path& imagePath, ImageConversion::FileFormat fileFormat) const {
1949 if (auto data_ = this->saveThumbnailToFile(fileFormat); !data_.empty()) {
1950 return fs::writeFileBuffer(imagePath, data_);
1951 }
1952 return false;
1953}
1954
1956 return this->fallbackWidth > 0 && this->fallbackHeight > 0 && this->fallbackMipCount > 0;
1957}
1958
1959std::span<const std::byte> VTF::getFallbackDataRaw(uint8_t mip, uint16_t frame, uint8_t face) const {
1960 if (const auto fallbackResource = this->getResource(Resource::TYPE_FALLBACK_DATA)) {
1961 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, mip, this->fallbackMipCount, frame, this->frameCount, face, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight)) {
1962 return fallbackResource->data.subspan(offset, length);
1963 }
1964 }
1965 return {};
1966}
1967
1968std::vector<std::byte> VTF::getFallbackDataAs(ImageFormat newFormat, uint8_t mip, uint16_t frame, uint8_t face) const {
1969 const auto rawFallbackData = this->getFallbackDataRaw(mip, frame, face);
1970 if (rawFallbackData.empty()) {
1971 return {};
1972 }
1973 if (this->format == ImageFormat::P8) {
1974 if (const auto paletteData = this->getPaletteResourceFrame(frame); !paletteData.empty()) {
1976 }
1977 }
1978 return ImageConversion::convertImageDataToFormat(rawFallbackData, this->format, newFormat, this->fallbackWidth, this->fallbackHeight);
1979}
1980
1981std::vector<std::byte> VTF::getFallbackDataAsRGBA8888(uint8_t mip, uint16_t frame, uint8_t face) const {
1982 return this->getFallbackDataAs(ImageFormat::RGBA8888, mip, frame, face);
1983}
1984
1986 if (!this->hasImageData()) {
1987 return;
1988 }
1989
1990 const auto faceCount = this->getFaceCount();
1991
1992 this->fallbackWidth = 8;
1993 this->fallbackHeight = 8;
1995
1996 std::vector<std::byte> fallbackData;
1997 fallbackData.resize(ImageFormatDetails::getDataLength(this->format, this->fallbackMipCount, this->frameCount, faceCount, this->fallbackWidth, this->fallbackHeight));
1998 for (int i = this->fallbackMipCount - 1; i >= 0; i--) {
1999 const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(i, this->fallbackWidth, this->fallbackHeight);
2000 for (int j = 0; j < this->frameCount; j++) {
2001 for (int k = 0; k < faceCount; k++) {
2002 auto mip = ImageConversion::resizeImageData(this->getImageDataRaw(0, j, k, 0), this->format, this->width, mipWidth, this->height, mipHeight, this->isSRGB(), filter);
2003 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, this->fallbackMipCount, j, this->frameCount, k, faceCount, this->fallbackWidth, this->fallbackHeight) && mip.size() == length) {
2004 std::memcpy(fallbackData.data() + offset, mip.data(), length);
2005 }
2006 }
2007 }
2008 }
2010}
2011
2013 this->fallbackWidth = 0;
2014 this->fallbackHeight = 0;
2015 this->fallbackMipCount = 0;
2017}
2018
2019std::vector<std::byte> VTF::saveFallbackToFile(uint8_t mip, uint16_t frame, uint8_t face, ImageConversion::FileFormat fileFormat) const {
2020 const auto [mipWidth, mipHeight] = ImageDimensions::getMipDims(mip, this->fallbackWidth, this->fallbackHeight);
2021 return ImageConversion::convertImageDataToFile(this->getFallbackDataRaw(mip, frame, face), this->format, mipWidth, mipHeight, fileFormat);
2022}
2023
2024bool VTF::saveFallbackToFile(const std::filesystem::path& imagePath, uint8_t mip, uint16_t frame, uint8_t face, ImageConversion::FileFormat fileFormat) const {
2025 if (auto data_ = this->saveFallbackToFile(mip, frame, face, fileFormat); !data_.empty()) {
2026 return fs::writeFileBuffer(imagePath, data_);
2027 }
2028 return false;
2029}
2030
2032 return this->consoleMipScale;
2033}
2034
2035void VTF::setConsoleMipScale(uint8_t consoleMipScale_) {
2036 this->consoleMipScale = consoleMipScale_;
2037}
2038
2039uint64_t VTF::estimateBakeSize() const {
2040 bool isExact;
2041 return this->estimateBakeSize(isExact);
2042}
2043
2044uint64_t VTF::estimateBakeSize(bool& isExact) const {
2045 isExact = true;
2046 uint64_t vtfSize = 0;
2047
2048 switch (this->platform) {
2049 case PLATFORM_UNKNOWN:
2050 break;
2051 case PLATFORM_PC:
2052 switch (this->version) {
2053 case 6:
2054 if (this->compressionLevel > 0) {
2055 vtfSize += sizeof(uint64_t); // AXC header size
2056 vtfSize += sizeof(uint32_t) * (2 + this->mipCount * this->frameCount * this->getFaceCount()); // AXC resource size
2057 }
2058 case 5:
2059 case 4:
2060 case 3:
2061 vtfSize += 11 + sizeof(uint32_t) + sizeof(uint64_t) * this->getResources().size(); // resources header size
2062 for (const auto& [resourceType, resourceFlags, resourceData] : this->getResources()) {
2063 if (!(resourceFlags & Resource::FLAG_LOCAL_DATA) && resourceType != Resource::TYPE_THUMBNAIL_DATA && resourceType != Resource::TYPE_IMAGE_DATA) {
2064 vtfSize += resourceData.size(); // resource size
2065 }
2066 }
2067 case 2:
2068 vtfSize += sizeof(uint16_t); // depth header field
2069 case 1:
2070 case 0:
2071 vtfSize += sizeof(uint32_t) * 9 + sizeof(float) * 4 + sizeof(uint16_t) * 4 + sizeof(uint8_t) * 3; // header fields
2072 if (this->version < 3) {
2073 vtfSize += math::paddingForAlignment(16, vtfSize); // align to 16 bytes
2074 }
2075
2076 if (const auto* thumbnailResource = this->getResource(Resource::TYPE_THUMBNAIL_DATA); thumbnailResource && this->hasThumbnailData()) {
2077 vtfSize += thumbnailResource->data.size(); // thumbnail size
2078 }
2079 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
2081 vtfSize += imageResource->data.size(); // uncompressed image size
2082 } else {
2083 isExact = false;
2084 vtfSize += static_cast<uint64_t>(static_cast<double>(imageResource->data.size()) / 1.75); // compressed image data
2085 }
2086 }
2087 break;
2088 default:
2089 break;
2090 }
2091 break;
2092 case PLATFORM_XBOX:
2093 vtfSize += sizeof(uint32_t) * 6 + sizeof(float) * 4 + sizeof(uint16_t) * 6 + sizeof(uint8_t) * 6; // header fields
2094
2095 if (const auto* thumbnailResource = this->getResource(Resource::TYPE_THUMBNAIL_DATA); thumbnailResource && this->hasThumbnailData()) {
2096 vtfSize += thumbnailResource->data.size(); // thumbnail size
2097 }
2098 if (this->format == ImageFormat::P8) {
2099 if (const auto* paletteResource = this->getResource(Resource::TYPE_PALETTE_DATA)) {
2100 vtfSize += paletteResource->data.size(); // palette size
2101 }
2102 }
2103 if (const auto* fallbackResource = this->getResource(Resource::TYPE_FALLBACK_DATA); fallbackResource && this->hasFallbackData()) {
2104 vtfSize += ImageFormatDetails::getDataLengthXBOX(false, this->format, this->fallbackMipCount, this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight); // fallback size
2105 vtfSize += math::paddingForAlignment(512, vtfSize); // align to 512 bytes if fallback present
2106 }
2107 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
2108 vtfSize += ImageFormatDetails::getDataLengthXBOX(true, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->depth); // image size
2109 }
2110 if (vtfSize > 512) {
2111 vtfSize += math::paddingForAlignment(512, vtfSize); // align to 512 bytes if longer than 512 bytes
2112 }
2113 break;
2115 vtfSize += sizeof(uint32_t); // align to 16 bytes
2116 case PLATFORM_X360:
2118 vtfSize += sizeof(uint32_t) * 7 + sizeof(float) * 4 + sizeof(uint16_t) * 5 + sizeof(uint8_t) * 6; // header fields
2119 vtfSize += sizeof(uint64_t) * this->getResources().size(); // resources header size
2120
2121 for (const auto& [resourceType, resourceFlags, resourceData] : this->getResources()) {
2122 if (!(resourceFlags & Resource::FLAG_LOCAL_DATA) && resourceType != Resource::TYPE_IMAGE_DATA) {
2123 vtfSize += resourceData.size(); // resource size
2124 }
2125 }
2126 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
2128 vtfSize += imageResource->data.size(); // uncompressed image data
2129 } else {
2130 isExact = false;
2131 vtfSize += static_cast<uint64_t>(static_cast<double>(imageResource->data.size()) / 1.75); // compressed image data
2132 }
2133 }
2134 break;
2135 }
2136 return vtfSize;
2137}
2138
2139std::vector<std::byte> VTF::bake() const {
2140 std::vector<std::byte> out;
2141 BufferStream writer{out};
2142
2143 const auto writeResources = [&writer](uint64_t headerLengthPos, const std::vector<Resource>& sortedResources) -> uint64_t {
2144 auto resourceHeaderCurPos = writer.tell();
2145 writer.pad<uint64_t>(sortedResources.size());
2146 auto resourceDataCurPos = writer.tell();
2147 writer.seek_u(headerLengthPos).write<uint32_t>(resourceDataCurPos);
2148
2149 uint64_t resourceHeaderImagePos = 0;
2150 for (const auto& resource : sortedResources) {
2151 writer.seek_u(resourceHeaderCurPos);
2152
2153 uint32_t resourceType = resource.type;
2154 if (resource.flags & Resource::FLAG_LOCAL_DATA) {
2155 resourceType |= Resource::FLAG_LOCAL_DATA << 24;
2156 }
2157 if (writer.is_big_endian()) {
2158 // type threeCC is little-endian
2159 BufferStream::swap_endian(&resourceType);
2160 }
2161 writer.write<uint32_t>(resourceType);
2162
2163 if (resource.type == Resource::TYPE_IMAGE_DATA) {
2164 resourceHeaderImagePos = writer.tell();
2165 writer.write<uint32_t>(0);
2166 resourceHeaderCurPos = writer.tell();
2167 continue;
2168 }
2169
2170 if (resource.flags & Resource::FLAG_LOCAL_DATA) {
2171 writer.write(resource.data);
2172 resourceHeaderCurPos = writer.tell();
2173 } else {
2174 writer.write<uint32_t>(resourceDataCurPos);
2175 resourceHeaderCurPos = writer.tell();
2176 writer.seek_u(resourceDataCurPos).write(resource.data);
2177 resourceDataCurPos = writer.tell();
2178 }
2179 }
2180 if (resourceHeaderImagePos) {
2181 writer.seek_u(resourceHeaderImagePos).write<uint32_t>(resourceDataCurPos);
2182 for (auto& resource : sortedResources) {
2183 if (resource.type == Resource::TYPE_IMAGE_DATA) {
2184 writer.seek_u(resourceDataCurPos).write(resource.data);
2185 break;
2186 }
2187 }
2188 return resourceDataCurPos;
2189 }
2190 return 0;
2191 };
2192
2193 // HACK: no source game supports this format, but they do support the flag with reg. DXT1
2194 auto bakeFormat = this->format;
2195 auto bakeFlags = this->flags;
2196 if (bakeFormat == ImageFormat::DXT1_ONE_BIT_ALPHA) {
2197 bakeFormat = ImageFormat::DXT1;
2198 bakeFlags |= FLAG_V0_ONE_BIT_ALPHA;
2199 }
2200 // HACK: NV_NULL / ATI2N / ATI1N are at a different position based on engine branch
2201 if (this->version < 5 && (this->format == ImageFormat::EMPTY || this->format == ImageFormat::ATI2N || this->format == ImageFormat::ATI1N)) {
2202 bakeFormat = static_cast<ImageFormat>(static_cast<int32_t>(this->format) + 3);
2203 }
2204
2205 switch (this->platform) {
2206 case PLATFORM_UNKNOWN:
2207 return out;
2208 case PLATFORM_PC: {
2209 writer
2210 .write(VTF_SIGNATURE)
2211 .write<int32_t>(7)
2212 .write(this->version);
2213
2214 const auto headerLengthPos = writer.tell();
2215 writer.write<uint32_t>(0);
2216
2217 writer
2218 .write(this->width)
2219 .write(this->height)
2220 .write(bakeFlags)
2221 .write(this->frameCount)
2222 .write(this->startFrame)
2223 .pad<uint32_t>()
2224 .write(this->reflectivity)
2225 .pad<uint32_t>()
2226 .write(this->bumpMapScale)
2227 .write(bakeFormat)
2228 .write(this->mipCount)
2229 .write(this->thumbnailFormat)
2230 .write(this->thumbnailWidth)
2231 .write(this->thumbnailHeight);
2232
2233 if (this->version >= 2) {
2234 writer << this->depth;
2235 }
2236
2237 if (this->version < 3) {
2238 writer.pad(math::paddingForAlignment(16, writer.tell()));
2239 const auto headerSize = writer.tell();
2240 writer.seek_u(headerLengthPos).write<uint32_t>(headerSize).seek_u(headerSize);
2241
2242 if (const auto* thumbnailResource = this->getResource(Resource::TYPE_THUMBNAIL_DATA); thumbnailResource && this->hasThumbnailData()) {
2243 writer.write(thumbnailResource->data);
2244 }
2245 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
2246 writer.write(imageResource->data);
2247 }
2248 } else {
2249 std::vector<std::byte> auxCompressionResourceData;
2250 std::vector<std::byte> compressedImageResourceData;
2251 bool hasAuxCompression = false;
2252 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA)) {
2253 hasAuxCompression = this->version >= 6 && this->compressionLevel != 0;
2254 if (hasAuxCompression) {
2255 const auto faceCount = this->getFaceCount();
2256 auxCompressionResourceData.resize((this->mipCount * this->frameCount * faceCount + 2) * sizeof(uint32_t));
2257 BufferStream auxWriter{auxCompressionResourceData, false};
2258
2259 // Format of aux resource is as follows, with each item of unspecified type being a 4 byte integer:
2260 // - Size of resource in bytes, not counting this int
2261 // - Compression level, method (2 byte integers)
2262 // - (X times) Size of each mip-face-frame combo
2263 auxWriter
2264 .write<uint32_t>(auxCompressionResourceData.size() - sizeof(uint32_t))
2265 .write(this->compressionLevel)
2266 .write(this->compressionMethod);
2267
2268 for (int i = this->mipCount - 1; i >= 0; i--) {
2269 for (int j = 0; j < this->frameCount; j++) {
2270 for (int k = 0; k < faceCount; k++) {
2271 if (uint32_t offset, length; ImageFormatDetails::getDataPosition(offset, length, this->format, i, this->mipCount, j, this->frameCount, k, faceCount, this->width, this->height, 0, this->depth)) {
2272 auto compressedData = ::compressData({imageResource->data.data() + offset, length * this->depth}, this->compressionLevel, this->compressionMethod);
2273 compressedImageResourceData.insert(compressedImageResourceData.end(), compressedData.begin(), compressedData.end());
2274 auxWriter.write<uint32_t>(compressedData.size());
2275 }
2276 }
2277 }
2278 }
2279 }
2280 }
2281
2282 writer.pad(3).write<uint32_t>(this->getResources().size() + hasAuxCompression).pad(8);
2283
2284 std::vector<Resource> sortedResources = this->getResources();
2285 if (hasAuxCompression) {
2286 for (auto& resource : sortedResources) {
2287 if (resource.type == Resource::TYPE_IMAGE_DATA) {
2288 resource.data = compressedImageResourceData;
2289 break;
2290 }
2291 }
2292 sortedResources.push_back({
2294 .flags = Resource::FLAG_NONE,
2295 .data = auxCompressionResourceData,
2296 });
2297 }
2298 std::ranges::sort(sortedResources, [](const Resource& lhs, const Resource& rhs) {
2299 return lhs.type < rhs.type;
2300 });
2301 writeResources(headerLengthPos, sortedResources);
2302 }
2303 break;
2304 }
2305 case PLATFORM_XBOX: {
2306 writer << XTF_SIGNATURE << PLATFORM_XBOX;
2307 writer.write<uint32_t>(0);
2308
2309 const auto headerSizePos = writer.tell();
2310 writer
2311 .write<uint32_t>(0)
2312 .write(this->flags)
2313 .write(this->width)
2314 .write(this->height)
2315 .write(this->depth)
2316 .write(this->frameCount);
2317 const auto preloadSizePos = writer.tell();
2318 writer.write<uint16_t>(0);
2319 const auto imageOffsetPos = writer.tell();
2320 writer
2321 .write<uint16_t>(0)
2322 .write(this->reflectivity[0])
2323 .write(this->reflectivity[1])
2324 .write(this->reflectivity[2])
2325 .write(this->bumpMapScale)
2326 .write(this->format)
2327 .write(this->thumbnailWidth)
2328 .write(this->thumbnailHeight)
2329 .write(this->fallbackWidth)
2330 .write(this->fallbackHeight)
2331 .write(this->consoleMipScale)
2332 .write<uint8_t>(0);
2333
2334 const auto headerSize = writer.tell();
2335 writer.seek_u(headerSizePos).write<uint32_t>(headerSize).seek_u(headerSize);
2336
2337 if (const auto* thumbnailResource = this->getResource(Resource::TYPE_THUMBNAIL_DATA); thumbnailResource && this->hasThumbnailData()) {
2338 writer.write(thumbnailResource->data);
2339 }
2340
2341 if (this->format == ImageFormat::P8) {
2342 if (const auto* paletteResource = this->getResource(Resource::TYPE_PALETTE_DATA)) {
2343 writer.write(paletteResource->data);
2344 }
2345 }
2346
2347 bool hasFallbackResource = false;
2348 if (const auto* fallbackResource = this->getResource(Resource::TYPE_FALLBACK_DATA); fallbackResource && this->hasFallbackData()) {
2349 hasFallbackResource = true;
2350 bool ok;
2351 auto reorderedFallbackData = ::convertBetweenDDSAndVTFMipOrderForXBOX<false>(false, fallbackResource->data, this->format, this->fallbackMipCount, this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight, 1, ok);
2352 if (ok) {
2353 ::swapImageDataEndianForConsole<false>(reorderedFallbackData, this->format, this->fallbackMipCount, this->frameCount, this->getFaceCount(), this->fallbackWidth, this->fallbackHeight, 1, this->platform);
2354 writer.write(reorderedFallbackData);
2355 } else {
2356 writer.pad(fallbackResource->data.size());
2357 }
2358 }
2359
2360 const auto preloadSize = writer.tell();
2361 writer.seek_u(preloadSizePos).write<uint32_t>(preloadSize).seek_u(preloadSize);
2362
2363 if (hasFallbackResource) {
2364 writer.pad(math::paddingForAlignment(512, writer.tell()));
2365 }
2366 const auto imageOffset = writer.tell();
2367 writer.seek_u(imageOffsetPos).write<uint16_t>(imageOffset).seek_u(imageOffset);
2368
2369 if (const auto* imageResource = this->getResource(Resource::TYPE_IMAGE_DATA); imageResource && this->hasImageData()) {
2370 bool ok;
2371 auto reorderedImageData = ::convertBetweenDDSAndVTFMipOrderForXBOX<false>(true, imageResource->data, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->depth, ok);
2372 if (ok) {
2373 ::swapImageDataEndianForConsole<false>(reorderedImageData, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->depth, this->platform);
2374 writer.write(reorderedImageData);
2375 } else {
2376 writer.pad(imageResource->data.size());
2377 }
2378 }
2379 if (writer.tell() > 512) {
2380 writer.pad(math::paddingForAlignment(512, writer.tell()));
2381 }
2382 break;
2383 }
2384 case PLATFORM_X360:
2386 case PLATFORM_PS3_PORTAL2: {
2387 if (this->platform == PLATFORM_PS3_PORTAL2) {
2388 writer << VTF3_SIGNATURE;
2389 writer.set_big_endian(true);
2390 writer << PLATFORM_PS3_ORANGEBOX; // Intentional
2391 } else {
2392 writer << VTFX_SIGNATURE;
2393 writer.set_big_endian(true);
2394 writer << this->platform;
2395 }
2396 writer.write<uint32_t>(8);
2397
2398 const auto headerLengthPos = writer.tell();
2399 writer
2400 .write<uint32_t>(0)
2401 .write(bakeFlags)
2402 .write(this->width)
2403 .write(this->height)
2404 .write(this->depth)
2405 .write(this->frameCount);
2406 const auto preloadPos = writer.tell();
2407 writer
2408 .write<uint16_t>(0) // preload size
2409 .write(this->consoleMipScale)
2410 .write<uint8_t>(this->resources.size())
2411 .write(this->reflectivity[0])
2412 .write(this->reflectivity[1])
2413 .write(this->reflectivity[2])
2414 .write(this->bumpMapScale)
2415 .write(bakeFormat)
2416 .write<uint8_t>(std::clamp(static_cast<int>(std::roundf(this->reflectivity[0] * 255)), 0, 255))
2417 .write<uint8_t>(std::clamp(static_cast<int>(std::roundf(this->reflectivity[1] * 255)), 0, 255))
2418 .write<uint8_t>(std::clamp(static_cast<int>(std::roundf(this->reflectivity[2] * 255)), 0, 255))
2419 .write<uint8_t>(255);
2420 const auto compressionPos = writer.tell();
2421 writer.write<uint32_t>(0); // compressed length
2422
2423 if (this->platform == PLATFORM_PS3_PORTAL2) {
2424 // 16 byte aligned
2425 writer.write<uint32_t>(0);
2426 }
2427
2428 std::vector<std::byte> thumbnailResourceData;
2429 std::vector<std::byte> imageResourceData;
2430 std::vector<Resource> sortedResources = this->getResources();
2431 for (auto& resource : sortedResources) {
2432 if (resource.type == Resource::TYPE_THUMBNAIL_DATA) {
2433 thumbnailResourceData = {resource.data.begin(), resource.data.end()};
2434 ::swapImageDataEndianForConsole<false>(thumbnailResourceData, this->thumbnailFormat, 1, 1, 1, this->thumbnailWidth, this->thumbnailHeight, 1, this->platform);
2435 resource.data = thumbnailResourceData;
2436 } else if (resource.type == Resource::TYPE_IMAGE_DATA) {
2437 if (this->platform == PLATFORM_PS3_ORANGEBOX) {
2438 bool ok;
2439 imageResourceData = ::convertBetweenDDSAndVTFMipOrderForXBOX<false>(false, resource.data, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->depth, ok);
2440 if (!ok || imageResourceData.size() != resource.data.size()) {
2441 return {};
2442 }
2443 } else {
2444 imageResourceData = {resource.data.begin(), resource.data.end()};
2445 }
2446 ::swapImageDataEndianForConsole<false>(imageResourceData, this->format, this->mipCount, this->frameCount, this->getFaceCount(), this->width, this->height, this->depth, this->platform);
2447
2448 // LZMA compression has not been observed on the PS3 copy of The Orange Box
2449 // todo(vtfpp): check cubemaps
2451 auto fixedCompressionLevel = this->compressionLevel;
2452 if (this->compressionLevel == 0) {
2453 // Compression level defaults to 0, so it works differently on console.
2454 // Rather than not compress on 0, 0 will be replaced with the default
2455 // compression level (6) if the compression method is LZMA.
2456 fixedCompressionLevel = 6;
2457 }
2458 if (auto compressedData = compression::compressValveLZMA(imageResourceData, fixedCompressionLevel)) {
2459 imageResourceData = std::move(*compressedData);
2460 const auto curPos = writer.tell();
2461 writer.seek_u(compressionPos).write<uint32_t>(imageResourceData.size()).seek_u(curPos);
2462 }
2463 }
2464
2465 resource.data = imageResourceData;
2466 break;
2467 }
2468 }
2469 std::ranges::sort(sortedResources, [](const Resource& lhs, const Resource& rhs) {
2470 return lhs.type < rhs.type;
2471 });
2472 const auto resourceDataImagePos = writeResources(headerLengthPos, sortedResources);
2473 writer.seek_u(preloadPos).write(std::max<uint16_t>(resourceDataImagePos, 2048));
2474 break;
2475 }
2476 }
2477 out.resize(writer.size());
2478 return out;
2479}
2480
2481bool VTF::bake(const std::filesystem::path& vtfPath) const {
2482 return fs::writeFileBuffer(vtfPath, this->bake());
2483}
#define SOURCEPP_DEBUG_BREAK
Create a breakpoint in debug.
Definition Macros.h:19
std::vector< std::byte > bake() const
Definition HOT.cpp:61
std::vector< std::byte > bake() const
Definition SHT.cpp:84
uint16_t width
Definition VTF.h:516
@ PLATFORM_PC
Definition VTF.h:224
@ PLATFORM_X360
Definition VTF.h:226
@ PLATFORM_PS3_PORTAL2
Definition VTF.h:228
@ PLATFORM_PS3_ORANGEBOX
Definition VTF.h:227
@ PLATFORM_UNKNOWN
Definition VTF.h:223
@ PLATFORM_XBOX
Definition VTF.h:225
void setImageHeightResizeMethod(ImageConversion::ResizeMethod imageHeightResizeMethod_)
Definition VTF.cpp:1006
bool setFrameFaceAndDepth(uint16_t newFrameCount, bool isCubeMap, uint16_t newDepth=1)
Definition VTF.cpp:1281
void removeKeyValuesDataResource()
Definition VTF.cpp:1701
void computeFallback(ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT)
Definition VTF.cpp:1985
uint8_t getFallbackHeight() const
Definition VTF.cpp:1394
CompressionMethod compressionMethod
Definition VTF.h:549
void computeReflectivity()
Definition VTF.cpp:1305
uint8_t getThumbnailWidth() const
Definition VTF.cpp:1382
std::vector< std::byte > saveThumbnailToFile(ImageConversion::FileFormat fileFormat=ImageConversion::FileFormat::DEFAULT) const
Definition VTF.cpp:1944
uint8_t consoleMipScale
Definition VTF.h:538
uint8_t fallbackWidth
Definition VTF.h:533
ImageFormat getFormat() const
Definition VTF.cpp:1120
void setPlatform(Platform newPlatform)
Definition VTF.cpp:889
uint16_t getHeight(uint8_t mip=0) const
Definition VTF.cpp:1018
VTF & operator=(const VTF &other)
Definition VTF.cpp:717
void removeFallback()
Definition VTF.cpp:2012
uint8_t getFallbackWidth() const
Definition VTF.cpp:1390
int16_t compressionLevel
Definition VTF.h:548
sourcepp::math::Vec3f reflectivity
Definition VTF.h:523
uint16_t getWidth(uint8_t mip=0) const
Definition VTF.cpp:1010
uint16_t startFrame
Definition VTF.h:521
void setThumbnail(std::span< const std::byte > imageData_, ImageFormat format_, uint16_t width_, uint16_t height_, float quality=ImageConversion::DEFAULT_COMPRESSED_QUALITY)
Definition VTF.cpp:1882
uint8_t mipCount
Definition VTF.h:527
bool setRecommendedMipCount()
Definition VTF.cpp:1164
static bool createInternal(VTF &writer, CreationOptions options)
Definition VTF.cpp:760
std::vector< std::byte > getParticleSheetFrameDataAsRGBA8888(uint16_t &spriteWidth, uint16_t &spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds=0, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
This is a convenience function. You're best off uploading the bounds to the GPU and scaling the UV th...
Definition VTF.cpp:1643
void regenerateImageData(ImageFormat newFormat, uint16_t newWidth, uint16_t newHeight, uint8_t newMipCount, uint16_t newFrameCount, uint8_t newFaceCount, uint16_t newDepth, ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT, float quality=ImageConversion::DEFAULT_COMPRESSED_QUALITY)
Definition VTF.cpp:1483
bool hasFallbackData() const
Definition VTF.cpp:1955
void computeMips(ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT)
Definition VTF.cpp:1171
void setImageWidthResizeMethod(ImageConversion::ResizeMethod imageWidthResizeMethod_)
Definition VTF.cpp:1002
uint8_t thumbnailWidth
Definition VTF.h:530
void setCompressionLevel(int16_t newCompressionLevel)
Definition VTF.cpp:1724
uint8_t fallbackHeight
Definition VTF.h:534
ImageConversion::ResizeMethod imageHeightResizeMethod
Definition VTF.h:551
void setFormat(ImageFormat newFormat, ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT, float quality=ImageConversion::DEFAULT_COMPRESSED_QUALITY)
Definition VTF.cpp:1124
void setImageResizeMethods(ImageConversion::ResizeMethod imageWidthResizeMethod_, ImageConversion::ResizeMethod imageHeightResizeMethod_)
Definition VTF.cpp:997
std::vector< std::byte > getParticleSheetFrameDataAs(ImageFormat newFormat, uint16_t &spriteWidth, uint16_t &spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds=0, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
This is a convenience function. You're best off uploading the bounds to the GPU and scaling the UV th...
Definition VTF.cpp:1639
float getBumpMapScale() const
Definition VTF.cpp:1370
void setKeyValuesDataResource(std::string_view value)
Definition VTF.cpp:1691
void computeTransparencyFlags()
Definition VTF.cpp:1093
std::vector< std::byte > saveFallbackToFile(uint8_t mip=0, uint16_t frame=0, uint8_t face=0, ImageConversion::FileFormat fileFormat=ImageConversion::FileFormat::DEFAULT) const
Definition VTF.cpp:2019
std::vector< std::byte > data
Definition VTF.h:512
ImageFormat format
Definition VTF.h:526
Platform getPlatform() const
Definition VTF.cpp:885
std::vector< std::byte > getPaletteResourceFrame(uint16_t frame=0) const
Definition VTF.cpp:1590
void addFlags(uint32_t flags_)
Definition VTF.cpp:1065
uint8_t getFaceCount() const
Definition VTF.cpp:1235
ImageFormat thumbnailFormat
Definition VTF.h:529
void setFlags(uint32_t flags_)
Definition VTF.cpp:1061
void setSize(uint16_t newWidth, uint16_t newHeight, ImageConversion::ResizeFilter filter)
Definition VTF.cpp:1026
void removeLODResource()
Definition VTF.cpp:1679
ImageConversion::ResizeMethod getImageHeightResizeMethod() const
Definition VTF.cpp:993
void setBumpMapScale(float newBumpMapScale)
Definition VTF.cpp:1374
ImageFormat getThumbnailFormat() const
Definition VTF.cpp:1378
uint16_t getStartFrame() const
Definition VTF.cpp:1289
static constexpr auto FORMAT_DEFAULT
This value is only valid when passed to VTF::create through CreationOptions or VTF::setFormat.
Definition VTF.h:260
bool hasThumbnailData() const
Definition VTF.cpp:1859
void setReflectivity(sourcepp::math::Vec3f newReflectivity)
Definition VTF.cpp:1301
void setSRGB(bool srgb)
Definition VTF.cpp:1077
const std::vector< Resource > & getResources() const
Definition VTF.cpp:1402
void removeHotspotDataResource()
Definition VTF.cpp:1716
void setVersion(uint32_t newVersion)
Definition VTF.cpp:955
std::vector< std::byte > saveImageToFile(uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0, ImageConversion::FileFormat fileFormat=ImageConversion::FileFormat::DEFAULT) const
Definition VTF.cpp:1848
Resource * getResourceInternal(Resource::Type type)
Definition VTF.cpp:1415
void computeThumbnail(ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT, float quality=ImageConversion::DEFAULT_COMPRESSED_QUALITY)
Definition VTF.cpp:1914
std::vector< std::byte > bake() const
Definition VTF.cpp:2139
bool hasImageData() const
Definition VTF.cpp:1742
ImageConversion::ResizeMethod imageWidthResizeMethod
Definition VTF.h:550
std::span< const std::byte > getFallbackDataRaw(uint8_t mip=0, uint16_t frame=0, uint8_t face=0) const
Definition VTF.cpp:1959
uint8_t fallbackMipCount
Definition VTF.h:535
void removeParticleSheetResource()
Definition VTF.cpp:1658
std::vector< std::byte > getFallbackDataAs(ImageFormat newFormat, uint8_t mip=0, uint16_t frame=0, uint8_t face=0) const
Definition VTF.cpp:1968
static bool create(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t height, const std::filesystem::path &vtfPath, const CreationOptions &options)
Definition VTF.cpp:815
std::vector< std::byte > getImageDataAsRGBA8888(uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
Definition VTF.cpp:1770
uint16_t getFrameCount() const
Definition VTF.cpp:1223
uint8_t getMipCount() const
Definition VTF.cpp:1148
void setLODResource(uint8_t u, uint8_t v, uint8_t u360=0, uint8_t v360=0)
Definition VTF.cpp:1670
void removeResourceInternal(Resource::Type type)
Definition VTF.cpp:1479
std::vector< std::byte > getFallbackDataAsRGBA8888(uint8_t mip=0, uint16_t frame=0, uint8_t face=0) const
Definition VTF.cpp:1981
bool setFrameCount(uint16_t newFrameCount)
Definition VTF.cpp:1227
ImageConversion::ResizeMethod getImageWidthResizeMethod() const
Definition VTF.cpp:989
std::vector< std::byte > getImageDataAs(ImageFormat newFormat, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
Definition VTF.cpp:1755
static ImageFormat getDefaultCompressedFormat(ImageFormat inputFormat, uint32_t version, bool isCubeMap)
Definition VTF.cpp:1107
void setStartFrame(uint16_t newStartFrame)
Definition VTF.cpp:1293
std::vector< std::byte > getThumbnailDataAs(ImageFormat newFormat) const
Definition VTF.cpp:1870
std::span< const std::byte > getImageDataRaw(uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
Definition VTF.cpp:1746
uint16_t depth
Definition VTF.h:541
sourcepp::math::Vec3f getReflectivity() const
Definition VTF.cpp:1297
uint8_t getFallbackMipCount() const
Definition VTF.cpp:1398
std::vector< std::byte > getThumbnailDataAsRGBA8888() const
Definition VTF.cpp:1878
uint8_t getConsoleMipScale() const
Definition VTF.cpp:2031
uint16_t getDepth(uint8_t mip=0) const
Definition VTF.cpp:1269
bool isSRGB() const
Definition VTF.cpp:1073
uint32_t flags
Definition VTF.h:518
uint16_t getPaddedHeight(uint8_t mip=0) const
Definition VTF.cpp:1022
void removeThumbnail()
Definition VTF.cpp:1937
static constexpr auto FORMAT_UNCHANGED
This value is only valid when passed to VTF::create through CreationOptions.
Definition VTF.h:257
uint32_t version
Definition VTF.h:514
std::span< const std::byte > getThumbnailDataRaw() const
Definition VTF.cpp:1863
bool setMipCount(uint8_t newMipCount)
Definition VTF.cpp:1152
void setExtendedFlagsResource(uint32_t value)
Definition VTF.cpp:1683
CompressionMethod getCompressionMethod() const
Definition VTF.cpp:1728
uint16_t getPaddedWidth(uint8_t mip=0) const
Definition VTF.cpp:1014
void setCRCResource(uint32_t value)
Definition VTF.cpp:1662
void setCompressionMethod(CompressionMethod newCompressionMethod)
Definition VTF.cpp:1732
bool setFaceCount(bool isCubeMap)
Definition VTF.cpp:1261
uint8_t getThumbnailHeight() const
Definition VTF.cpp:1386
void setConsoleMipScale(uint8_t consoleMipScale_)
Definition VTF.cpp:2035
const Resource * getResource(Resource::Type type) const
Definition VTF.cpp:1406
uint32_t getFlags() const
Definition VTF.cpp:1057
void setParticleSheetResource(const SHT &value)
Definition VTF.cpp:1647
@ FLAG_MASK_V5
Definition VTF.h:210
@ FLAG_V0_ONE_BIT_ALPHA
Definition VTF.h:161
@ FLAG_MASK_V4_TF2
Definition VTF.h:204
@ FLAG_V0_MULTI_BIT_ALPHA
Definition VTF.h:162
@ FLAG_MASK_V2
Definition VTF.h:181
@ FLAG_MASK_V5_CSGO
Definition VTF.h:217
@ FLAG_V0_NO_LOD
Definition VTF.h:158
@ FLAG_V4_SRGB
Definition VTF.h:196
@ FLAG_V0_NO_MIP
Definition VTF.h:157
@ FLAG_V5_SRGB
Definition VTF.h:207
@ FLAG_MASK_XBOX
Definition VTF.h:187
@ FLAG_V5_PWL_CORRECTED
Definition VTF.h:206
@ FLAG_MASK_V4
Definition VTF.h:197
@ FLAG_MASK_V3
Definition VTF.h:194
@ FLAG_V0_ENVMAP
Definition VTF.h:163
@ FLAG_MASK_V1
Definition VTF.h:174
@ FLAG_MASK_INTERNAL
Definition VTF.h:219
uint16_t height
Definition VTF.h:517
void removeCRCResource()
Definition VTF.cpp:1666
uint16_t frameCount
Definition VTF.h:520
uint32_t getVersion() const
Definition VTF.cpp:951
int16_t getCompressionLevel() const
Definition VTF.cpp:1720
void setHotspotDataResource(const HOT &value)
Definition VTF.cpp:1705
void removeFlags(uint32_t flags_)
Definition VTF.cpp:1069
void setResourceInternal(Resource::Type type, std::span< const std::byte > data_)
Definition VTF.cpp:1424
bool setDepth(uint16_t newDepth)
Definition VTF.cpp:1273
uint64_t estimateBakeSize() const
Definition VTF.cpp:2039
std::vector< Resource > resources
Definition VTF.h:544
uint8_t thumbnailHeight
Definition VTF.h:531
Platform platform
Definition VTF.h:547
bool setImage(std::span< const std::byte > imageData_, ImageFormat format_, uint16_t width_, uint16_t height_, ImageConversion::ResizeFilter filter=ImageConversion::ResizeFilter::DEFAULT, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0, float quality=ImageConversion::DEFAULT_COMPRESSED_QUALITY)
Definition VTF.cpp:1774
std::vector< std::byte > getParticleSheetFrameDataRaw(uint16_t &spriteWidth, uint16_t &spriteHeight, uint32_t shtSequenceID, uint32_t shtFrame, uint8_t shtBounds=0, uint8_t mip=0, uint16_t frame=0, uint8_t face=0, uint16_t slice=0) const
This is a convenience function. You're best off uploading the bounds to the GPU and scaling the UV th...
Definition VTF.cpp:1597
void removeExtendedFlagsResource()
Definition VTF.cpp:1687
bool opened
Definition VTF.h:510
float bumpMapScale
Definition VTF.h:525
std::optional< std::vector< std::byte > > compressValveLZMA(std::span< const std::byte > data, uint8_t compressLevel=6)
Definition LZMA.cpp:8
constexpr auto VALVE_LZMA_SIGNATURE
Definition LZMA.h:13
std::optional< std::vector< std::byte > > decompressValveLZMA(std::span< const std::byte > data)
Definition LZMA.cpp:51
bool writeFileBuffer(const std::filesystem::path &filepath, std::span< const std::byte > buffer)
Definition FS.cpp:25
std::vector< std::byte > readFileBuffer(const std::filesystem::path &filepath, std::size_t startOffset=0)
Definition FS.cpp:7
constexpr uint16_t paddingForAlignment(uint16_t alignment, uint64_t n)
Definition Math.h:61
constexpr uint32_t log2ceil(uint32_t value)
Definition Math.h:57
std::vector< std::byte > convertFileToImageData(std::span< const std::byte > fileData, ImageFormat &format, int &width, int &height, int &frameCount)
std::vector< std::byte > convertImageDataToFile(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t height, FileFormat fileFormat=FileFormat::DEFAULT)
Converts image data to the given file format (PNG or EXR by default).
void setResizedDims(uint16_t &width, ResizeMethod widthResize, uint16_t &height, ResizeMethod heightResize)
Set the new image dimensions given a resize method.
std::vector< std::byte > convertImageDataToFormat(std::span< const std::byte > imageData, ImageFormat oldFormat, ImageFormat newFormat, uint16_t width, uint16_t height, float quality=DEFAULT_COMPRESSED_QUALITY)
Converts an image from one format to another.
std::vector< std::byte > convertSeveralImageDataToFormat(std::span< const std::byte > imageData, ImageFormat oldFormat, ImageFormat newFormat, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t depth, float quality=DEFAULT_COMPRESSED_QUALITY)
Converts several images from one format to another.
std::vector< std::byte > gammaCorrectImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t height, float gamma)
Perform gamma correction on the given image data. Will not perform gamma correction if the input imag...
std::vector< std::byte > cropImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t newWidth, uint16_t xOffset, uint16_t height, uint16_t newHeight, uint16_t yOffset)
Crops the given image to the new dimensions. If the image format is compressed it will be converted t...
std::vector< std::byte > invertGreenChannelForImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t height)
Invert the green channel. Meant for converting normal maps between OpenGL and DirectX formats.
std::vector< std::byte > resizeImageData(std::span< const std::byte > imageData, ImageFormat format, uint16_t width, uint16_t newWidth, uint16_t height, uint16_t newHeight, bool srgb, ResizeFilter filter, ResizeEdge edge=ResizeEdge::CLAMP)
Resize given image data to the new dimensions.
constexpr uint8_t getMaximumMipCount(uint16_t width, uint16_t height, uint16_t depth=1)
Calculate the largest mip count a texture with the given width, height, and depth can contain.
constexpr std::pair< uint16_t, uint16_t > getMipDims(uint8_t mip, uint16_t width, uint16_t height, bool addCompressedFormatPadding=false)
Get the width and height at a given mip level.
constexpr uint16_t getMipDim(uint8_t mip, uint16_t dim, bool addCompressedFormatPadding=false)
Get the dimension at a given mip level.
constexpr uint32_t getDataLength(ImageFormat format, uint16_t width, uint16_t height, uint16_t depth=1)
Calculate the amount of data required to store a texture with the given format and dimensions.
constexpr bool getDataPosition(uint32_t &offset, uint32_t &length, ImageFormat format, uint8_t mip, uint8_t mipCount, uint16_t frame, uint16_t frameCount, uint8_t face, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t slice=0, uint16_t depth=1)
Find the position of a specific mip, frame, face, and slice within a texture.
constexpr bool large(ImageFormat format)
Check if the given format is representable by RGBA8888 without losing data.
constexpr bool transparent(ImageFormat format)
Check if the given format can store transparency.
constexpr uint32_t getDataLengthXBOX(bool padded, ImageFormat format, uint8_t mipCount, uint16_t frameCount, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t depth=1)
Calculate the amount of data required to store an XBOX platform texture with the given format,...
constexpr int8_t decompressedAlpha(ImageFormat format)
Get the number of bits of precision of the alpha channel in the given format regardless of compressio...
constexpr uint8_t bpp(ImageFormat format)
Find the bits per pixel of the given format.
constexpr bool getDataPositionXbox(uint32_t &offset, uint32_t &length, bool padded, ImageFormat format, uint8_t mip, uint8_t mipCount, uint16_t frame, uint16_t frameCount, uint8_t face, uint8_t faceCount, uint16_t width, uint16_t height, uint16_t slice=0, uint16_t depth=1)
Find the position of a specific mip, frame, face, and slice within an XBOX platform texture.
constexpr bool compressed(ImageFormat format)
Check if the given format is a compressed format (DXT1, DXT3, DXT5, ATI1N, ATI2N, BC7,...
std::vector< std::byte > convertP8ImageDataToBGRA8888(std::span< const std::byte > paletteData, std::span< const std::byte > imageData)
Converts a paletted image to something usable.
Definition HOT.h:11
constexpr uint32_t VTF_SIGNATURE
Definition VTF.h:23
constexpr uint32_t XTF_SIGNATURE
Definition VTF.h:24
constexpr uint32_t VTFX_SIGNATURE
Definition VTF.h:25
constexpr uint32_t VTF3_SIGNATURE
Definition VTF.h:26
CompressionMethod
Definition VTF.h:28
Type type
Definition VTF.h:58
ConvertedData convertData() const
Definition VTF.cpp:238
std::variant< std::monostate, SHT, uint32_t, std::tuple< uint8_t, uint8_t, uint8_t, uint8_t >, std::string, HOT > ConvertedData
Definition VTF.h:62
@ FLAG_LOCAL_DATA
Definition VTF.h:55
@ TYPE_KEYVALUES_DATA
Definition VTF.h:50
@ TYPE_FALLBACK_DATA
Definition VTF.h:42
@ TYPE_PALETTE_DATA
Definition VTF.h:41
@ TYPE_PARTICLE_SHEET_DATA
Definition VTF.h:43
@ TYPE_EXTENDED_FLAGS
Definition VTF.h:46
@ TYPE_IMAGE_DATA
Definition VTF.h:45
@ TYPE_AUX_COMPRESSION
Definition VTF.h:48
@ TYPE_LOD_CONTROL_INFO
Definition VTF.h:49
@ TYPE_THUMBNAIL_DATA
Definition VTF.h:40
@ TYPE_HOTSPOT_DATA
Definition VTF.h:44
Flags flags
Definition VTF.h:59
std::span< std::byte > data
Definition VTF.h:60
uint16_t initialFrameCount
Definition VTF.h:239
ImageConversion::ResizeFilter filter
Definition VTF.h:237
ImageConversion::ResizeMethod heightResizeMethod
Definition VTF.h:236
CompressionMethod compressionMethod
Definition VTF.h:249
ImageConversion::ResizeMethod widthResizeMethod
Definition VTF.h:235
ImageFormat outputFormat
Definition VTF.h:233