Browse Source

An option to convert fonts to CEL in-memory

Reduced RAM usage by 200 KiB with no performance drop, perhaps even an
improvement.
pull/4518/head
Gleb Mazovetskiy 4 years ago
parent
commit
2b161e5535
  1. 1
      CMake/Definitions.cmake
  2. 2
      CMakeLists.txt
  3. 1
      Source/CMakeLists.txt
  4. 10
      Source/engine/cel_sprite.hpp
  5. 31
      Source/engine/render/cel_render.cpp
  6. 8
      Source/engine/render/cel_render.hpp
  7. 82
      Source/engine/render/text_render.cpp
  8. 175
      Source/utils/pcx_to_cel.cpp
  9. 20
      Source/utils/pcx_to_cel.hpp

1
CMake/Definitions.cmake

@ -16,6 +16,7 @@ foreach(
STREAM_ALL_AUDIO
PACKET_ENCRYPTION
DEVILUTIONX_MASKED_ART_RLE
DEVILUTIONX_CONVERT_FONTS_TO_CEL
)
if(${def_name})
list(APPEND DEVILUTIONX_DEFINITIONS ${def_name})

2
CMakeLists.txt

@ -61,6 +61,8 @@ mark_as_advanced(STREAM_ALL_AUDIO)
option(DEVILUTIONX_MASKED_ART_RLE "RLE-compress masked art in memory (e.g. fonts). Slightly lower RAM usage at a significant performance cost." OFF)
option(DEVILUTIONX_CONVERT_FONTS_TO_CEL "Convert PCX font files to CEL in memory.")
if(TSAN)
set(ASAN OFF)
endif()

1
Source/CMakeLists.txt

@ -108,6 +108,7 @@ set(libdevilutionx_SRCS
utils/logged_fstream.cpp
utils/paths.cpp
utils/pcx.cpp
utils/pcx_to_cel.cpp
utils/sdl_bilinear_scale.cpp
utils/sdl_thread.cpp
utils/utf8.cpp

10
Source/engine/cel_sprite.hpp

@ -80,6 +80,11 @@ public:
OwnedCelSprite(OwnedCelSprite &&) noexcept = default;
OwnedCelSprite &operator=(OwnedCelSprite &&) noexcept = default;
[[nodiscard]] byte *MutableData()
{
return data_.get();
}
private:
std::unique_ptr<byte[]> data_;
};
@ -89,4 +94,9 @@ inline CelSprite::CelSprite(const OwnedCelSprite &owned)
{
}
struct OwnedCelSpriteWithFrameHeight {
OwnedCelSprite sprite;
unsigned frameHeight;
};
} // namespace devilution

31
Source/engine/render/cel_render.cpp

@ -708,4 +708,35 @@ std::pair<int, int> MeasureSolidHorizontalBounds(CelSprite cel, int frame)
return { xBegin, xEnd };
}
void CelApplyTrans(byte *p, const std::array<uint8_t, 256> &translation)
{
assert(p != nullptr);
const uint32_t numFrames = LoadLE32(p);
const byte *frameOffsets = p + 4;
p += 4 * (2 + static_cast<size_t>(numFrames));
uint32_t frameEnd = LoadLE32(&frameOffsets[0]);
for (uint32_t i = 0; i < numFrames; ++i) {
const uint32_t frameBegin = frameEnd;
frameEnd = LoadLE32(&frameOffsets[4 * (static_cast<size_t>(i) + 1)]);
const byte *end = p + (frameEnd - frameBegin);
const bool frameHasHeader = static_cast<uint8_t>(*p) == 0;
if (frameHasHeader) {
constexpr uint32_t FrameHeaderSize = 5 * 2;
p += FrameHeaderSize;
}
while (p != end) {
const auto val = static_cast<uint8_t>(*p++);
if (IsCelTransparent(val)) {
continue;
}
for (unsigned i = 0; i < val; ++i) {
const auto color = static_cast<uint8_t>(*p);
*p++ = static_cast<byte>(translation[color]);
}
}
}
}
} // namespace devilution

8
Source/engine/render/cel_render.hpp

