Files
Gleb Mazovetskiy d8cd147ac7 Re-encode CL2 on load
Original Blizzard encoder is slightly less optimal than our encoder.

While size in RAM in less of a concern for the non-`UNPACKED_MPQS`
build, smaller files are faster to render.

Savings for unpacked and minified MPQs:
* diabdat.mpq: 918,311 bytes.
* hellfire.mpq: 313,882 bytes.

Example player graphics (note that only a few are loaded at any given time for single player):
* diabdat/plrgfx/warrior/: 366,564 bytes.

Example monster graphics savings:

* diabdat/monsters/skelbow: 5,391 bytes.

Based on the implementation from https://github.com/diasurgical/devilutionx-graphics-tools/pull/6
2023-07-13 01:44:16 +01:00

98 lines
3.3 KiB
C++

#include "utils/surface_to_clx.hpp"
#include <cstdint>
#include <cstring>
#include <vector>
#include "utils/clx_encode.hpp"
#include "utils/endian.hpp"
#ifdef DEBUG_SURFACE_TO_CLX_SIZE
#include <iomanip>
#include <iostream>
#endif
namespace devilution {
OwnedClxSpriteList SurfaceToClx(const Surface &surface, unsigned numFrames,
std::optional<uint8_t> transparentColor)
{
// CLX header: frame count, frame offset for each frame, file size
std::vector<uint8_t> clxData(4 * (2 + static_cast<size_t>(numFrames)));
WriteLE32(clxData.data(), numFrames);
const auto height = static_cast<unsigned>(surface.h());
const auto width = static_cast<unsigned>(surface.w());
const auto pitch = static_cast<unsigned>(surface.pitch());
const unsigned frameHeight = height / numFrames;
// We process the surface a whole frame at a time because the lines are reversed in CEL.
const uint8_t *dataPtr = surface.begin();
for (unsigned frame = 1; frame <= numFrames; ++frame) {
WriteLE32(&clxData[4 * static_cast<size_t>(frame)], static_cast<uint32_t>(clxData.size()));
// Frame header: 5 16-bit values:
// 1. Offset to start of the pixel data.
// 2. Width
// 3. Height
// 4..5. Unused (0)
const size_t frameHeaderPos = clxData.size();
constexpr size_t FrameHeaderSize = 10;
clxData.resize(clxData.size() + FrameHeaderSize);
// Frame header:
WriteLE16(&clxData[frameHeaderPos], FrameHeaderSize);
WriteLE16(&clxData[frameHeaderPos + 2], static_cast<uint16_t>(width));
WriteLE16(&clxData[frameHeaderPos + 4], static_cast<uint16_t>(frameHeight));
memset(&clxData[frameHeaderPos + 6], 0, 4);
unsigned transparentRunWidth = 0;
size_t line = 0;
while (line != frameHeight) {
// Process line:
const uint8_t *src = &dataPtr[(frameHeight - (line + 1)) * pitch];
if (transparentColor) {
unsigned solidRunWidth = 0;
for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) {
if (*src == *transparentColor) {
if (solidRunWidth != 0) {
AppendClxPixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, clxData);
solidRunWidth = 0;
}
++transparentRunWidth;
} else {
AppendClxTransparentRun(transparentRunWidth, clxData);
transparentRunWidth = 0;
++solidRunWidth;
}
}
if (solidRunWidth != 0) {
AppendClxPixelsOrFillRun(src - solidRunWidth, solidRunWidth, clxData);
}
} else {
AppendClxPixelsOrFillRun(src, width, clxData);
}
++line;
}
AppendClxTransparentRun(transparentRunWidth, clxData);
dataPtr += static_cast<unsigned>(pitch * frameHeight);
}
WriteLE32(&clxData[4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(clxData.size()));
auto out = std::unique_ptr<uint8_t[]>(new uint8_t[clxData.size()]);
memcpy(&out[0], clxData.data(), clxData.size());
#ifdef DEBUG_SURFACE_TO_CLX_SIZE
const int surfaceSize = surface.h() * surface.pitch();
std::cout << "Surface(" << surface.w() << ", " << surface.h() << ") -> CLX\t"
<< surfaceSize << " -> " << clxData.size() << "\t"
<< std::setprecision(1) << std::fixed << (static_cast<int>(clxData.size()) - surfaceSize) / ((float)surfaceSize) * 100 << "%" << std::endl;
#endif
return OwnedClxSpriteList { std::move(out) };
}
} // namespace devilution