diff --git a/CMake/Definitions.cmake b/CMake/Definitions.cmake index 5c48a83a7..b3584929f 100644 --- a/CMake/Definitions.cmake +++ b/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}) diff --git a/CMakeLists.txt b/CMakeLists.txt index 05d623186..39f311ddc 100644 --- a/CMakeLists.txt +++ b/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() diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 46e40a4d2..73c628293 100644 --- a/Source/CMakeLists.txt +++ b/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 diff --git a/Source/engine/cel_sprite.hpp b/Source/engine/cel_sprite.hpp index bcbbd52ce..d9d8d8612 100644 --- a/Source/engine/cel_sprite.hpp +++ b/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 data_; }; @@ -89,4 +94,9 @@ inline CelSprite::CelSprite(const OwnedCelSprite &owned) { } +struct OwnedCelSpriteWithFrameHeight { + OwnedCelSprite sprite; + unsigned frameHeight; +}; + } // namespace devilution diff --git a/Source/engine/render/cel_render.cpp b/Source/engine/render/cel_render.cpp index 4b9244ac7..e202804d2 100644 --- a/Source/engine/render/cel_render.cpp +++ b/Source/engine/render/cel_render.cpp @@ -708,4 +708,35 @@ std::pair MeasureSolidHorizontalBounds(CelSprite cel, int frame) return { xBegin, xEnd }; } +void CelApplyTrans(byte *p, const std::array &translation) +{ + assert(p != nullptr); + const uint32_t numFrames = LoadLE32(p); + const byte *frameOffsets = p + 4; + p += 4 * (2 + static_cast(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(i) + 1)]); + + const byte *end = p + (frameEnd - frameBegin); + const bool frameHasHeader = static_cast(*p) == 0; + if (frameHasHeader) { + constexpr uint32_t FrameHeaderSize = 5 * 2; + p += FrameHeaderSize; + } + while (p != end) { + const auto val = static_cast(*p++); + if (IsCelTransparent(val)) { + continue; + } + for (unsigned i = 0; i < val; ++i) { + const auto color = static_cast(*p); + *p++ = static_cast(translation[color]); + } + } + } +} + } // namespace devilution diff --git a/Source/engine/render/cel_render.hpp b/Source/engine/render/cel_render.hpp index c7fe9da1a..ad580432b 100644 --- a/Source/engine/render/cel_render.hpp +++ b/Source/engine/render/cel_render.hpp @@ -20,6 +20,14 @@ namespace devilution { */ std::pair 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 &translation); + /** * @brief Blit CEL sprite to the back buffer at the given coordinates * @param out Target buffer diff --git a/Source/engine/render/text_render.cpp b/Source/engine/render/text_render.cpp index 01323229e..1f5b741bc 100644 --- a/Source/engine/render/text_render.cpp +++ b/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> Fonts; +#else +using Font = Art; std::unordered_map Fonts; +#endif std::unordered_map> FontKerns; std::array FontSizes = { 12, 24, 30, 42, 46, 22 }; std::array CJKWidth = { 17, 24, 28, 41, 47, 16 }; @@ -162,9 +170,61 @@ std::array *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 &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 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(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 *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 *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), '|'); } } diff --git a/Source/utils/pcx_to_cel.cpp b/Source/utils/pcx_to_cel.cpp new file mode 100644 index 000000000..4802a9b00 --- /dev/null +++ b/Source/utils/pcx_to_cel.cpp @@ -0,0 +1,175 @@ +#include "utils/pcx_to_cel.hpp" + +#include +#include +#include +#include +#include + +#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 &out) +{ + out.push_back(0xFF - (width - 1)); +} + +void AppendCelSolidRun(const uint8_t *src, unsigned width, std::vector &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 &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 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(-1)) { + SDL_RWclose(handle); + return std::nullopt; + } + + pixelDataSize -= PcxHeaderSize; + + std::unique_ptr 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 celData(4 * (2 + static_cast(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(new uint8_t[static_cast(frameHeight) * width]); + + const unsigned srcSkip = width % 2; + uint8_t *dataPtr = fileBuffer.get(); + for (unsigned frame = 1; frame <= numFrames; ++frame) { + WriteLE32(&celData[4 * static_cast(frame)], static_cast(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(j) * width]; + for (unsigned x = 0; x < static_cast(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(numFrames))], static_cast(celData.size())); + + SDL_RWclose(handle); + + auto out = std::unique_ptr(new byte[celData.size()]); + memcpy(&out[0], celData.data(), celData.size()); + return OwnedCelSpriteWithFrameHeight { + OwnedCelSprite { std::move(out), static_cast(width) }, + static_cast(frameHeight) + }; +} + +} // namespace devilution diff --git a/Source/utils/pcx_to_cel.hpp b/Source/utils/pcx_to_cel.hpp new file mode 100644 index 000000000..11e420032 --- /dev/null +++ b/Source/utils/pcx_to_cel.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#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 LoadPcxAsCel(SDL_RWops *handle, unsigned numFrames, bool generateFrameHeaders); + +} // namespace devilution