Browse Source
Similar to #5059, which converted CEL to CL2 at load time, we now do the same for PCX. Some size stats: https://gist.github.com/glebm/067bf1ec73f9d14514932cfe237e4e8e Notably, fonts RAM usage is decreased by ~35%.pull/5084/head
27 changed files with 554 additions and 371 deletions
@ -0,0 +1,266 @@
|
||||
#include "utils/pcx_to_cl2.hpp" |
||||
|
||||
#include <array> |
||||
#include <cstdint> |
||||
#include <cstring> |
||||
#include <memory> |
||||
#include <vector> |
||||
|
||||
#include "appfat.h" |
||||
#include "utils/endian.hpp" |
||||
#include "utils/pcx.hpp" |
||||
#include "utils/stdcompat/cstddef.hpp" |
||||
|
||||
#ifdef DEBUG_PCX_TO_CL2_SIZE |
||||
#include <iomanip> |
||||
#include <iostream> |
||||
#endif |
||||
|
||||
#ifdef USE_SDL1 |
||||
#include "utils/sdl2_to_1_2_backports.h" |
||||
#endif |
||||
|
||||
namespace devilution { |
||||
|
||||
namespace { |
||||
|
||||
void AppendCl2TransparentRun(unsigned width, std::vector<uint8_t> &out) |
||||
{ |
||||
while (width >= 0x7F) { |
||||
out.push_back(0x7F); |
||||
width -= 0x7F; |
||||
} |
||||
if (width == 0) |
||||
return; |
||||
out.push_back(width); |
||||
} |
||||
|
||||
void AppendCl2FillRun(uint8_t color, unsigned width, std::vector<uint8_t> &out) |
||||
{ |
||||
while (width >= 0x3F) { |
||||
out.push_back(0x80); |
||||
out.push_back(color); |
||||
width -= 0x3F; |
||||
} |
||||
if (width == 0) |
||||
return; |
||||
out.push_back(0xBF - width); |
||||
out.push_back(color); |
||||
} |
||||
|
||||
void AppendCl2PixelsRun(const uint8_t *src, unsigned width, std::vector<uint8_t> &out) |
||||
{ |
||||
while (width >= 0x41) { |
||||
out.push_back(0xBF); |
||||
for (size_t i = 0; i < 0x41; ++i) |
||||
out.push_back(src[i]); |
||||
width -= 0x41; |
||||
src += 0x41; |
||||
} |
||||
if (width == 0) |
||||
return; |
||||
out.push_back(256 - width); |
||||
for (size_t i = 0; i < width; ++i) |
||||
out.push_back(src[i]); |
||||
} |
||||
|
||||
void AppendCl2PixelsOrFillRun(const uint8_t *src, unsigned length, std::vector<uint8_t> &out) |
||||
{ |
||||
const uint8_t *begin = src; |
||||
const uint8_t *prevColorBegin = src; |
||||
unsigned prevColorRunLength = 1; |
||||
uint8_t prevColor = *src++; |
||||
while (--length > 0) { |
||||
const uint8_t color = *src; |
||||
if (prevColor == color) { |
||||
++prevColorRunLength; |
||||
} else { |
||||
// A tunable parameter that decides at which minimum length we encode a fill run.
|
||||
// 3 appears to be optimal for most of our data (much better than 2, rarely very slightly worse than 4).
|
||||
constexpr unsigned MinFillRunLength = 3; |
||||
if (prevColorRunLength >= MinFillRunLength) { |
||||
AppendCl2PixelsRun(begin, prevColorBegin - begin, out); |
||||
AppendCl2FillRun(prevColor, prevColorRunLength, out); |
||||
begin = src; |
||||
} |
||||
prevColorBegin = src; |
||||
prevColorRunLength = 1; |
||||
prevColor = color; |
||||
} |
||||
++src; |
||||
} |
||||
AppendCl2PixelsRun(begin, prevColorBegin - begin, out); |
||||
AppendCl2FillRun(prevColor, prevColorRunLength, out); |
||||
} |
||||
|
||||
size_t GetReservationSize(size_t pcxSize) |
||||
{ |
||||
// For the most part, CL2 is smaller than PCX, with a few exceptions.
|
||||
switch (pcxSize) { |
||||
case 2352187: // ui_art\hf_logo1.pcx
|
||||
return 2464867; |
||||
case 172347: // ui_art\creditsw.pcx
|
||||
return 172347; |
||||
case 157275: // ui_art\credits.pcx
|
||||
return 173367; |
||||
default: |
||||
return pcxSize; |
||||
} |
||||
} |
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<OwnedCelSpriteWithFrameHeight> PcxToCl2(SDL_RWops *handle, int numFramesOrFrameHeight, std::optional<uint8_t> transparentColor, SDL_Color *outPalette) |
||||
{ |
||||
int width; |
||||
int height; |
||||
uint8_t bpp; |
||||
if (!LoadPcxMeta(handle, width, height, bpp)) { |
||||
SDL_RWclose(handle); |
||||
return std::nullopt; |
||||
} |
||||
assert(bpp == 8); |
||||
|
||||
unsigned numFrames; |
||||
unsigned frameHeight; |
||||
if (numFramesOrFrameHeight > 0) { |
||||
numFrames = numFramesOrFrameHeight; |
||||
frameHeight = height / numFrames; |
||||
} else { |
||||
frameHeight = -numFramesOrFrameHeight; |
||||
numFrames = height / frameHeight; |
||||
} |
||||
|
||||
ptrdiff_t pixelDataSize = SDL_RWsize(handle); |
||||
if (pixelDataSize < 0) { |
||||
SDL_RWclose(handle); |
||||
return std::nullopt; |
||||
} |
||||
|
||||
pixelDataSize -= PcxHeaderSize; |
||||
|
||||
std::unique_ptr<uint8_t[]> fileBuffer { new uint8_t[pixelDataSize] }; |
||||
if (SDL_RWread(handle, fileBuffer.get(), pixelDataSize, 1) == 0) { |
||||
SDL_RWclose(handle); |
||||
return std::nullopt; |
||||
} |
||||
|
||||
// CEL header: frame count, frame offset for each frame, file size
|
||||
std::vector<uint8_t> cl2Data; |
||||
cl2Data.reserve(GetReservationSize(pixelDataSize)); |
||||
cl2Data.resize(4 * (2 + static_cast<size_t>(numFrames))); |
||||
WriteLE32(cl2Data.data(), numFrames); |
||||
|
||||
// We process the PCX a whole frame at a time because the lines are reversed in CEL.
|
||||
auto frameBuffer = std::unique_ptr<uint8_t[]>(new uint8_t[static_cast<size_t>(frameHeight) * width]); |
||||
|
||||
const unsigned srcSkip = width % 2; |
||||
uint8_t *dataPtr = fileBuffer.get(); |
||||
for (unsigned frame = 1; frame <= numFrames; ++frame) { |
||||
WriteLE32(&cl2Data[4 * static_cast<size_t>(frame)], static_cast<uint32_t>(cl2Data.size())); |
||||
|
||||
// Frame header: 5 16-bit offsets to 32-pixel height blocks.
|
||||
const size_t frameHeaderPos = cl2Data.size(); |
||||
constexpr size_t FrameHeaderSize = 10; |
||||
cl2Data.resize(cl2Data.size() + FrameHeaderSize); |
||||
|
||||
// Frame header offset (first of five):
|
||||
WriteLE16(&cl2Data[frameHeaderPos], FrameHeaderSize); |
||||
|
||||
for (unsigned j = 0; j < frameHeight; ++j) { |
||||
uint8_t *buffer = &frameBuffer[static_cast<size_t>(j) * width]; |
||||
for (unsigned x = 0; x < static_cast<unsigned>(width);) { |
||||
constexpr uint8_t PcxMaxSinglePixel = 0xBF; |
||||
const uint8_t byte = *dataPtr++; |
||||
if (byte <= PcxMaxSinglePixel) { |
||||
*buffer++ = byte; |
||||
++x; |
||||
continue; |
||||
} |
||||
constexpr uint8_t PcxRunLengthMask = 0x3F; |
||||
const uint8_t runLength = (byte & PcxRunLengthMask); |
||||
std::memset(buffer, *dataPtr++, runLength); |
||||
buffer += runLength; |
||||
x += runLength; |
||||
} |
||||
dataPtr += srcSkip; |
||||
} |
||||
|
||||
unsigned transparentRunWidth = 0; |
||||
size_t line = 0; |
||||
while (line != frameHeight) { |
||||
// Process line:
|
||||
const uint8_t *src = &frameBuffer[(frameHeight - (line + 1)) * width]; |
||||
if (transparentColor) { |
||||
unsigned solidRunWidth = 0; |
||||
for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) { |
||||
if (*src == *transparentColor) { |
||||
if (solidRunWidth != 0) { |
||||
AppendCl2PixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, cl2Data); |
||||
solidRunWidth = 0; |
||||
} |
||||
++transparentRunWidth; |
||||
} else { |
||||
AppendCl2TransparentRun(transparentRunWidth, cl2Data); |
||||
transparentRunWidth = 0; |
||||
++solidRunWidth; |
||||
} |
||||
} |
||||
if (solidRunWidth != 0) { |
||||
AppendCl2PixelsOrFillRun(src - solidRunWidth, solidRunWidth, cl2Data); |
||||
} |
||||
} else { |
||||
AppendCl2PixelsOrFillRun(src, width, cl2Data); |
||||
} |
||||
|
||||
// Frame header offset:
|
||||
switch (++line) { |
||||
case 32: |
||||
case 64: |
||||
case 96: |
||||
case 128: |
||||
// Finish any active transparent run to not allow it to go over an offset line boundary.
|
||||
AppendCl2TransparentRun(transparentRunWidth, cl2Data); |
||||
transparentRunWidth = 0; |
||||
WriteLE16(&cl2Data[frameHeaderPos + line / 16], static_cast<uint16_t>(cl2Data.size() - frameHeaderPos)); |
||||
break; |
||||
} |
||||
} |
||||
AppendCl2TransparentRun(transparentRunWidth, cl2Data); |
||||
} |
||||
WriteLE32(&cl2Data[4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(cl2Data.size())); |
||||
|
||||
if (outPalette != nullptr) { |
||||
[[maybe_unused]] constexpr unsigned PcxPaletteSeparator = 0x0C; |
||||
assert(*dataPtr == PcxPaletteSeparator); // PCX may not have a palette
|
||||
++dataPtr; |
||||
|
||||
for (unsigned i = 0; i < 256; ++i) { |
||||
outPalette->r = *dataPtr++; |
||||
outPalette->g = *dataPtr++; |
||||
outPalette->b = *dataPtr++; |
||||
#ifndef USE_SDL1 |
||||
outPalette->a = SDL_ALPHA_OPAQUE; |
||||
#endif |
||||
++outPalette; |
||||
} |
||||
} |
||||
|
||||
SDL_RWclose(handle); |
||||
|
||||
// Release buffers before allocating the result array to reduce peak memory use.
|
||||
frameBuffer = nullptr; |
||||
fileBuffer = nullptr; |
||||
|
||||
auto out = std::unique_ptr<byte[]>(new byte[cl2Data.size()]); |
||||
memcpy(&out[0], cl2Data.data(), cl2Data.size()); |
||||
#ifdef DEBUG_PCX_TO_CL2_SIZE |
||||
std::cout << "\t" << pixelDataSize << "\t" << cl2Data.size() << "\t" << std::setprecision(1) << std::fixed << (static_cast<int>(cl2Data.size()) - static_cast<int>(pixelDataSize)) / ((float)pixelDataSize) * 100 << "%" << std::endl; |
||||
#endif |
||||
return OwnedCelSpriteWithFrameHeight { |
||||
OwnedCelSprite { std::move(out), static_cast<uint16_t>(width) }, |
||||
static_cast<uint16_t>(frameHeight) |
||||
}; |
||||
} |
||||
|
||||
} // namespace devilution
|
||||
@ -0,0 +1,21 @@
|
||||
#pragma once |
||||
|
||||
#include <cstdint> |
||||
|
||||
#include <SDL.h> |
||||
|
||||
#include "engine/cel_sprite.hpp" |
||||
#include "utils/stdcompat/optional.hpp" |
||||
|
||||
namespace devilution { |
||||
|
||||
/** @brief Loads a PCX file as a CL2 sprite.
|
||||
* |
||||
* |
||||
* @param handle A non-null SDL_RWops handle. Closed by this function. |
||||
* @param numFramesOrFrameHeight Pass a positive value with the number of frames, or the frame height as a negative value. |
||||
* @param transparentColorIndex The PCX palette index of the transparent color. |
||||
*/ |
||||
std::optional<OwnedCelSpriteWithFrameHeight> PcxToCl2(SDL_RWops *handle, int numFramesOrFrameHeight = 1, std::optional<uint8_t> transparentColor = std::nullopt, SDL_Color *outPalette = nullptr); |
||||
|
||||
} // namespace devilution
|
||||
Loading…
Reference in new issue