Browse Source

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
pull/6353/head
Gleb Mazovetskiy 3 years ago
parent
commit
d8cd147ac7
  1. 8
      Source/engine/load_cl2.hpp
  2. 74
      Source/engine/render/clx_render.cpp
  3. 28
      Source/monster.cpp
  4. 8
      Source/utils/cel_to_clx.cpp
  5. 142
      Source/utils/cl2_to_clx.cpp
  6. 11
      Source/utils/cl2_to_clx.hpp
  7. 44
      Source/utils/clx_decode.hpp
  8. 18
      Source/utils/clx_encode.hpp
  9. 12
      Source/utils/pcx_to_clx.cpp
  10. 12
      Source/utils/surface_to_clx.cpp

8
Source/engine/load_cl2.hpp

@ -61,13 +61,13 @@ OwnedClxSpriteSheet LoadMultipleCl2Sheet(tl::function_ref<const char *(size_t)>
FailedToOpenFileError(paths[i].data(), handle.error()); FailedToOpenFileError(paths[i].data(), handle.error());
} }
WriteLE32(&data[i * 4], accumulatedSize); WriteLE32(&data[i * 4], accumulatedSize);
#ifndef UNPACKED_MPQS
[[maybe_unused]] const uint16_t numLists = Cl2ToClx(&data[accumulatedSize], size, frameWidth);
assert(numLists == 0);
#endif
accumulatedSize += size; accumulatedSize += size;
} }
#ifdef UNPACKED_MPQS
return OwnedClxSpriteSheet { std::move(data), static_cast<uint16_t>(count) }; return OwnedClxSpriteSheet { std::move(data), static_cast<uint16_t>(count) };
#else
return Cl2ToClx(std::move(data), accumulatedSize, frameWidth).sheet();
#endif
} }
inline OwnedClxSpriteList LoadCl2(const char *pszName, uint16_t width) inline OwnedClxSpriteList LoadCl2(const char *pszName, uint16_t width)

74
Source/engine/render/clx_render.cpp

