blob: 25340b6cfa9f44429028c0b434b402544d742cc2 [file]
#include "rive/texture_archive.hpp"
#include <cstdio>
#include <cstring>
#include <fstream>
#include <limits>
using namespace rive;
namespace
{
// Per-texture descriptor on disk: 4*u16 + 4*u8 + 1*u8(format) = 13 bytes.
constexpr size_t kDescriptorSize = 13;
// Per-mip entry on disk: 3*u32 = 12 bytes.
constexpr size_t kMipEntrySize = 12;
// Defensive caps to bound allocation and reject hostile inputs early.
constexpr uint16_t kMaxTextures = 4096;
constexpr uint8_t kMaxMipsPerTexture = 16;
// 256 MB per mip is far above any realistic compressed texture.
constexpr uint32_t kMaxBytesPerMip = 256u * 1024u * 1024u;
} // namespace
// ---------------------------------------------------------------------------
// Binary file format (.rtex)
//
// [4] Magic: "RTEX"
// [2] Version: uint16_t (currently 1)
// [2] TextureCount: uint16_t
// [per texture]:
// [2] width: uint16_t (original, un-padded)
// [2] height: uint16_t
// [2] paddedWidth: uint16_t
// [2] paddedHeight: uint16_t
// [1] blockSizeX: uint8_t
// [1] blockSizeY: uint8_t
// [1] bytesPerBlock:uint8_t
// [1] numMips: uint8_t
// [1] format: uint8_t (GPUTextureFormat)
// [per mip (numMips entries)]:
// [4] blocksX: uint32_t
// [4] blocksY: uint32_t
// [4] bytesTotal: uint32_t
// [data blob: all mip data for every texture, in declaration order]
// ---------------------------------------------------------------------------
void TextureDirectory::addTexture(TextureData& td)
{
const uint8_t* oldBase = dataBlob.empty() ? nullptr : dataBlob.data();
std::vector<size_t> offsets;
offsets.reserve(td.mipLevels.size());
for (auto& mip : td.mipLevels)
{
offsets.push_back(dataBlob.size());
dataBlob.insert(dataBlob.end(),
mip.blocks,
mip.blocks + mip.bytesTotal);
}
const uint8_t* newBase = dataBlob.data();
if (oldBase != nullptr && oldBase != newBase)
{
for (auto& existing : dir)
for (auto& mip : existing.mipLevels)
mip.blocks = newBase + (mip.blocks - oldBase);
}
for (size_t i = 0; i < td.mipLevels.size(); ++i)
td.mipLevels[i].blocks = newBase + offsets[i];
dir.push_back(td);
}
bool TextureDirectory::exportArchive(const std::string& path)
{
FILE* fp = std::fopen(path.c_str(), "wb");
if (!fp)
{
std::fprintf(stderr,
"TextureDirectory::exportArchive: cannot open '%s'\n",
path.c_str());
return false;
}
TextureArchiveHeader header;
std::memcpy(header.magic,
TextureArchiveHeader::kMagic,
sizeof(header.magic));
header.version = TextureArchiveHeader::kCurrentVersion;
header.textureCount = static_cast<uint16_t>(dir.size());
std::fwrite(&header, sizeof(header), 1, fp);
for (const auto& td : dir)
{
std::fwrite(&td.width, sizeof(td.width), 1, fp);
std::fwrite(&td.height, sizeof(td.height), 1, fp);
std::fwrite(&td.paddedWidth, sizeof(td.paddedWidth), 1, fp);
std::fwrite(&td.paddedHeight, sizeof(td.paddedHeight), 1, fp);
std::fwrite(&td.blockSizeX, sizeof(td.blockSizeX), 1, fp);
std::fwrite(&td.blockSizeY, sizeof(td.blockSizeY), 1, fp);
std::fwrite(&td.bytesPerBlock, sizeof(td.bytesPerBlock), 1, fp);
std::fwrite(&td.numMips, sizeof(td.numMips), 1, fp);
const uint8_t fmt = static_cast<uint8_t>(td.format);
std::fwrite(&fmt, sizeof(fmt), 1, fp);
for (const auto& mip : td.mipLevels)
{
const uint32_t bx = static_cast<uint32_t>(mip.blocksX);
const uint32_t by = static_cast<uint32_t>(mip.blocksY);
const uint32_t bt = mip.bytesTotal;
std::fwrite(&bx, sizeof(bx), 1, fp);
std::fwrite(&by, sizeof(by), 1, fp);
std::fwrite(&bt, sizeof(bt), 1, fp);
}
}
std::fwrite(dataBlob.data(), 1, dataBlob.size(), fp);
std::fclose(fp);
return true;
}
bool TextureDirectory::import(Span<const uint8_t> texBytes)
{
dir.clear();
dataBlob.clear();
if (texBytes.size() < sizeof(TextureArchiveHeader))
{
printf("TextureDirectory::import: file too small\n");
return false;
}
const uint8_t* p = texBytes.data();
const uint8_t* end = p + texBytes.size();
TextureArchiveHeader header;
std::memcpy(&header, p, sizeof(header));
p += sizeof(header);
if (std::memcmp(header.magic,
TextureArchiveHeader::kMagic,
sizeof(header.magic)) != 0)
{
printf("TextureDirectory::import: bad magic\n");
return false;
}
if (header.version != TextureArchiveHeader::kCurrentVersion)
{
printf("TextureDirectory::import: unsupported version %u\n",
static_cast<unsigned>(header.version));
return false;
}
const uint16_t count = header.textureCount;
if (count > kMaxTextures)
{
printf("TextureDirectory::import: textureCount %u exceeds cap %u\n",
static_cast<unsigned>(count),
static_cast<unsigned>(kMaxTextures));
return false;
}
struct MipDesc
{
uint32_t blocksX, blocksY, bytesTotal;
};
struct TexDesc
{
TextureData td;
std::vector<MipDesc> mips;
};
std::vector<TexDesc> descs(count);
size_t totalData = 0;
for (auto& desc : descs)
{
TextureData& td = desc.td;
if (static_cast<size_t>(end - p) < kDescriptorSize)
{
printf("TextureDirectory::import: truncated descriptor\n");
return false;
}
std::memcpy(&td.width, p, 2);
p += 2;
std::memcpy(&td.height, p, 2);
p += 2;
std::memcpy(&td.paddedWidth, p, 2);
p += 2;
std::memcpy(&td.paddedHeight, p, 2);
p += 2;
td.blockSizeX = *p++;
td.blockSizeY = *p++;
td.bytesPerBlock = *p++;
td.numMips = *p++;
td.format = static_cast<GPUTextureFormat>(*p++);
if (td.numMips > kMaxMipsPerTexture)
{
printf("TextureDirectory::import: numMips %u exceeds cap %u\n",
static_cast<unsigned>(td.numMips),
static_cast<unsigned>(kMaxMipsPerTexture));
return false;
}
desc.mips.resize(td.numMips);
const size_t mipTableBytes =
static_cast<size_t>(td.numMips) * kMipEntrySize;
if (static_cast<size_t>(end - p) < mipTableBytes)
{
printf("TextureDirectory::import: truncated mip table\n");
return false;
}
td.totalBytes = 0;
for (auto& m : desc.mips)
{
std::memcpy(&m.blocksX, p, 4);
p += 4;
std::memcpy(&m.blocksY, p, 4);
p += 4;
std::memcpy(&m.bytesTotal, p, 4);
p += 4;
if (m.bytesTotal > kMaxBytesPerMip)
{
printf("TextureDirectory::import: mip bytesTotal %u "
"exceeds cap %u\n",
static_cast<unsigned>(m.bytesTotal),
static_cast<unsigned>(kMaxBytesPerMip));
return false;
}
// Sum without overflow: cap remaining headroom.
if (m.bytesTotal >
std::numeric_limits<uint32_t>::max() - td.totalBytes)
{
printf("TextureDirectory::import: totalBytes overflow\n");
return false;
}
td.totalBytes += m.bytesTotal;
}
if (td.totalBytes > std::numeric_limits<size_t>::max() - totalData)
{
printf("TextureDirectory::import: totalData overflow\n");
return false;
}
totalData += td.totalBytes;
}
if (static_cast<size_t>(end - p) != totalData)
{
printf("TextureDirectory::import: blob size mismatch "
"(expected %zu, got %zu)\n",
totalData,
static_cast<size_t>(end - p));
return false;
}
dataBlob.assign(p, end);
const uint8_t* blob = dataBlob.data();
size_t offset = 0;
dir.reserve(count);
for (auto& desc : descs)
{
TextureData& td = desc.td;
td.mipLevels.resize(desc.mips.size());
for (size_t i = 0; i < desc.mips.size(); i++)
{
const MipDesc& m = desc.mips[i];
if (m.blocksX > std::numeric_limits<uint16_t>::max() ||
m.blocksY > std::numeric_limits<uint16_t>::max())
{
printf("TextureDirectory::import: blocks dimension "
"exceeds uint16\n");
return false;
}
td.mipLevels[i].blocksX = static_cast<uint16_t>(m.blocksX);
td.mipLevels[i].blocksY = static_cast<uint16_t>(m.blocksY);
td.mipLevels[i].bytesTotal = m.bytesTotal;
td.mipLevels[i].blocks = blob + offset;
offset += m.bytesTotal;
}
dir.push_back(std::move(td));
}
return true;
}
bool TextureDirectory::import(const std::string& path)
{
std::ifstream in(path, std::ios::binary | std::ios::ate);
if (!in)
{
printf("TextureDirectory::import: cannot open %s\n", path.c_str());
return false;
}
const std::streamsize len = in.tellg();
in.seekg(0, std::ios::beg);
std::vector<uint8_t> buf(static_cast<size_t>(len));
if (!in.read(reinterpret_cast<char*>(buf.data()), len))
{
printf("TextureDirectory::import: read failed for %s\n", path.c_str());
return false;
}
return import(Span<const uint8_t>(buf.data(), buf.size()));
}