You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

581 lines
21 KiB

/**
* @file cl2_render.cpp
*
* CL2 rendering.
*/
#include "cl2_render.hpp"
#include <algorithm>
#include "engine/cel_header.hpp"
#include "engine/render/common_impl.h"
#include "engine/render/scrollrt.h"
#include "utils/attributes.h"
namespace devilution {
namespace {
/**
* CL2 is similar to CEL, with the following differences:
*
* 1. Transparent runs can cross line boundaries.
* 2. Control bytes are different, and the [0x80, 0xBE] control byte range
* 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 };
}
/**
* @brief Blit CL2 sprite to the given buffer
* @param out Target buffer
* @param sx Target buffer coordinate
* @param sy Target buffer coordinate
* @param pRLEBytes CL2 pixel stream (run-length encoded)
* @param nDataSize Size of CL2 in bytes
* @param nWidth Width of sprite
*/
void Cl2Blit(const Surface &out, Point position, const byte *pRLEBytes, int nDataSize, int nWidth)
{
DoRenderBackwards<Cl2GetBlitCommand>(
out, position, reinterpret_cast<const uint8_t *>(pRLEBytes), nDataSize, nWidth, BlitDirect {});
}
/**
* @brief Blit CL2 sprite, and apply lighting, to the given buffer
* @param out Target buffer
* @param sx Target buffer coordinate
* @param sy Target buffer coordinate
* @param pRLEBytes CL2 pixel stream (run-length encoded)
* @param nDataSize Size of CL2 in bytes
* @param nWidth With of CL2 sprite
* @param pTable Light color table
*/
void Cl2BlitTRN(const Surface &out, Point position, const byte *pRLEBytes, int nDataSize, int nWidth, uint8_t *pTable)
{
DoRenderBackwards<Cl2GetBlitCommand>(
out, position, reinterpret_cast<const uint8_t *>(pRLEBytes), nDataSize, nWidth, BlitWithMap { pTable });
}
void Cl2BlitBlendedTRN(const Surface &out, Point position, const byte *pRLEBytes, int nDataSize, int nWidth, uint8_t *pTable)
{
DoRenderBackwards<Cl2GetBlitCommand>(
out, position, reinterpret_cast<const uint8_t *>(pRLEBytes), nDataSize, nWidth, BlitBlendedWithMap { pTable });
}
template <bool Fill, bool North, bool West, bool South, bool East, bool SkipColorIndexZero>
uint8_t *RenderCl2OutlinePixelsCheckFirstColumn(
uint8_t *dst, int dstPitch, int dstX,
const uint8_t *src, uint8_t width, uint8_t color)
{
if (dstX == -1) {
if (Fill) {
RenderOutlineForPixel</*North=*/false, /*West=*/false, /*South=*/false, East>(
dst++, dstPitch, color);
} else {
RenderOutlineForPixel</*North=*/false, /*West=*/false, /*South=*/false, East, SkipColorIndexZero>(
dst++, dstPitch, *src++, color);
}
--width;
}
if (width > 0) {
if (Fill) {
RenderOutlineForPixel<North, /*West=*/false, South, East>(dst++, dstPitch, color);
} else {
RenderOutlineForPixel<North, /*West=*/false, South, East, SkipColorIndexZero>(dst++, dstPitch, *src++, color);
}
--width;
}
if (width > 0) {
if (Fill) {
RenderOutlineForPixels<North, West, South, East>(dst, dstPitch, width, color);
} else {
RenderOutlineForPixels<North, West, South, East, SkipColorIndexZero>(dst, dstPitch, width, src, color);
}
dst += width;
}
return dst;
}
template <bool Fill, bool North, bool West, bool South, bool East, bool SkipColorIndexZero>
uint8_t *RenderCl2OutlinePixelsCheckLastColumn(
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;
if (Fill) {
RenderOutlineForPixels<North, West, South, East>(dst, dstPitch, width, color);
} else {
RenderOutlineForPixels<North, West, South, East, SkipColorIndexZero>(dst, dstPitch, width, src, color);
src += width;
}
dst += width;
}
if (lastPixel) {
if (Fill) {
RenderOutlineForPixel<North, West, South, /*East=*/false>(dst++, dstPitch, color);
} else {
RenderOutlineForPixel<North, West, South, /*East=*/false, SkipColorIndexZero>(dst++, dstPitch, *src++, color);
}
}
if (oobPixel) {
if (Fill) {
RenderOutlineForPixel</*North=*/false, West, /*South=*/false, /*East=*/false>(dst++, dstPitch, color);
} else {
RenderOutlineForPixel</*North=*/false, West, /*South=*/false, /*East=*/false, SkipColorIndexZero>(dst++, dstPitch, *src++, color);
}
}
return dst;
}
template <bool Fill, bool North, bool West, bool South, bool East, bool SkipColorIndexZero, bool CheckFirstColumn, bool CheckLastColumn>
uint8_t *RenderCl2OutlinePixels(
uint8_t *dst, int dstPitch, int dstX, int dstW,
const uint8_t *src, uint8_t width, uint8_t color)
{
if (SkipColorIndexZero && Fill && *src == 0)
return dst + width;
if (CheckFirstColumn && dstX <= 0) {
return RenderCl2OutlinePixelsCheckFirstColumn<Fill, North, West, South, East, SkipColorIndexZero>(
dst, dstPitch, dstX, src, width, color);
}
if (CheckLastColumn && dstX + width >= dstW) {
return RenderCl2OutlinePixelsCheckLastColumn<Fill, North, West, South, East, SkipColorIndexZero>(
dst, dstPitch, dstX, dstW, src, width, color);
}
if (Fill) {
RenderOutlineForPixels<North, West, South, East>(dst, dstPitch, width, color);
} else {
RenderOutlineForPixels<North, West, South, East, SkipColorIndexZero>(dst, dstPitch, width, src, color);
}
return dst + width;
}
template <bool North, bool West, bool South, bool East, bool SkipColorIndexZero,
bool ClipWidth = false, bool CheckFirstColumn = false, bool CheckLastColumn = false>
const uint8_t *RenderCl2OutlineRowClipped( // NOLINT(readability-function-cognitive-complexity)
const Surface &out, Point position, const uint8_t *src, std::size_t srcWidth,
ClipX clipX, uint8_t color, SkipSize &skipSize)
{
int_fast16_t remainingWidth = clipX.width;
uint8_t v;
auto *dst = &out[position];
const auto dstPitch = out.pitch();
const auto renderPixels = [&](bool fill, uint8_t w) {
if (fill) {
dst = RenderCl2OutlinePixels</*Fill=*/true, North, West, South, East, SkipColorIndexZero, CheckFirstColumn, CheckLastColumn>(
dst, dstPitch, position.x, out.w(), src, w, color);
++src;
} else {
dst = RenderCl2OutlinePixels</*Fill=*/false, North, West, South, East, SkipColorIndexZero, CheckFirstColumn, CheckLastColumn>(
dst, dstPitch, position.x, out.w(), src, w, color);
src += v;
}
};
if (ClipWidth) {
auto remainingLeftClip = clipX.left - skipSize.xOffset;
if (skipSize.xOffset > clipX.left) {
position.x += static_cast<int>(skipSize.xOffset - clipX.left);
dst += skipSize.xOffset - clipX.left;
}
while (remainingLeftClip > 0) {
v = static_cast<uint8_t>(*src++);
if (IsCl2Opaque(v)) {
const bool fill = IsCl2OpaqueFill(v);
v = fill ? GetCl2OpaqueFillWidth(v) : GetCl2OpaquePixelsWidth(v);
if (v > remainingLeftClip) {
const uint8_t overshoot = v - remainingLeftClip;
renderPixels(fill, overshoot);
position.x += overshoot;
} else {
src += fill ? 1 : v;
}
} else {
if (v > remainingLeftClip) {
const uint8_t overshoot = v - remainingLeftClip;
dst += overshoot;
position.x += overshoot;
}
}
remainingLeftClip -= v;
}
remainingWidth += remainingLeftClip;
} else {
position.x += static_cast<int>(skipSize.xOffset);
dst += skipSize.xOffset;
remainingWidth -= skipSize.xOffset;
}
while (remainingWidth > 0) {
v = static_cast<uint8_t>(*src++);
if (IsCl2Opaque(v)) {
const bool fill = IsCl2OpaqueFill(v);
v = fill ? GetCl2OpaqueFillWidth(v) : GetCl2OpaquePixelsWidth(v);
renderPixels(fill, ClipWidth ? std::min(remainingWidth, static_cast<int_fast16_t>(v)) : v);
} else {
dst += v;
}
remainingWidth -= v;
position.x += v;
}
if (ClipWidth) {
remainingWidth += clipX.right;
if (remainingWidth > 0) {
skipSize.xOffset = static_cast<int_fast16_t>(srcWidth) - remainingWidth;
src = SkipRestOfLineWithOverrun<Cl2GetBlitCommand>(src, static_cast<int_fast16_t>(srcWidth), skipSize);
if (skipSize.wholeLines > 1)
dst -= dstPitch * (skipSize.wholeLines - 1);
remainingWidth = -skipSize.xOffset;
}
}
if (remainingWidth < 0) {
skipSize = GetSkipSize(-remainingWidth, static_cast<int_fast16_t>(srcWidth));
++skipSize.wholeLines;
} else {
skipSize.xOffset = 0;
skipSize.wholeLines = 1;
}
return src;
}
template <bool SkipColorIndexZero>
void RenderCl2OutlineClippedY(const Surface &out, Point position, RenderSrcBackwards src, // NOLINT(readability-function-cognitive-complexity)
uint8_t color)
{
// Skip the bottom clipped lines.
const int dstHeight = out.h();
SkipSize skipSize = { 0, SkipLinesForRenderBackwardsWithOverrun<Cl2GetBlitCommand>(position, src, dstHeight) };
if (src.begin == src.end)
return;
const ClipX clipX = { 0, 0, static_cast<decltype(ClipX {}.width)>(src.width) };
if (position.y == dstHeight) {
// After-bottom line - can only draw north.
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/false, /*South=*/false, /*East=*/false, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
if (src.begin == src.end)
return;
if (position.y + 1 == dstHeight) {
// Bottom line - cannot draw south.
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/false, /*East=*/true, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
while (position.y > 0 && src.begin != src.end) {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
if (src.begin == src.end)
return;
if (position.y == 0) {
src.begin = RenderCl2OutlineRowClipped</*North=*/false, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
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.
RenderCl2OutlineRowClipped</*North=*/false, /*West=*/false, /*South=*/true, /*East=*/false, SkipColorIndexZero>(
out, position, src.begin, src.width, clipX, color, skipSize);
}
}
template <bool SkipColorIndexZero>
void RenderCl2OutlineClippedXY(const Surface &out, Point position, RenderSrcBackwards src, // NOLINT(readability-function-cognitive-complexity)
uint8_t color)
{
// Skip the bottom clipped lines.
const int dstHeight = out.h();
SkipSize skipSize = { 0, SkipLinesForRenderBackwardsWithOverrun<Cl2GetBlitCommand>(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<int>(clipX.left);
if (position.y == dstHeight) {
// After-bottom line - can only draw north.
if (position.x <= 0) {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/false, /*South=*/false, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(out, position, src.begin, src.width, clipX, color, skipSize);
} else if (position.x + clipX.width >= out.w()) {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/false, /*South=*/false, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(out, position, src.begin, src.width, clipX, color, skipSize);
} else {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/false, /*South=*/false, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true>(out, position, src.begin, src.width, clipX, color, skipSize);
}
position.y -= static_cast<int>(skipSize.wholeLines);
}
if (src.begin == src.end)
return;
if (position.y + 1 == dstHeight) {
// Bottom line - cannot draw south.
if (position.x <= 0) {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/false, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(
out, position, src.begin, src.width, clipX, color, skipSize);
} else if (position.x + clipX.width >= out.w()) {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/false, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
} else {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/false, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
}
position.y -= static_cast<int>(skipSize.wholeLines);
}
if (position.x <= 0) {
while (position.y > 0 && src.begin != src.end) {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
} else if (position.x + clipX.width >= out.w()) {
while (position.y > 0 && src.begin != src.end) {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
} else {
while (position.y > 0 && src.begin != src.end) {
src.begin = RenderCl2OutlineRowClipped</*North=*/true, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
position.y -= static_cast<int>(skipSize.wholeLines);
}
}
if (src.begin == src.end)
return;
if (position.y == 0) {
if (position.x <= 0) {
src.begin = RenderCl2OutlineRowClipped</*North=*/false, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(
out, position, src.begin, src.width, clipX, color, skipSize);
} else if (position.x + clipX.width >= out.w()) {
src.begin = RenderCl2OutlineRowClipped</*North=*/false, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
} else {
src.begin = RenderCl2OutlineRowClipped</*North=*/false, /*West=*/true, /*South=*/true, /*East=*/true, SkipColorIndexZero,
/*ClipWidth=*/true>(
out, position, src.begin, src.width, clipX, color, skipSize);
}
position.y -= static_cast<int>(skipSize.wholeLines);
}
if (src.begin == src.end)
return;
if (position.y == -1) {
// Before-top line - can only draw south.
if (position.x <= 0) {
src.begin = RenderCl2OutlineRowClipped</*North=*/false, /*West=*/false, /*South=*/true, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/true, /*CheckLastColumn=*/false>(out, position, src.begin, src.width, clipX, color, skipSize);
} else if (position.x + clipX.width >= out.w()) {
src.begin = RenderCl2OutlineRowClipped</*North=*/false, /*West=*/false, /*South=*/true, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true, /*CheckFirstColumn=*/false, /*CheckLastColumn=*/true>(out, position, src.begin, src.width, clipX, color, skipSize);
} else {
src.begin = RenderCl2OutlineRowClipped</*North=*/false, /*West=*/false, /*South=*/true, /*East=*/false, SkipColorIndexZero,
/*ClipWidth=*/true>(out, position, src.begin, src.width, clipX, color, skipSize);
}
}
}
template <bool SkipColorIndexZero>
void RenderCl2Outline(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<uint_fast16_t>(srcWidth) };
if (position.x > 0 && position.x + static_cast<int>(srcWidth) < static_cast<int>(out.w())) {
RenderCl2OutlineClippedY<SkipColorIndexZero>(out, position, srcForBackwards, color);
} else {
RenderCl2OutlineClippedXY<SkipColorIndexZero>(out, position, srcForBackwards, color);
}
}
} // namespace
void Cl2ApplyTrans(byte *p, const std::array<uint8_t, 256> &ttbl, int numFrames)
{
assert(p != nullptr);
for (int i = 0; i < numFrames; ++i) {
constexpr int FrameHeaderSize = 10;
int nDataSize;
byte *dst = CelGetFrame(p, i, &nDataSize) + FrameHeaderSize;
nDataSize -= FrameHeaderSize;
while (nDataSize > 0) {
auto v = static_cast<uint8_t>(*dst++);
nDataSize--;
assert(nDataSize >= 0);
if (!IsCl2Opaque(v))
continue;
if (IsCl2OpaqueFill(v)) {
nDataSize--;
assert(nDataSize >= 0);
*dst = static_cast<byte>(ttbl[static_cast<uint8_t>(*dst)]);
dst++;
} else {
v = GetCl2OpaquePixelsWidth(v);
nDataSize -= v;
assert(nDataSize >= 0);
while (v-- > 0) {
*dst = static_cast<byte>(ttbl[static_cast<uint8_t>(*dst)]);
dst++;
}
}
}
}
}
std::pair<int, int> Cl2MeasureSolidHorizontalBounds(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;
int xCur = 0;
while (src < end) {
while (xCur < celWidth) {
auto val = static_cast<uint8_t>(*src++);
if (!IsCl2Opaque(val)) {
xCur += val;
continue;
}
if (IsCl2OpaqueFill(val)) {
val = GetCl2OpaqueFillWidth(val);
++src;
} else {
val = GetCl2OpaquePixelsWidth(val);
src += val;
}
xBegin = std::min(xBegin, xCur);
xCur += val;
xEnd = std::max(xEnd, xCur);
}
while (xCur >= celWidth)
xCur -= celWidth;
if (xBegin == 0 && xEnd == celWidth)
break;
}
return { xBegin, xEnd };
}
void Cl2Draw(const Surface &out, Point position, CelSprite cel, int frame)
{
assert(frame >= 0);
int nDataSize;
const byte *pRLEBytes = CelGetFrameClipped(cel.Data(), frame, &nDataSize);
Cl2Blit(out, position, pRLEBytes, nDataSize, cel.Width(frame));
}
void Cl2DrawOutline(const Surface &out, uint8_t col, Point position, CelSprite cel, int frame)
{
assert(frame >= 0);
int nDataSize;
const byte *src = CelGetFrameClipped(cel.Data(), frame, &nDataSize);
RenderCl2Outline</*SkipColorIndexZero=*/true>(out, position, reinterpret_cast<const uint8_t *>(src), nDataSize, cel.Width(frame), col);
}
void Cl2DrawOutlineSkipColorZero(const Surface &out, uint8_t col, Point position, CelSprite cel, int frame)
{
assert(frame >= 0);
int nDataSize;
const byte *src = CelGetFrameClipped(cel.Data(), frame, &nDataSize);
RenderCl2Outline</*SkipColorIndexZero=*/true>(out, position, reinterpret_cast<const uint8_t *>(src), nDataSize, cel.Width(frame), col);
}
void Cl2DrawTRN(const Surface &out, Point position, CelSprite cel, int frame, uint8_t *trn)
{
assert(frame >= 0);
int nDataSize;
const byte *pRLEBytes = CelGetFrameClipped(cel.Data(), frame, &nDataSize);
Cl2BlitTRN(out, position, pRLEBytes, nDataSize, cel.Width(frame), trn);
}
void Cl2DrawBlendedTRN(const Surface &out, Point position, CelSprite cel, int frame, uint8_t *trn)
{
assert(frame >= 0);
int nDataSize;
const byte *pRLEBytes = CelGetFrameClipped(cel.Data(), frame, &nDataSize);
Cl2BlitBlendedTRN(out, position, pRLEBytes, nDataSize, cel.Width(frame), trn);
}
} // namespace devilution