@ -11,6 +11,7 @@
#include "engine/render/blit_impl.hpp" #include "engine/render/blit_impl.hpp"
#include "engine/render/scrollrt.h" #include "engine/render/scrollrt.h"
#include "utils/attributes.h" #include "utils/attributes.h"
#include "utils/clx_decode.hpp"
#ifdef DEBUG_CLX #ifdef DEBUG_CLX
#include <fmt/format.h> #include <fmt/format.h>
@ -29,43 +30,6 @@ namespace {
* indicates a fill-N command. * indicates a fill-N command.
*/ */
constexpr bool IsCl2Opaque(uint8_t control)
{
constexpr uint8_t Cl2OpaqueMin = 0x80;
return control >= Cl2OpaqueMin;
}
constexpr uint8_t GetCl2OpaquePixelsWidth(uint8_t control)
{
return -static_cast<std::int8_t>(control);
}
constexpr bool IsCl2OpaqueFill(uint8_t control)
{
constexpr uint8_t Cl2FillMax = 0xBE;
return control <= Cl2FillMax;
}
constexpr uint8_t GetCl2OpaqueFillWidth(uint8_t control)
{
constexpr uint8_t Cl2FillEnd = 0xBF;
return static_cast<int_fast16_t>(Cl2FillEnd - control);
}
BlitCommand Cl2GetBlitCommand(const uint8_t *src)
{
const uint8_t control = *src++;
if (!IsCl2Opaque(control))
return BlitCommand { BlitType::Transparent, src, control, 0 };
if (IsCl2OpaqueFill(control)) {
const uint8_t width = GetCl2OpaqueFillWidth(control);
const uint8_t color = *src++;
return BlitCommand { BlitType::Fill, src, width, color };
}
const uint8_t width = GetCl2OpaquePixelsWidth(control);
return BlitCommand { BlitType::Pixels, src + width, width, 0 };
}
struct ClipX { struct ClipX {
int_fast16_t left; int_fast16_t left;
int_fast16_t right; int_fast16_t right;
@ -112,7 +76,7 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT const uint8_t *SkipRestOfLineWithOverrun(
{ {
int_fast16_t remainingWidth = srcWidth - skipSize.xOffset; int_fast16_t remainingWidth = srcWidth - skipSize.xOffset;
while (remainingWidth > 0) { while (remainingWidth > 0) {
const BlitCommand cmd = Cl2GetBlitCommand(src); const BlitCommand cmd = ClxGetBlitCommand(src);
src = cmd.srcEnd; src = cmd.srcEnd;
remainingWidth -= cmd.length; remainingWidth -= cmd.length;
} }
@ -149,7 +113,7 @@ void DoRenderBackwardsClipY(
auto remainingWidth = static_cast<int_fast16_t>(src.width) - xOffset; auto remainingWidth = static_cast<int_fast16_t>(src.width) - xOffset;
dst += xOffset; dst += xOffset;
while (remainingWidth > 0) { while (remainingWidth > 0) {
BlitCommand cmd = Cl2GetBlitCommand(src.begin); BlitCommand cmd = ClxGetBlitCommand(src.begin);
blitFn(cmd, dst, src.begin + 1); blitFn(cmd, dst, src.begin + 1);
src.begin = cmd.srcEnd; src.begin = cmd.srcEnd;
dst += cmd.length; dst += cmd.length;
@ -186,7 +150,7 @@ void DoRenderBackwardsClipXY(
remainingWidth += remainingLeftClip; remainingWidth += remainingLeftClip;
} }
while (remainingLeftClip > 0) { while (remainingLeftClip > 0) {
BlitCommand cmd = Cl2GetBlitCommand(src.begin); BlitCommand cmd = ClxGetBlitCommand(src.begin);
if (static_cast<int_fast16_t>(cmd.length) > remainingLeftClip) { if (static_cast<int_fast16_t>(cmd.length) > remainingLeftClip) {
const auto overshoot = static_cast<int>(cmd.length - remainingLeftClip); const auto overshoot = static_cast<int>(cmd.length - remainingLeftClip);
cmd.length = std::min<unsigned>(remainingWidth, overshoot); cmd.length = std::min<unsigned>(remainingWidth, overshoot);
@ -200,7 +164,7 @@ void DoRenderBackwardsClipXY(
remainingLeftClip -= cmd.length; remainingLeftClip -= cmd.length;
} }
while (remainingWidth > 0) { while (remainingWidth > 0) {
BlitCommand cmd = Cl2GetBlitCommand(src.begin); BlitCommand cmd = ClxGetBlitCommand(src.begin);
const unsigned unclippedLength = cmd.length; const unsigned unclippedLength = cmd.length;
cmd.length = std::min<unsigned>(remainingWidth, cmd.length); cmd.length = std::min<unsigned>(remainingWidth, cmd.length);
blitFn(cmd, dst, src.begin + 1); blitFn(cmd, dst, src.begin + 1);
@ -414,9 +378,9 @@ const uint8_t *RenderClxOutlineRowClipped( // NOLINT(readability-function-cognit
} }
while (remainingLeftClip > 0) { while (remainingLeftClip > 0) {
v = static_cast<uint8_t>(*src++); v = static_cast<uint8_t>(*src++);
if (IsCl2Opaque(v)) { if (IsClxOpaque(v)) {
const bool fill = IsCl2OpaqueFill(v); const bool fill = IsClxOpaqueFill(v);
v = fill ? GetCl2OpaqueFillWidth(v) : GetCl2OpaquePixelsWidth(v); v = fill ? GetClxOpaqueFillWidth(v) : GetClxOpaquePixelsWidth(v);
if (v > remainingLeftClip) { if (v > remainingLeftClip) {
const uint8_t overshoot = v - remainingLeftClip; const uint8_t overshoot = v - remainingLeftClip;
renderPixels(fill, overshoot); renderPixels(fill, overshoot);
@ -442,9 +406,9 @@ const uint8_t *RenderClxOutlineRowClipped( // NOLINT(readability-function-cognit
while (remainingWidth > 0) { while (remainingWidth > 0) {
v = static_cast<uint8_t>(*src++); v = static_cast<uint8_t>(*src++);
if (IsCl2Opaque(v)) { if (IsClxOpaque(v)) {
const bool fill = IsCl2OpaqueFill(v); const bool fill = IsClxOpaqueFill(v);
v = fill ? GetCl2OpaqueFillWidth(v) : GetCl2OpaquePixelsWidth(v); v = fill ? GetClxOpaqueFillWidth(v) : GetClxOpaquePixelsWidth(v);
renderPixels(fill, ClipWidth ? std::min(remainingWidth, static_cast<int_fast16_t>(v)) : v); renderPixels(fill, ClipWidth ? std::min(remainingWidth, static_cast<int_fast16_t>(v)) : v);
} else { } else {
dst += v; dst += v;
@ -650,14 +614,14 @@ void ClxApplyTrans(ClxSprite sprite, const uint8_t *trn)
while (remaining != 0) { while (remaining != 0) {
uint8_t val = *dst++; uint8_t val = *dst++;
--remaining; --remaining;
if (!IsCl2Opaque(val)) if (!IsClxOpaque(val))
continue; continue;
if (IsCl2OpaqueFill(val)) { if (IsClxOpaqueFill(val)) {
--remaining; --remaining;
*dst = trn[*dst]; *dst = trn[*dst];
dst++; dst++;
} else { } else {
val = GetCl2OpaquePixelsWidth(val); val = GetClxOpaquePixelsWidth(val);
remaining -= val; remaining -= val;
while (val-- > 0) { while (val-- > 0) {
*dst = trn[*dst]; *dst = trn[*dst];
@ -695,15 +659,15 @@ std::pair<int, int> ClxMeasureSolidHorizontalBounds(ClxSprite clx)
while (src < end) { while (src < end) {
while (xCur < width) { while (xCur < width) {
auto val = *src++; auto val = *src++;
if (!IsCl2Opaque(val)) { if (!IsClxOpaque(val)) {
xCur += val; xCur += val;
continue; continue;
} }
if (IsCl2OpaqueFill(val)) { if (IsClxOpaqueFill(val)) {
val = GetCl2OpaqueFillWidth(val); val = GetClxOpaqueFillWidth(val);
++src; ++src;
} else { } else {
val = GetCl2OpaquePixelsWidth(val); val = GetClxOpaquePixelsWidth(val);
src += val; src += val;
} }
xBegin = std::min(xBegin, xCur); xBegin = std::min(xBegin, xCur);
@ -729,7 +693,7 @@ std::string ClxDescribe(ClxSprite clx)
const uint8_t *src = clx.pixelData(); const uint8_t *src = clx.pixelData();
const uint8_t *end = src + clx.pixelDataSize(); const uint8_t *end = src + clx.pixelDataSize();
while (src < end) { while (src < end) {
BlitCommand cmd = Cl2GetBlitCommand(src); BlitCommand cmd = ClxGetBlitCommand(src);
switch (cmd.type) { switch (cmd.type) {
case BlitType::Transparent: case BlitType::Transparent:
out.append(fmt::format("Transp. | {:>5} | {:>5} |\n", cmd.length, cmd.srcEnd - src)); out.append(fmt::format("Transp. | {:>5} | {:>5} |\n", cmd.length, cmd.srcEnd - src));

28
Source/monster.cpp

@ -3352,6 +3352,32 @@ void InitMonsterGFX(CMonster &monsterType)
hasAnim); hasAnim);
} }
#ifndef UNPACKED_MPQS
if (!HeadlessMode) {
// Convert CL2 to CLX:
std::vector<std::vector<uint8_t>> clxData;
size_t accumulatedSize = 0;
for (size_t i = 0, j = 0; i < numAnims; ++i) {
if (!hasAnim(i))
continue;
const uint32_t begin = animOffsets[j];
const uint32_t end = animOffsets[j + 1];
clxData.emplace_back();
Cl2ToClx(reinterpret_cast<uint8_t *>(&monsterType.animData[begin]), end - begin,
PointerOrValue<uint16_t> { monsterData.width }, clxData.back());
animOffsets[j] = accumulatedSize;
accumulatedSize += clxData.back().size();
++j;
}
animOffsets[clxData.size()] = accumulatedSize;
monsterType.animData = nullptr;
monsterType.animData = std::unique_ptr<byte[]>(new byte[accumulatedSize]);
for (size_t i = 0; i < clxData.size(); ++i) {
memcpy(&monsterType.animData[animOffsets[i]], clxData[i].data(), clxData[i].size());
}
}
#endif
for (size_t i = 0, j = 0; i < numAnims; ++i) { for (size_t i = 0, j = 0; i < numAnims; ++i) {
AnimStruct &anim = monsterType.anims[i]; AnimStruct &anim = monsterType.anims[i];
if (!hasAnim(i)) { if (!hasAnim(i)) {
@ -3365,7 +3391,7 @@ void InitMonsterGFX(CMonster &monsterType)
const uint32_t begin = animOffsets[j]; const uint32_t begin = animOffsets[j];
const uint32_t end = animOffsets[j + 1]; const uint32_t end = animOffsets[j + 1];
auto spritesData = reinterpret_cast<uint8_t *>(&monsterType.animData[begin]); auto spritesData = reinterpret_cast<uint8_t *>(&monsterType.animData[begin]);
const uint16_t numLists = Cl2ToClx(spritesData, end - begin, PointerOrValue<uint16_t> { monsterData.width }); const uint16_t numLists = GetNumListsFromClxListOrSheetBuffer(spritesData, end - begin);
anim.sprites = ClxSpriteListOrSheet { spritesData, numLists }; anim.sprites = ClxSpriteListOrSheet { spritesData, numLists };
} }
++j; ++j;

8
Source/utils/cel_to_clx.cpp

@ -11,7 +11,7 @@
#endif #endif
#include "appfat.h" #include "appfat.h"
#include "utils/clx_write.hpp" #include "utils/clx_encode.hpp"
#include "utils/endian.hpp" #include "utils/endian.hpp"
namespace devilution { namespace devilution {
@ -101,9 +101,9 @@ OwnedClxSpriteListOrSheet CelToClx(const uint8_t *data, size_t size, PointerOrVa
val = GetCelTransparentWidth(val); val = GetCelTransparentWidth(val);
transparentRunWidth += val; transparentRunWidth += val;
} else { } else {
AppendCl2TransparentRun(transparentRunWidth, cl2Data); AppendClxTransparentRun(transparentRunWidth, cl2Data);
transparentRunWidth = 0; transparentRunWidth = 0;
AppendCl2PixelsOrFillRun(src, val, cl2Data); AppendClxPixelsOrFillRun(src, val, cl2Data);
src += val; src += val;
} }
remainingCelWidth -= val; remainingCelWidth -= val;
@ -112,7 +112,7 @@ OwnedClxSpriteListOrSheet CelToClx(const uint8_t *data, size_t size, PointerOrVa
} }
WriteLE16(&cl2Data[frameHeaderPos + 4], frameHeight); WriteLE16(&cl2Data[frameHeaderPos + 4], frameHeight);
memset(&cl2Data[frameHeaderPos + 6], 0, 4); memset(&cl2Data[frameHeaderPos + 6], 0, 4);
AppendCl2TransparentRun(transparentRunWidth, cl2Data); AppendClxTransparentRun(transparentRunWidth, cl2Data);
} }
WriteLE32(&cl2Data[cl2DataOffset + 4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(cl2Data.size() - cl2DataOffset)); WriteLE32(&cl2Data[cl2DataOffset + 4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(cl2Data.size() - cl2DataOffset));

142
Source/utils/cl2_to_clx.cpp

@ -3,71 +3,51 @@
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <vector>
#include "utils/clx_decode.hpp"
#include "utils/clx_encode.hpp"
#include "utils/endian.hpp" #include "utils/endian.hpp"
namespace devilution { namespace devilution {
namespace { namespace {
constexpr bool IsCl2Opaque(uint8_t control) constexpr size_t FrameHeaderSize = 10;
{
constexpr uint8_t Cl2OpaqueMin = 0x80;
return control >= Cl2OpaqueMin;
}
constexpr uint8_t GetCl2OpaquePixelsWidth(uint8_t control) struct SkipSize {
int_fast16_t wholeLines;
int_fast16_t xOffset;
};
SkipSize GetSkipSize(int_fast16_t overrun, int_fast16_t srcWidth)
{ {
return -static_cast<std::int8_t>(control); SkipSize result;
} result.wholeLines = overrun / srcWidth;
result.xOffset = overrun - srcWidth * result.wholeLines;
constexpr bool IsCl2OpaqueFill(uint8_t control) return result;
{
constexpr uint8_t Cl2FillMax = 0xBE;
return control <= Cl2FillMax;
}
constexpr uint8_t GetCl2OpaqueFillWidth(uint8_t control)
{
constexpr uint8_t Cl2FillEnd = 0xBF;
return static_cast<int_fast16_t>(Cl2FillEnd - control);
}
size_t CountCl2FramePixels(const uint8_t *src, const uint8_t *srcEnd)
{
size_t numPixels = 0;
while (src != srcEnd) {
uint8_t val = *src++;
if (IsCl2Opaque(val)) {
if (IsCl2OpaqueFill(val)) {
numPixels += GetCl2OpaqueFillWidth(val);
++src;
} else {
val = GetCl2OpaquePixelsWidth(val);
numPixels += val;
src += val;
}
} else {
numPixels += val;
}
}
return numPixels;
} }
} // namespace } // namespace
uint16_t Cl2ToClx(uint8_t *data, size_t size, PointerOrValue<uint16_t> widthOrWidths) uint16_t Cl2ToClx(const uint8_t *data, size_t size,
PointerOrValue<uint16_t> widthOrWidths, std::vector<uint8_t> &clxData)
{ {
uint32_t numGroups = 1; uint32_t numGroups = 1;
const uint32_t maybeNumFrames = LoadLE32(data); const uint32_t maybeNumFrames = LoadLE32(data);
uint8_t *groupBegin = data; const uint8_t *groupBegin = data;
// If it is a number of frames, then the last frame offset will be equal to the size of the file. // If it is a number of frames, then the last frame offset will be equal to the size of the file.
if (LoadLE32(&data[maybeNumFrames * 4 + 4]) != size) { if (LoadLE32(&data[maybeNumFrames * 4 + 4]) != size) {
// maybeNumFrames is the address of the first group, right after // maybeNumFrames is the address of the first group, right after
// the list of group offsets. // the list of group offsets.
numGroups = maybeNumFrames / 4; numGroups = maybeNumFrames / 4;
clxData.resize(maybeNumFrames);
} }
// Transient buffer for a contiguous run of non-transparent pixels.
std::vector<uint8_t> pixels;
pixels.reserve(4096);
for (size_t group = 0; group < numGroups; ++group) { for (size_t group = 0; group < numGroups; ++group) {
uint32_t numFrames; uint32_t numFrames;
if (numGroups == 1) { if (numGroups == 1) {
@ -75,22 +55,82 @@ uint16_t Cl2ToClx(uint8_t *data, size_t size, PointerOrValue<uint16_t> widthOrWi
} else { } else {
groupBegin = &data[LoadLE32(&data[group * 4])]; groupBegin = &data[LoadLE32(&data[group * 4])];
numFrames = LoadLE32(groupBegin); numFrames = LoadLE32(groupBegin);
WriteLE32(&clxData[4 * group], clxData.size());
} }
uint8_t *frameEnd = &groupBegin[LoadLE32(&groupBegin[4])]; // CLX header: frame count, frame offset for each frame, file size
const size_t clxDataOffset = clxData.size();
clxData.resize(clxData.size() + 4 * (2 + static_cast<size_t>(numFrames)));
WriteLE32(&clxData[clxDataOffset], numFrames);
const uint8_t *frameEnd = &groupBegin[LoadLE32(&groupBegin[4])];
for (size_t frame = 1; frame <= numFrames; ++frame) { for (size_t frame = 1; frame <= numFrames; ++frame) {
uint8_t *frameBegin = frameEnd; WriteLE32(&clxData[clxDataOffset + 4 * frame],
frameEnd = &groupBegin[LoadLE32(&groupBegin[4 * (frame + 1)])]; static_cast<uint32_t>(clxData.size() - clxDataOffset));
constexpr size_t Cl2FrameHeaderSize = 10; const uint8_t *frameBegin = frameEnd;
const size_t numPixels = CountCl2FramePixels(frameBegin + Cl2FrameHeaderSize, frameEnd); frameEnd = &groupBegin[LoadLE32(&groupBegin[4 * (frame + 1)])];
const uint16_t frameWidth = widthOrWidths.HoldsPointer() ? widthOrWidths.AsPointer()[frame - 1] : widthOrWidths.AsValue(); const uint16_t frameWidth = widthOrWidths.HoldsPointer() ? widthOrWidths.AsPointer()[frame - 1] : widthOrWidths.AsValue();
const uint16_t frameHeight = numPixels / frameWidth;
WriteLE16(&frameBegin[2], frameWidth); const size_t frameHeaderPos = clxData.size();
WriteLE16(&frameBegin[4], frameHeight); clxData.resize(clxData.size() + FrameHeaderSize);
memset(&frameBegin[6], 0, 4); WriteLE16(&clxData[frameHeaderPos], FrameHeaderSize);
WriteLE16(&clxData[frameHeaderPos + 2], frameWidth);
unsigned transparentRunWidth = 0;
int_fast16_t xOffset = 0;
size_t frameHeight = 0;
const uint8_t *src = frameBegin + FrameHeaderSize;
while (src != frameEnd) {
auto remainingWidth = static_cast<int_fast16_t>(frameWidth) - xOffset;
while (remainingWidth > 0) {
const BlitCommand cmd = ClxGetBlitCommand(src);
switch (cmd.type) {
case BlitType::Transparent:
if (!pixels.empty()) {
AppendClxPixelsOrFillRun(pixels.data(), pixels.size(), clxData);
pixels.clear();
}
transparentRunWidth += cmd.length;
break;
case BlitType::Fill:
case BlitType::Pixels:
AppendClxTransparentRun(transparentRunWidth, clxData);
transparentRunWidth = 0;
if (cmd.type == BlitType::Fill) {
pixels.insert(pixels.end(), cmd.length, cmd.color);
} else { // BlitType::Pixels
pixels.insert(pixels.end(), src + 1, cmd.srcEnd);
}
break;
}
src = cmd.srcEnd;
remainingWidth -= cmd.length;
}
++frameHeight;
if (remainingWidth < 0) {
const auto skipSize = GetSkipSize(-remainingWidth, static_cast<int_fast16_t>(frameWidth));
xOffset = skipSize.xOffset;
frameHeight += skipSize.wholeLines;
} else {
xOffset = 0;
}
}
if (!pixels.empty()) {
AppendClxPixelsOrFillRun(pixels.data(), pixels.size(), clxData);
pixels.clear();
}
AppendClxTransparentRun(transparentRunWidth, clxData);
WriteLE16(&clxData[frameHeaderPos + 4], frameHeight);
memset(&clxData[frameHeaderPos + 6], 0, 4);
} }
WriteLE32(&clxData[clxDataOffset + 4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(clxData.size() - clxDataOffset));
} }
return numGroups == 1 ? 0 : numGroups; return numGroups == 1 ? 0 : numGroups;
} }

11
Source/utils/cl2_to_clx.hpp

@ -2,8 +2,10 @@
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstring>
#include <memory> #include <memory>
#include <vector>
#include "engine/clx_sprite.hpp" #include "engine/clx_sprite.hpp"
#include "utils/pointer_value_union.hpp" #include "utils/pointer_value_union.hpp"
@ -15,11 +17,16 @@ namespace devilution {
* *
* @return uint16_t The number of lists in a sheet if it is a sheet, 0 otherwise. * @return uint16_t The number of lists in a sheet if it is a sheet, 0 otherwise.
*/ */
uint16_t Cl2ToClx(uint8_t *data, size_t size, PointerOrValue<uint16_t> widthOrWidths); uint16_t Cl2ToClx(const uint8_t *data, size_t size,
PointerOrValue<uint16_t> widthOrWidths, std::vector<uint8_t> &clxData);
inline OwnedClxSpriteListOrSheet Cl2ToClx(std::unique_ptr<uint8_t[]> &&data, size_t size, PointerOrValue<uint16_t> widthOrWidths) inline OwnedClxSpriteListOrSheet Cl2ToClx(std::unique_ptr<uint8_t[]> &&data, size_t size, PointerOrValue<uint16_t> widthOrWidths)
{ {
const uint16_t numLists = Cl2ToClx(data.get(), size, widthOrWidths); std::vector<uint8_t> clxData;
const uint16_t numLists = Cl2ToClx(data.get(), size, widthOrWidths, clxData);
data = nullptr;
data = std::unique_ptr<uint8_t[]>(new uint8_t[clxData.size()]);
memcpy(&data[0], clxData.data(), clxData.size());
return OwnedClxSpriteListOrSheet { std::move(data), numLists }; return OwnedClxSpriteListOrSheet { std::move(data), numLists };
} }

44
Source/utils/clx_decode.hpp

@ -0,0 +1,44 @@
#pragma once
#include "engine/render/blit_impl.hpp"
namespace devilution {
[[nodiscard]] constexpr bool IsClxOpaque(uint8_t control)
{
constexpr uint8_t ClxOpaqueMin = 0x80;
return control >= ClxOpaqueMin;
}
[[nodiscard]] constexpr uint8_t GetClxOpaquePixelsWidth(uint8_t control)
{
return -static_cast<std::int8_t>(control);
}
[[nodiscard]] constexpr bool IsClxOpaqueFill(uint8_t control)
{
constexpr uint8_t ClxFillMax = 0xBE;
return control <= ClxFillMax;
}
[[nodiscard]] constexpr uint8_t GetClxOpaqueFillWidth(uint8_t control)
{
constexpr uint8_t ClxFillEnd = 0xBF;
return static_cast<int_fast16_t>(ClxFillEnd - control);
}
[[nodiscard]] constexpr BlitCommand ClxGetBlitCommand(const uint8_t *src)
{
const uint8_t control = *src++;
if (!IsClxOpaque(control))
return BlitCommand { BlitType::Transparent, src, control, 0 };
if (IsClxOpaqueFill(control)) {
const uint8_t width = GetClxOpaqueFillWidth(control);
const uint8_t color = *src++;
return BlitCommand { BlitType::Fill, src, width, color };
}
const uint8_t width = GetClxOpaquePixelsWidth(control);
return BlitCommand { BlitType::Pixels, src + width, width, 0 };
}
} // namespace devilution

18
Source/utils/clx_write.hpp → Source/utils/clx_encode.hpp

@ -6,7 +6,7 @@
namespace devilution { namespace devilution {
inline void AppendCl2TransparentRun(unsigned width, std::vector<uint8_t> &out) inline void AppendClxTransparentRun(unsigned width, std::vector<uint8_t> &out)
{ {
while (width >= 0x7F) { while (width >= 0x7F) {
out.push_back(0x7F); out.push_back(0x7F);
@ -17,7 +17,7 @@ inline void AppendCl2TransparentRun(unsigned width, std::vector<uint8_t> &out)
out.push_back(width); out.push_back(width);
} }
inline void AppendCl2FillRun(uint8_t color, unsigned width, std::vector<uint8_t> &out) inline void AppendClxFillRun(uint8_t color, unsigned width, std::vector<uint8_t> &out)
{ {
while (width >= 0x3F) { while (width >= 0x3F) {
out.push_back(0x80); out.push_back(0x80);
@ -30,7 +30,7 @@ inline void AppendCl2FillRun(uint8_t color, unsigned width, std::vector<uint8_t>
out.push_back(color); out.push_back(color);
} }
inline void AppendCl2PixelsRun(const uint8_t *src, unsigned width, std::vector<uint8_t> &out) inline void AppendClxPixelsRun(const uint8_t *src, unsigned width, std::vector<uint8_t> &out)
{ {
while (width >= 0x41) { while (width >= 0x41) {
out.push_back(0xBF); out.push_back(0xBF);
@ -46,7 +46,7 @@ inline void AppendCl2PixelsRun(const uint8_t *src, unsigned width, std::vector<u
out.push_back(src[i]); out.push_back(src[i]);
} }
inline void AppendCl2PixelsOrFillRun(const uint8_t *src, unsigned length, std::vector<uint8_t> &out) inline void AppendClxPixelsOrFillRun(const uint8_t *src, unsigned length, std::vector<uint8_t> &out)
{ {
const uint8_t *begin = src; const uint8_t *begin = src;
const uint8_t *prevColorBegin = src; const uint8_t *prevColorBegin = src;
@ -61,8 +61,8 @@ inline void AppendCl2PixelsOrFillRun(const uint8_t *src, unsigned length, std::v
// 3 appears to be optimal for most of our data (much better than 2, rarely very slightly worse than 4). // 3 appears to be optimal for most of our data (much better than 2, rarely very slightly worse than 4).
constexpr unsigned MinFillRunLength = 3; constexpr unsigned MinFillRunLength = 3;
if (prevColorRunLength >= MinFillRunLength) { if (prevColorRunLength >= MinFillRunLength) {
AppendCl2PixelsRun(begin, prevColorBegin - begin, out); AppendClxPixelsRun(begin, prevColorBegin - begin, out);
AppendCl2FillRun(prevColor, prevColorRunLength, out); AppendClxFillRun(prevColor, prevColorRunLength, out);
begin = src; begin = src;
} }
prevColorBegin = src; prevColorBegin = src;
@ -76,10 +76,10 @@ inline void AppendCl2PixelsOrFillRun(const uint8_t *src, unsigned length, std::v
// is followed by transparent pixels. // is followed by transparent pixels.
// Width=2 Fill command takes 2 bytes, while the Pixels command is 3 bytes. // Width=2 Fill command takes 2 bytes, while the Pixels command is 3 bytes.
if (prevColorRunLength >= 2) { if (prevColorRunLength >= 2) {
AppendCl2PixelsRun(begin, prevColorBegin - begin, out); AppendClxPixelsRun(begin, prevColorBegin - begin, out);
AppendCl2FillRun(prevColor, prevColorRunLength, out); AppendClxFillRun(prevColor, prevColorRunLength, out);
} else { } else {
AppendCl2PixelsRun(begin, prevColorBegin - begin + prevColorRunLength, out); AppendClxPixelsRun(begin, prevColorBegin - begin + prevColorRunLength, out);
} }
} }

12
Source/utils/pcx_to_clx.cpp

@ -10,7 +10,7 @@
#include <SDL_endian.h> #include <SDL_endian.h>
#include "appfat.h" #include "appfat.h"
#include "utils/clx_write.hpp" #include "utils/clx_encode.hpp"
#include "utils/endian.hpp" #include "utils/endian.hpp"
#include "utils/pcx.hpp" #include "utils/pcx.hpp"
#include "utils/stdcompat/cstddef.hpp" #include "utils/stdcompat/cstddef.hpp"
@ -142,25 +142,25 @@ OptionalOwnedClxSpriteList PcxToClx(AssetHandle &handle, size_t fileSize, int nu
for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) { for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) {
if (*src == *transparentColor) { if (*src == *transparentColor) {
if (solidRunWidth != 0) { if (solidRunWidth != 0) {
AppendCl2PixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, cl2Data); AppendClxPixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, cl2Data);
solidRunWidth = 0; solidRunWidth = 0;
} }
++transparentRunWidth; ++transparentRunWidth;
} else { } else {
AppendCl2TransparentRun(transparentRunWidth, cl2Data); AppendClxTransparentRun(transparentRunWidth, cl2Data);
transparentRunWidth = 0; transparentRunWidth = 0;
++solidRunWidth; ++solidRunWidth;
} }
} }
if (solidRunWidth != 0) { if (solidRunWidth != 0) {
AppendCl2PixelsOrFillRun(src - solidRunWidth, solidRunWidth, cl2Data); AppendClxPixelsOrFillRun(src - solidRunWidth, solidRunWidth, cl2Data);
} }
} else { } else {
AppendCl2PixelsOrFillRun(src, width, cl2Data); AppendClxPixelsOrFillRun(src, width, cl2Data);
} }
++line; ++line;
} }
AppendCl2TransparentRun(transparentRunWidth, cl2Data); AppendClxTransparentRun(transparentRunWidth, cl2Data);
} }
WriteLE32(&cl2Data[4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(cl2Data.size())); WriteLE32(&cl2Data[4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(cl2Data.size()));

12
Source/utils/surface_to_clx.cpp

@ -4,7 +4,7 @@
#include <cstring> #include <cstring>
#include <vector> #include <vector>
#include "utils/clx_write.hpp" #include "utils/clx_encode.hpp"
#include "utils/endian.hpp" #include "utils/endian.hpp"
#ifdef DEBUG_SURFACE_TO_CLX_SIZE #ifdef DEBUG_SURFACE_TO_CLX_SIZE
@ -56,25 +56,25 @@ OwnedClxSpriteList SurfaceToClx(const Surface &surface, unsigned numFrames,
for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) { for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) {
if (*src == *transparentColor) { if (*src == *transparentColor) {
if (solidRunWidth != 0) { if (solidRunWidth != 0) {
AppendCl2PixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, clxData); AppendClxPixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, clxData);
solidRunWidth = 0; solidRunWidth = 0;
} }
++transparentRunWidth; ++transparentRunWidth;
} else { } else {
AppendCl2TransparentRun(transparentRunWidth, clxData); AppendClxTransparentRun(transparentRunWidth, clxData);
transparentRunWidth = 0; transparentRunWidth = 0;
++solidRunWidth; ++solidRunWidth;
} }
} }
if (solidRunWidth != 0) { if (solidRunWidth != 0) {
AppendCl2PixelsOrFillRun(src - solidRunWidth, solidRunWidth, clxData); AppendClxPixelsOrFillRun(src - solidRunWidth, solidRunWidth, clxData);
} }
} else { } else {
AppendCl2PixelsOrFillRun(src, width, clxData); AppendClxPixelsOrFillRun(src, width, clxData);
} }
++line; ++line;
} }
AppendCl2TransparentRun(transparentRunWidth, clxData); AppendClxTransparentRun(transparentRunWidth, clxData);
dataPtr += static_cast<unsigned>(pitch * frameHeight); dataPtr += static_cast<unsigned>(pitch * frameHeight);
} }

Loading…
Cancel
Save