@ -20,6 +20,14 @@ namespace devilution {
*/
std::pair<int, int> MeasureSolidHorizontalBounds(CelSprite cel, int frame = 0);
/**
* @brief Apply the color swaps to a CEL sprite
*
* @param p CEL buffer
* @param translation Palette translation table
*/
void CelApplyTrans(byte *p, const std::array<uint8_t, 256> &translation);
/**
* @brief Blit CEL sprite to the back buffer at the given coordinates
* @param out Target buffer

82
Source/engine/render/text_render.cpp

@ -21,7 +21,9 @@
#include "palette.h"
#include "utils/display.h"
#include "utils/language.h"
#include "utils/pcx_to_cel.hpp"
#include "utils/sdl_compat.h"
#include "utils/stdcompat/optional.hpp"
#include "utils/utf8.hpp"
namespace devilution {
@ -32,7 +34,13 @@ namespace {
constexpr char32_t ZWSP = U'\u200B'; // Zero-width space
#ifdef DEVILUTIONX_CONVERT_FONTS_TO_CEL
using Font = const OwnedCelSpriteWithFrameHeight;
std::unordered_map<uint32_t, std::optional<OwnedCelSpriteWithFrameHeight>> Fonts;
#else
using Font = Art;
std::unordered_map<uint32_t, Art> Fonts;
#endif
std::unordered_map<uint32_t, std::array<uint8_t, 256>> FontKerns;
std::array<int, 6> FontSizes = { 12, 24, 30, 42, 46, 22 };
std::array<uint8_t, 6> CJKWidth = { 17, 24, 28, 41, 47, 16 };
@ -162,9 +170,61 @@ std::array<uint8_t, 256> *LoadFontKerning(GameFontTables size, uint16_t row)
return kerning;
}
uint32_t GetFontId(GameFontTables size, text_color color, uint16_t row)
{
return (color << 24) | (size << 16) | row;
}
void GetFontPath(GameFontTables size, uint16_t row, char *out)
{
sprintf(out, "fonts\\%i-%02x.pcx", FontSizes[size], row);
}
#ifdef DEVILUTIONX_CONVERT_FONTS_TO_CEL
const OwnedCelSpriteWithFrameHeight *LoadFont(GameFontTables size, text_color color, uint16_t row)
{
const uint32_t fontId = GetFontId(size, color, row);
auto hotFont = Fonts.find(fontId);
if (hotFont != Fonts.end()) {
return &*hotFont->second;
}
char path[32];
GetFontPath(size, row, &path[0]);
std::optional<OwnedCelSpriteWithFrameHeight> &font = Fonts[fontId];
SDL_RWops *handle = OpenAsset(path);
if (handle == nullptr) {
LogError("Missing font: {}", path);
return nullptr;
}
constexpr unsigned NumFrames = 256;
font = LoadPcxAsCel(handle, NumFrames, /*generateFrameHeaders=*/false);
if (font->sprite.Data() == nullptr) {
LogError("Error loading font: {}", path);
font = std::nullopt;
return nullptr;
}
if (ColorTranlations[color] != nullptr) {
std::array<uint8_t, 256> colorMapping;
LoadFileInMem(ColorTranlations[color], colorMapping);
CelApplyTrans(font->sprite.MutableData(), colorMapping);
}
return &(*font);
}
void DrawFont(const Surface &out, Point position, const OwnedCelSpriteWithFrameHeight *font, int frame)
{
CelDrawTo(out, { position.x, static_cast<int>(position.y + font->frameHeight) }, CelSprite { font->sprite }, frame);
}
#else
Art *LoadFont(GameFontTables size, text_color color, uint16_t row)
{
uint32_t fontId = (color << 24) | (size << 16) | row;
const uint32_t fontId = GetFontId(size, color, row);
auto hotFont = Fonts.find(fontId);
if (hotFont != Fonts.end()) {
@ -172,7 +232,7 @@ Art *LoadFont(GameFontTables size, text_color color, uint16_t row)
}
char path[32];
sprintf(path, "fonts\\%i-%02x.pcx", FontSizes[size], row);
GetFontPath(size, row, &path[0]);
auto *font = &Fonts[fontId];
@ -190,6 +250,12 @@ Art *LoadFont(GameFontTables size, text_color color, uint16_t row)
return font;
}
void DrawFont(const Surface &out, Point position, Art *font, int frame)
{
DrawArt(out, position, font, frame);
}
#endif
bool IsWhitespace(char32_t c)
{
return IsAnyOf(c, U' ', U' ', ZWSP);
@ -325,7 +391,7 @@ int DoDrawString(const Surface &out, string_view text, Rectangle rect, Point &ch
int spacing, int lineHeight, int lineWidth, int rightMargin, int bottomMargin,
UiFlags flags, GameFontTables size, text_color color)
{
Art *font = nullptr;
Font *font = nullptr;
std::array<uint8_t, 256> *kerning = nullptr;
uint32_t currentUnicodeRow = 0;
@ -367,7 +433,7 @@ int DoDrawString(const Surface &out, string_view text, Rectangle rect, Point &ch
continue;
}
DrawArt(out, characterPosition, font, frame);
DrawFont(out, characterPosition, font, frame);
characterPosition.x += (*kerning)[frame] + spacing;
}
return text.data() - remaining.data();
@ -626,7 +692,7 @@ uint32_t DrawString(const Surface &out, string_view text, const Rectangle &rect,
if (HasAnyOf(flags, UiFlags::PentaCursor)) {
CelDrawTo(out, characterPosition + Displacement { 0, lineHeight - BaseLineOffset[size] }, *pSPentSpn2Cels, PentSpn2Spin());
} else if (HasAnyOf(flags, UiFlags::TextCursor) && GetAnimationFrame(2, 500) != 0) {
DrawArt(out, characterPosition, LoadFont(size, color, 0), '|');
DrawFont(out, characterPosition, LoadFont(size, color, 0), '|');
}
return bytesDrawn;
@ -665,7 +731,7 @@ void DrawStringWithColors(const Surface &out, string_view fmt, DrawStringFormatA
characterPosition.y += BaseLineOffset[size];
Art *font = nullptr;
Font *font = nullptr;
std::array<uint8_t, 256> *kerning = nullptr;
char32_t prev = U'\0';
@ -726,7 +792,7 @@ void DrawStringWithColors(const Surface &out, string_view fmt, DrawStringFormatA
}
}
DrawArt(out, characterPosition, font, frame);
DrawFont(out, characterPosition, font, frame);
characterPosition.x += (*kerning)[frame] + spacing;
prev = next;
}
@ -734,7 +800,7 @@ void DrawStringWithColors(const Surface &out, string_view fmt, DrawStringFormatA
if (HasAnyOf(flags, UiFlags::PentaCursor)) {
CelDrawTo(out, characterPosition + Displacement { 0, lineHeight - BaseLineOffset[size] }, *pSPentSpn2Cels, PentSpn2Spin());
} else if (HasAnyOf(flags, UiFlags::TextCursor) && GetAnimationFrame(2, 500) != 0) {
DrawArt(out, characterPosition, LoadFont(size, color, 0), '|');
DrawFont(out, characterPosition, LoadFont(size, color, 0), '|');
}
}

175
Source/utils/pcx_to_cel.cpp

@ -0,0 +1,175 @@
#include "utils/pcx_to_cel.hpp"
#include <array>
#include <cstdint>
#include <cstring>
#include <memory>
#include <vector>
#include "appfat.h"
#include "utils/pcx.hpp"
#include "utils/stdcompat/cstddef.hpp"
#ifdef USE_SDL1
#include "utils/sdl2_to_1_2_backports.h"
#endif
namespace devilution {
namespace {
constexpr uint8_t PcxTransparentColorIndex = 1;
void WriteLE32(uint8_t *out, uint32_t val)
{
const uint32_t littleEndian = SDL_SwapLE32(val);
memcpy(out, &littleEndian, 4);
}
void WriteLE16(uint8_t *out, uint16_t val)
{
const uint16_t littleEndian = SDL_SwapLE16(val);
memcpy(out, &littleEndian, 2);
}
void AppendCelTransparentRun(uint8_t width, std::vector<uint8_t> &out)
{
out.push_back(0xFF - (width - 1));
}
void AppendCelSolidRun(const uint8_t *src, unsigned width, std::vector<uint8_t> &out)
{
assert(width < 126);
out.push_back(width);
for (size_t i = 0; i < width; ++i)
out.push_back(src[i]);
}
void AppendCelLine(const uint8_t *src, unsigned width, std::vector<uint8_t> &out)
{
unsigned runBegin = 0;
bool transparentRun = false;
for (unsigned i = 0; i < width; ++i) {
const uint8_t pixel = src[i];
if (pixel == PcxTransparentColorIndex) {
if (transparentRun)
continue;
if (runBegin != i)
AppendCelSolidRun(src + runBegin, i - runBegin, out);
transparentRun = true;
runBegin = i;
} else if (transparentRun) {
AppendCelTransparentRun(i - runBegin, out);
transparentRun = false;
runBegin = i;
}
}
if (transparentRun) {
AppendCelTransparentRun(width - runBegin, out);
} else {
AppendCelSolidRun(src + runBegin, width - runBegin, out);
}
}
} // namespace
std::optional<OwnedCelSpriteWithFrameHeight> LoadPcxAsCel(SDL_RWops *handle, unsigned numFrames, bool generateFrameHeaders)
{
int width;
int height;
uint8_t bpp;
if (!LoadPcxMeta(handle, width, height, bpp)) {
SDL_RWclose(handle);
return std::nullopt;
}
assert(bpp == 8);
assert(width <= 128);
uint32_t pixelDataSize = SDL_RWsize(handle);
if (pixelDataSize == static_cast<uint32_t>(-1)) {
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> celData(4 * (2 + static_cast<size_t>(numFrames)));
WriteLE32(&celData[0], numFrames);
// We process the PCX a whole frame at a time because the lines are reversed in CEL.
const unsigned frameHeight = height / numFrames;
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(&celData[4 * static_cast<size_t>(frame)], static_cast<uint32_t>(celData.size()));
// Frame header: 5 16-bit offsets to 32-pixel height blocks.
const size_t frameHeaderPos = celData.size();
if (generateFrameHeaders) {
constexpr size_t FrameHeaderSize = 10;
celData.resize(celData.size() + FrameHeaderSize);
WriteLE16(&celData[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;
}
size_t line = frameHeight;
while (line-- != 0) {
AppendCelLine(&frameBuffer[line * width], width, celData);
if (generateFrameHeaders) {
switch (line) {
case 32:
WriteLE16(&celData[frameHeaderPos + 2], celData.size() - frameHeaderPos);
break;
case 64:
WriteLE16(&celData[frameHeaderPos + 4], celData.size() - frameHeaderPos);
break;
case 96:
WriteLE16(&celData[frameHeaderPos + 6], celData.size() - frameHeaderPos);
break;
case 128:
WriteLE16(&celData[frameHeaderPos + 8], celData.size() - frameHeaderPos);
break;
}
}
}
}
WriteLE32(&celData[4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(celData.size()));
SDL_RWclose(handle);
auto out = std::unique_ptr<byte[]>(new byte[celData.size()]);
memcpy(&out[0], celData.data(), celData.size());
return OwnedCelSpriteWithFrameHeight {
OwnedCelSprite { std::move(out), static_cast<uint16_t>(width) },
static_cast<unsigned>(frameHeight)
};
}
} // namespace devilution

20
Source/utils/pcx_to_cel.hpp

@ -0,0 +1,20 @@
#pragma once
#include <SDL.h>
#include "engine/cel_sprite.hpp"
#include "utils/stdcompat/optional.hpp"
namespace devilution {
/** @brief Loads a PCX file as CEL.
*
* Assumes that the PCX file does not have a palette.
*
* @param handle A non-null SDL_RWops handle. Closed by this function.
* @param numFrames The number of vertically stacked frames in the PCX file.
* @param generateFrameHeaders Whether to generate frame headers in the CEL sprite.
*/
std::optional<OwnedCelSpriteWithFrameHeight> LoadPcxAsCel(SDL_RWops *handle, unsigned numFrames, bool generateFrameHeaders);
} // namespace devilution
Loading…
Cancel
Save