/** * @file cel_render.cpp * * CEL rendering. */ #include "engine/render/cel_render.hpp" #include #include #include "engine/cel_header.hpp" #include "engine/palette.h" #include "engine/render/common_impl.h" #include "engine/render/scrollrt.h" #include "engine/trn.hpp" #include "options.h" #include "utils/attributes.h" namespace devilution { namespace { constexpr bool IsCelTransparent(uint8_t control) { constexpr uint8_t CelTransparentMin = 0x80; return control >= CelTransparentMin; } constexpr uint8_t GetCelTransparentWidth(uint8_t control) { return -static_cast(control); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT BlitCommand CelGetBlitCommand(const uint8_t *src) { const uint8_t control = *src++; if (IsCelTransparent(control)) return BlitCommand { BlitType::Transparent, src, GetCelTransparentWidth(control), 0 }; return BlitCommand { BlitType::Pixels, src + control, control, 0 }; } template uint8_t *RenderCelOutlinePixelsCheckFirstColumn( uint8_t *dst, int dstPitch, int dstX, const uint8_t *src, uint8_t width, uint8_t color) { if (dstX == -1) { RenderOutlineForPixel( dst++, dstPitch, *src++, color); --width; } if (width > 0) { RenderOutlineForPixel(dst++, dstPitch, *src++, color); --width; } if (width > 0) { RenderOutlineForPixels(dst, dstPitch, width, src, color); dst += width; } return dst; } template uint8_t *RenderCelOutlinePixelsCheckLastColumn( uint8_t *dst, int dstPitch, int dstX, int dstW, const uint8_t *src, uint8_t width, uint8_t color) { const bool lastPixel = dstX < dstW && width >= 1; const bool oobPixel = dstX + width > dstW; const int numSpecialPixels = (lastPixel ? 1 : 0) + (oobPixel ? 1 : 0); if (width > numSpecialPixels) { width -= numSpecialPixels; RenderOutlineForPixels(dst, dstPitch, width, src, color); src += width; dst += width; } if (lastPixel) RenderOutlineForPixel(dst++, dstPitch, *src++, color); if (oobPixel) RenderOutlineForPixel(dst++, dstPitch, *src++, color); return dst; } template uint8_t *RenderCelOutlinePixels( uint8_t *dst, int dstPitch, int dstX, int dstW, const uint8_t *src, uint8_t width, uint8_t color) { if (CheckFirstColumn && dstX <= 0) { return RenderCelOutlinePixelsCheckFirstColumn( dst, dstPitch, dstX, src, width, color); } if (CheckLastColumn && dstX + width >= dstW) { return RenderCelOutlinePixelsCheckLastColumn( dst, dstPitch, dstX, dstW, src, width, color); } RenderOutlineForPixels(dst, dstPitch, width, src, color); return dst + width; } template const uint8_t *RenderCelOutlineRowClipped( // NOLINT(readability-function-cognitive-complexity,misc-no-recursion) const Surface &out, Point position, const uint8_t *src, ClipX clipX, uint8_t color) { int_fast16_t remainingWidth = clipX.width; uint8_t v; auto *dst = &out[position]; const auto dstPitch = out.pitch(); const auto dstW = out.w(); if (ClipWidth) { auto remainingLeftClip = clipX.left; while (remainingLeftClip > 0) { v = static_cast(*src++); if (!IsCelTransparent(v)) { if (v > remainingLeftClip) { RenderCelOutlinePixels( dst, dstPitch, position.x, dstW, src, v - remainingLeftClip, color); } src += v; } else { v = GetCelTransparentWidth(v); } remainingLeftClip -= v; } dst -= static_cast(remainingLeftClip); position.x -= static_cast(remainingLeftClip); remainingWidth += remainingLeftClip; } while (remainingWidth > 0) { v = static_cast(*src++); if (!IsCelTransparent(v)) { dst = RenderCelOutlinePixels( dst, dstPitch, position.x, dstW, src, std::min(remainingWidth, static_cast(v)), color); src += v; } else { v = GetCelTransparentWidth(v); dst += v; } remainingWidth -= v; position.x += v; } src = SkipRestOfLine(src, clipX.right + remainingWidth); return src; } template void RenderCelOutlineClippedY(const Surface &out, Point position, RenderSrcBackwards src, uint8_t color) // NOLINT(readability-function-cognitive-complexity) { // Skip the bottom clipped lines. const auto dstHeight = out.h(); SkipLinesForRenderBackwards(position, src, dstHeight); if (src.begin == src.end) return; const ClipX clipX = { 0, 0, static_cast(src.width) }; if (position.y == dstHeight) { // After-bottom line - can only draw north. src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); --position.y; } if (src.begin == src.end) return; if (position.y + 1 == dstHeight) { // Bottom line - cannot draw south. src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); --position.y; } while (position.y > 0 && src.begin != src.end) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); --position.y; } if (src.begin == src.end) return; if (position.y == 0) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); --position.y; } if (src.begin == src.end) return; if (position.y == -1) { // Special case: the top of the sprite is 1px below the last line, render just the outline above. RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); } } template void RenderCelOutlineClippedXY(const Surface &out, Point position, RenderSrcBackwards src, uint8_t color) // NOLINT(readability-function-cognitive-complexity) { // Skip the bottom clipped lines. const auto dstHeight = out.h(); SkipLinesForRenderBackwards(position, src, dstHeight); if (src.begin == src.end) return; ClipX clipX = CalculateClipX(position.x, src.width, out); if (clipX.width < 0) return; if (clipX.left > 0) { --clipX.left, ++clipX.width; } else if (clipX.right > 0) { --clipX.right, ++clipX.width; } position.x += static_cast(clipX.left); if (position.y == dstHeight) { // After-bottom line - can only draw north. if (position.x <= 0) { src.begin = RenderCelOutlineRowClipped(out, position, src.begin, clipX, color); } else if (position.x + clipX.width >= out.w()) { src.begin = RenderCelOutlineRowClipped(out, position, src.begin, clipX, color); } else { src.begin = RenderCelOutlineRowClipped(out, position, src.begin, clipX, color); } --position.y; } if (src.begin == src.end) return; if (position.y + 1 == dstHeight) { // Bottom line - cannot draw south. if (position.x <= 0) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); } else if (position.x + clipX.width >= out.w()) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); } else { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); } --position.y; } if (position.x <= 0) { while (position.y > 0 && src.begin != src.end) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); --position.y; } } else if (position.x + clipX.width >= out.w()) { while (position.y > 0 && src.begin != src.end) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); --position.y; } } else { while (position.y > 0 && src.begin != src.end) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); --position.y; } } if (src.begin == src.end) return; if (position.y == 0) { if (position.x <= 0) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); } else if (position.x + clipX.width >= out.w()) { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); } else { src.begin = RenderCelOutlineRowClipped( out, position, src.begin, clipX, color); } --position.y; } if (src.begin == src.end) return; if (position.y == -1) { // After-bottom line - can only draw south. if (position.x <= 0) { src.begin = RenderCelOutlineRowClipped(out, position, src.begin, clipX, color); } else if (position.x + clipX.width >= out.w()) { src.begin = RenderCelOutlineRowClipped(out, position, src.begin, clipX, color); } else { src.begin = RenderCelOutlineRowClipped(out, position, src.begin, clipX, color); } } } template void RenderCelOutline(const Surface &out, Point position, const uint8_t *src, std::size_t srcSize, std::size_t srcWidth, uint8_t color) { RenderSrcBackwards srcForBackwards { src, src + srcSize, static_cast(srcWidth) }; if (position.x > 0 && position.x + static_cast(srcWidth) < static_cast(out.w())) { RenderCelOutlineClippedY(out, position, srcForBackwards, color); } else { RenderCelOutlineClippedXY(out, position, srcForBackwards, color); } } /** * @brief Blit CEL sprite to the given buffer, checks for drawing outside the buffer. * @param out Target buffer * @param position Target buffer coordinate * @param pRLEBytes CEL pixel stream (run-length encoded) * @param nDataSize Size of CEL in bytes * @param nWidth Width of sprite in pixels */ void CelBlitSafeTo(const Surface &out, Point position, const byte *pRLEBytes, int nDataSize, int nWidth) { assert(pRLEBytes != nullptr); DoRenderBackwards( out, position, reinterpret_cast(pRLEBytes), nDataSize, nWidth, BlitDirect {}); } /** * @brief Same as CelBlitLightSafe, with blended transparency applied * @param out Target buffer * @param position Target buffer coordinate * @param pRLEBytes CEL pixel stream (run-length encoded) * @param nDataSize Size of CEL in bytes * @param nWidth Width of sprite in pixels * @param tbl Palette translation table */ void CelBlitLightBlendedSafeTo(const Surface &out, Point position, const byte *pRLEBytes, int nDataSize, int nWidth, const uint8_t *tbl) { assert(pRLEBytes != nullptr); if (tbl == nullptr) tbl = &LightTables[LightTableIndex * 256]; DoRenderBackwards( out, position, reinterpret_cast(pRLEBytes), nDataSize, nWidth, BlitBlendedWithMap { tbl }); } void RenderCelWithLightTable(const Surface &out, Point position, const byte *src, std::size_t srcSize, std::size_t srcWidth, const uint8_t *tbl) { DoRenderBackwards( out, position, reinterpret_cast(src), srcSize, srcWidth, BlitWithMap { tbl }); } /** * @brief Blit CEL sprite, and apply lighting, to the given buffer, checks for drawing outside the buffer * @param out Target buffer * @param position Target buffer coordinate * @param pRLEBytes CEL pixel stream (run-length encoded) * @param nDataSize Size of CEL in bytes * @param nWidth Width of sprite in pixels * @param tbl Palette translation table */ void CelBlitLightSafeTo(const Surface &out, Point position, const byte *pRLEBytes, int nDataSize, int nWidth, uint8_t *tbl) { assert(pRLEBytes != nullptr); if (tbl == nullptr) tbl = &LightTables[LightTableIndex * 256]; RenderCelWithLightTable(out, position, pRLEBytes, nDataSize, nWidth, tbl); } } // namespace void CelDrawTo(const Surface &out, Point position, CelSprite cel, int frame) { int nDataSize; const auto *pRLEBytes = CelGetFrame(cel.Data(), frame, &nDataSize); CelBlitSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame)); } void CelClippedDrawTo(const Surface &out, Point position, CelSprite cel, int frame) { int nDataSize; const auto *pRLEBytes = CelGetFrameClipped(cel.Data(), frame, &nDataSize); CelBlitSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame)); } void CelDrawLightTo(const Surface &out, Point position, CelSprite cel, int frame, uint8_t *tbl) { int nDataSize; const auto *pRLEBytes = CelGetFrame(cel.Data(), frame, &nDataSize); if (LightTableIndex != 0 || tbl != nullptr) CelBlitLightSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame), tbl); else CelBlitSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame)); } void CelClippedDrawLightTo(const Surface &out, Point position, CelSprite cel, int frame) { int nDataSize; const auto *pRLEBytes = CelGetFrameClipped(cel.Data(), frame, &nDataSize); if (LightTableIndex != 0) CelBlitLightSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame), nullptr); else CelBlitSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame)); } void CelDrawLightRedTo(const Surface &out, Point position, CelSprite cel, int frame) { int nDataSize; const auto *pRLEBytes = CelGetFrameClipped(cel.Data(), frame, &nDataSize); RenderCelWithLightTable(out, position, pRLEBytes, nDataSize, cel.Width(frame), GetInfravisionTRN()); } void CelDrawItem(const Item &item, const Surface &out, Point position, CelSprite cel, int frame) { bool usable = item._iStatFlag; if (!usable) { CelDrawLightRedTo(out, position, cel, frame); } else { CelClippedDrawTo(out, position, cel, frame); } } void CelClippedBlitLightTransTo(const Surface &out, Point position, CelSprite cel, int frame) { int nDataSize; const byte *pRLEBytes = CelGetFrameClipped(cel.Data(), frame, &nDataSize); if (cel_transparency_active) { CelBlitLightBlendedSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame), nullptr); } else if (LightTableIndex != 0) CelBlitLightSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame), nullptr); else CelBlitSafeTo(out, position, pRLEBytes, nDataSize, cel.Width(frame)); } void CelDrawUnsafeTo(const Surface &out, Point position, CelSprite cel, int frame) { int srcSize; const auto *srcBegin = reinterpret_cast(CelGetFrame(cel.Data(), frame, &srcSize)); RenderSrcBackwards src { srcBegin, srcBegin + srcSize, cel.Width(frame) }; DoRenderBackwardsClipY( out, position, src, BlitDirect {}); } void CelBlitOutlineTo(const Surface &out, uint8_t col, Point position, CelSprite cel, int frame, bool skipColorIndexZero) { int nDataSize; const uint8_t *src = reinterpret_cast(CelGetFrameClipped(cel.Data(), frame, &nDataSize)); if (skipColorIndexZero) RenderCelOutline(out, position, src, nDataSize, cel.Width(frame), col); else RenderCelOutline(out, position, src, nDataSize, cel.Width(frame), col); } std::pair MeasureSolidHorizontalBounds(CelSprite cel, int frame) { int nDataSize; const byte *src = CelGetFrameClipped(cel.Data(), frame, &nDataSize); const auto *end = &src[nDataSize]; const int celWidth = cel.Width(frame); int xBegin = celWidth; int xEnd = 0; while (src < end) { int xCur = 0; while (xCur < celWidth) { const auto val = static_cast(*src++); if (IsCelTransparent(val)) { const int width = GetCelTransparentWidth(val); xCur += width; } else { xBegin = std::min(xBegin, xCur); xCur += val; xEnd = std::max(xEnd, xCur); src += val; } } if (xBegin == 0 && xEnd == celWidth) break; } 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