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