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