|
|
|
|
/**
|
|
|
|
|
* @file clx_render.cpp
|
|
|
|
|
*
|
|
|
|
|
* CL2 rendering.
|
|
|
|
|
*/
|
|
|
|
|
#include "engine/render/clx_render.hpp"
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <utility>
|
|
|
|
|
|
|
|
|
|
#include "engine/point.hpp"
|
|
|
|
|
#include "engine/render/blit_impl.hpp"
|
|
|
|
|
#include "engine/surface.hpp"
|
|
|
|
|
#include "utils/attributes.h"
|
|
|
|
|
#include "utils/clx_decode.hpp"
|
|
|
|
|
#include "utils/static_vector.hpp"
|
|
|
|
|
|
|
|
|
|
#ifdef DEBUG_CLX
|
|
|
|
|
#include <fmt/format.h>
|
|
|
|
|
|
|
|
|
|
#include "utils/str_cat.hpp"
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
struct BlitCommandInfo {
|
|
|
|
|
const uint8_t *srcEnd;
|
|
|
|
|
unsigned length;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
BlitCommandInfo ClxBlitInfo(const uint8_t *src)
|
|
|
|
|
{
|
|
|
|
|
const uint8_t control = *src;
|
|
|
|
|
if (!IsClxOpaque(control))
|
|
|
|
|
return { src + 1, control };
|
|
|
|
|
if (IsClxOpaqueFill(control)) {
|
|
|
|
|
const uint8_t width = GetClxOpaqueFillWidth(control);
|
|
|
|
|
return { src + 2, width };
|
|
|
|
|
}
|
|
|
|
|
const uint8_t width = GetClxOpaquePixelsWidth(control);
|
|
|
|
|
return { src + 1 + width, width };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct ClipX {
|
|
|
|
|
int_fast16_t left;
|
|
|
|
|
int_fast16_t right;
|
|
|
|
|
int_fast16_t width;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT ClipX CalculateClipX(int_fast16_t x, std::size_t w, const Surface &out)
|
|
|
|
|
{
|
|
|
|
|
ClipX clip;
|
|
|
|
|
clip.left = static_cast<int_fast16_t>(x < 0 ? -x : 0);
|
|
|
|
|
clip.right = static_cast<int_fast16_t>(static_cast<int_fast16_t>(x + w) > out.w() ? x + w - out.w() : 0);
|
|
|
|
|
clip.width = static_cast<int_fast16_t>(w - clip.left - clip.right);
|
|
|
|
|
return clip;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Source data for rendering backwards: first line of input -> last line of output.
|
|
|
|
|
struct RenderSrc {
|
|
|
|
|
const uint8_t *begin;
|
|
|
|
|
const uint8_t *end;
|
|
|
|
|
uint_fast16_t width;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT const uint8_t *SkipRestOfLineWithOverrun(
|
|
|
|
|
const uint8_t *src, int_fast16_t srcWidth, SkipSize &skipSize)
|
|
|
|
|
{
|
|
|
|
|
int_fast16_t remainingWidth = srcWidth - skipSize.xOffset;
|
|
|
|
|
while (remainingWidth > 0) {
|
|
|
|
|
const auto [srcEnd, length] = ClxBlitInfo(src);
|
|
|
|
|
src = srcEnd;
|
|
|
|
|
remainingWidth -= length;
|
|
|
|
|
}
|
|
|
|
|
skipSize = GetSkipSize(remainingWidth, srcWidth);
|
|
|
|
|
return src;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the horizontal overrun.
|
|
|
|
|
DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT int_fast16_t SkipLinesForRenderBackwardsWithOverrun(
|
|
|
|
|
Point &position, RenderSrc &src, int_fast16_t dstHeight)
|
|
|
|
|
{
|
|
|
|
|
SkipSize skipSize { 0, 0 };
|
|
|
|
|
while (position.y >= dstHeight && src.begin != src.end) {
|
|
|
|
|
src.begin = SkipRestOfLineWithOverrun(
|
|
|
|
|
src.begin, static_cast<int_fast16_t>(src.width), skipSize);
|
|
|
|
|
position.y -= static_cast<int>(skipSize.wholeLines);
|
|
|
|
|
}
|
|
|
|
|
return skipSize.xOffset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename BlitFn>
|
|
|
|
|
void DoRenderBackwardsClipY(
|
|
|
|
|
const Surface &out, Point position, RenderSrc src, BlitFn &&blitFn)
|
|
|
|
|
{
|
|
|
|
|
// Skip the bottom clipped lines.
|
|
|
|
|
int_fast16_t xOffset = SkipLinesForRenderBackwardsWithOverrun(position, src, out.h());
|
|
|
|
|
if (src.begin >= src.end)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
auto *dst = &out[position];
|
|
|
|
|
const auto *dstBegin = out.begin();
|
|
|
|
|
const int dstPitch = out.pitch();
|
|
|
|
|
while (src.begin != src.end && dst >= dstBegin) {
|
|
|
|
|
auto remainingWidth = static_cast<int_fast16_t>(src.width) - xOffset;
|
|
|
|
|
dst += xOffset;
|
|
|
|
|
while (remainingWidth > 0) {
|
|
|
|
|
uint8_t v = *src.begin++;
|
|
|
|
|
if (IsClxOpaque(v)) {
|
|
|
|
|
if (IsClxOpaqueFill(v)) {
|
|
|
|
|
v = GetClxOpaqueFillWidth(v);
|
|
|
|
|
const uint8_t color = *src.begin++;
|
|
|
|
|
blitFn(v, color, dst);
|
|
|
|
|
} else {
|
|
|
|
|
v = GetClxOpaquePixelsWidth(v);
|
|
|
|
|
blitFn(v, dst, src.begin);
|
|
|
|
|
src.begin += v;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dst += v;
|
|
|
|
|
remainingWidth -= v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SkipSize skipSize = GetSkipSize(remainingWidth, static_cast<int_fast16_t>(src.width));
|
|
|
|
|
xOffset = skipSize.xOffset;
|
|
|
|
|
dst -= skipSize.wholeLines * dstPitch + src.width - remainingWidth;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename BlitFn>
|
|
|
|
|
void DoRenderBackwardsClipXY(
|
|
|
|
|
const Surface &out, Point position, RenderSrc src, ClipX clipX, BlitFn &&blitFn)
|
|
|
|
|
{
|
|
|
|
|
// Skip the bottom clipped lines.
|
|
|
|
|
int_fast16_t xOffset = SkipLinesForRenderBackwardsWithOverrun(position, src, out.h());
|
|
|
|
|
if (src.begin >= src.end)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
position.x += static_cast<int>(clipX.left);
|
|
|
|
|
auto *dst = &out[position];
|
|
|
|
|
const auto *dstBegin = out.begin();
|
|
|
|
|
const int dstPitch = out.pitch();
|
|
|
|
|
|
|
|
|
|
while (src.begin != src.end && dst >= dstBegin) {
|
|
|
|
|
// Skip initial src if clipping on the left.
|
|
|
|
|
// Handles overshoot, i.e. when the RLE segment goes into the unclipped area.
|
|
|
|
|
int_fast16_t remainingWidth = clipX.width;
|
|
|
|
|
int_fast16_t remainingLeftClip = clipX.left - xOffset;
|
|
|
|
|
if (remainingLeftClip < 0) {
|
|
|
|
|
dst += std::min<unsigned>(remainingWidth, -remainingLeftClip);
|
|
|
|
|
remainingWidth += remainingLeftClip;
|
|
|
|
|
}
|
|
|
|
|
while (remainingLeftClip > 0) {
|
|
|
|
|
auto [srcEnd, length] = ClxBlitInfo(src.begin);
|
|
|
|
|
if (static_cast<int_fast16_t>(length) > remainingLeftClip) {
|
|
|
|
|
const uint8_t control = *src.begin;
|
|
|
|
|
const auto overshoot = static_cast<int>(length - remainingLeftClip);
|
|
|
|
|
length = std::min<unsigned>(remainingWidth, overshoot);
|
|
|
|
|
if (IsClxOpaque(control)) {
|
|
|
|
|
if (IsClxOpaqueFill(control)) {
|
|
|
|
|
blitFn(length, src.begin[1], dst);
|
|
|
|
|
} else {
|
|
|
|
|
blitFn(length, dst, src.begin + 1 + remainingLeftClip);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
dst += length;
|
|
|
|
|
remainingWidth -= overshoot;
|
|
|
|
|
src.begin = srcEnd;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
src.begin = srcEnd;
|
|
|
|
|
remainingLeftClip -= length;
|
|
|
|
|
}
|
|
|
|
|
while (remainingWidth > 0) {
|
|
|
|
|
auto [srcEnd, length] = ClxBlitInfo(src.begin);
|
|
|
|
|
const uint8_t control = *src.begin;
|
|
|
|
|
const unsigned unclippedLength = length;
|
|
|
|
|
length = std::min<unsigned>(remainingWidth, length);
|
|
|
|
|
if (IsClxOpaque(control)) {
|
|
|
|
|
if (IsClxOpaqueFill(control)) {
|
|
|
|
|
blitFn(length, src.begin[1], dst);
|
|
|
|
|
} else {
|
|
|
|
|
blitFn(length, dst, src.begin + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
src.begin = srcEnd;
|
|
|
|
|
dst += length;
|
|
|
|
|
remainingWidth -= unclippedLength; // result can be negative
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// `remainingWidth` can be negative, in which case it is the amount of pixels
|
|
|
|
|
// that the source has overran the line.
|
|
|
|
|
remainingWidth += clipX.right;
|
|
|
|
|
SkipSize skipSize;
|
|
|
|
|
if (remainingWidth > 0) {
|
|
|
|
|
skipSize.xOffset = static_cast<int_fast16_t>(src.width) - remainingWidth;
|
|
|
|
|
src.begin = SkipRestOfLineWithOverrun(
|
|
|
|
|
src.begin, static_cast<int_fast16_t>(src.width), skipSize);
|
|
|
|
|
} else {
|
|
|
|
|
skipSize = GetSkipSize(remainingWidth, static_cast<int_fast16_t>(src.width));
|
|
|
|
|
}
|
|
|
|
|
xOffset = skipSize.xOffset;
|
|
|
|
|
dst -= dstPitch * skipSize.wholeLines + clipX.width;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <typename BlitFn>
|
|
|
|
|
void DoRenderBackwards(
|
|
|
|
|
const Surface &out, Point position, const uint8_t *src, size_t srcSize,
|
|
|
|
|
unsigned srcWidth, unsigned srcHeight, BlitFn &&blitFn)
|
|
|
|
|
{
|
|
|
|
|
if (position.y < 0 || position.y + 1 >= static_cast<int>(out.h() + srcHeight))
|
|
|
|
|
return;
|
|
|
|
|
const ClipX clipX = CalculateClipX(position.x, srcWidth, out);
|
|
|
|
|
if (clipX.width <= 0)
|
|
|
|
|
return;
|
|
|
|
|
const RenderSrc srcForBackwards { src, src + srcSize, static_cast<uint_fast16_t>(srcWidth) };
|
|
|
|
|
if (static_cast<std::size_t>(clipX.width) == srcWidth) {
|
|
|
|
|
DoRenderBackwardsClipY(
|
|
|
|
|
out, position, srcForBackwards, std::forward<BlitFn>(blitFn));
|
|
|
|
|
} else {
|
|
|
|
|
DoRenderBackwardsClipXY(
|
|
|
|
|
out, position, srcForBackwards, clipX, std::forward<BlitFn>(blitFn));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
constexpr size_t MaxOutlinePixels = 4096;
|
|
|
|
|
constexpr size_t MaxOutlineSpriteWidth = 253;
|
|
|
|
|
using OutlinePixels = StaticVector<PointOf<uint8_t>, MaxOutlinePixels>;
|
|
|
|
|
using OutlineRowSolidRuns = StaticVector<std::pair<uint8_t, uint8_t>, MaxOutlineSpriteWidth / 2 + 1>;
|
|
|
|
|
|
|
|
|
|
struct OutlinePixelsCacheEntry {
|
|
|
|
|
OutlinePixels outlinePixels;
|
|
|
|
|
const void *spriteData = nullptr;
|
|
|
|
|
bool skipColorIndexZero;
|
|
|
|
|
};
|
|
|
|
|
OutlinePixelsCacheEntry OutlinePixelsCache;
|
|
|
|
|
|
|
|
|
|
void PopulateOutlinePixelsForRow(
|
|
|
|
|
const OutlineRowSolidRuns &runs,
|
|
|
|
|
const bool *DVL_RESTRICT below,
|
|
|
|
|
bool *DVL_RESTRICT cur,
|
|
|
|
|
bool *DVL_RESTRICT above,
|
|
|
|
|
uint8_t y,
|
|
|
|
|
OutlinePixels &result)
|
|
|
|
|
{
|
|
|
|
|
DVL_ASSUME(!runs.empty());
|
|
|
|
|
for (const auto &[begin, end] : runs) {
|
|
|
|
|
if (!cur[static_cast<uint8_t>(begin - 1)]) {
|
|
|
|
|
result.emplace_back(static_cast<uint8_t>(begin - 1), y);
|
|
|
|
|
cur[static_cast<uint8_t>(begin - 1)] = true;
|
|
|
|
|
}
|
|
|
|
|
if (!cur[end]) {
|
|
|
|
|
result.emplace_back(end, y);
|
|
|
|
|
cur[end] = true;
|
|
|
|
|
}
|
|
|
|
|
for (uint8_t x = begin; x < end; ++x) {
|
|
|
|
|
if (!below[x]) {
|
|
|
|
|
result.emplace_back(x, static_cast<uint8_t>(y + 1));
|
|
|
|
|
}
|
|
|
|
|
if (!above[x]) {
|
|
|
|
|
result.emplace_back(x, static_cast<uint8_t>(y - 1));
|
|
|
|
|
above[x] = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void AppendOutlineRowSolidRuns(uint8_t x, uint8_t w, OutlineRowSolidRuns &solidRuns)
|
|
|
|
|
{
|
|
|
|
|
if (solidRuns.empty() || solidRuns.back().second != x) {
|
|
|
|
|
solidRuns.emplace_back(x, x + w);
|
|
|
|
|
} else {
|
|
|
|
|
solidRuns.back().second = static_cast<uint8_t>(x + w);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <bool SkipColorIndexZero>
|
|
|
|
|
void GetOutline(ClxSprite sprite, OutlinePixels &result) // NOLINT(readability-function-cognitive-complexity)
|
|
|
|
|
{
|
|
|
|
|
const unsigned width = sprite.width();
|
|
|
|
|
assert(width < MaxOutlineSpriteWidth);
|
|
|
|
|
|
|
|
|
|
int x = 1;
|
|
|
|
|
auto y = static_cast<uint8_t>(sprite.height());
|
|
|
|
|
|
|
|
|
|
bool rows[3][MaxOutlineSpriteWidth + 2] = { {}, {}, {} };
|
|
|
|
|
bool *rowAbove = rows[0];
|
|
|
|
|
bool *row = rows[1];
|
|
|
|
|
bool *rowBelow = rows[2];
|
|
|
|
|
|
|
|
|
|
OutlineRowSolidRuns solidRuns[2];
|
|
|
|
|
OutlineRowSolidRuns *solidRunAbove = &solidRuns[0];
|
|
|
|
|
OutlineRowSolidRuns *solidRun = &solidRuns[1];
|
|
|
|
|
|
|
|
|
|
const uint8_t *src = sprite.pixelData();
|
|
|
|
|
const uint8_t *const end = src + sprite.pixelDataSize();
|
|
|
|
|
while (src < end) {
|
|
|
|
|
while (x <= static_cast<int>(width)) {
|
|
|
|
|
const auto v = static_cast<uint8_t>(*src++);
|
|
|
|
|
uint8_t w;
|
|
|
|
|
if (IsClxOpaque(v)) {
|
|
|
|
|
if constexpr (SkipColorIndexZero) {
|
|
|
|
|
if (IsClxOpaqueFill(v)) {
|
|
|
|
|
w = GetClxOpaqueFillWidth(v);
|
|
|
|
|
const auto color = static_cast<uint8_t>(*src++);
|
|
|
|
|
if (color != 0) {
|
|
|
|
|
AppendOutlineRowSolidRuns(x, w, *solidRunAbove);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
w = GetClxOpaquePixelsWidth(v);
|
|
|
|
|
bool prevZero = solidRunAbove->empty() || solidRunAbove->back().second != x;
|
|
|
|
|
for (unsigned i = 0; i < w; ++i) {
|
|
|
|
|
const auto color = static_cast<uint8_t>(src[i]);
|
|
|
|
|
if (color == 0) {
|
|
|
|
|
if (!prevZero) ++solidRunAbove->back().second;
|
|
|
|
|
prevZero = true;
|
|
|
|
|
} else {
|
|
|
|
|
if (prevZero) solidRunAbove->emplace_back(x + i, x + i);
|
|
|
|
|
++solidRunAbove->back().second;
|
|
|
|
|
prevZero = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
src += w;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (IsClxOpaqueFill(v)) {
|
|
|
|
|
w = GetClxOpaqueFillWidth(v);
|
|
|
|
|
++src;
|
|
|
|
|
} else {
|
|
|
|
|
w = GetClxOpaquePixelsWidth(v);
|
|
|
|
|
src += w;
|
|
|
|
|
}
|
|
|
|
|
AppendOutlineRowSolidRuns(x, w, *solidRunAbove);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
w = v;
|
|
|
|
|
}
|
|
|
|
|
x += w;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const auto &[xBegin, xEnd] : *solidRunAbove) {
|
|
|
|
|
std::fill(rowAbove + xBegin, rowAbove + xEnd, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!solidRun->empty()) {
|
|
|
|
|
PopulateOutlinePixelsForRow(*solidRun, rowBelow, row, rowAbove, static_cast<uint8_t>(y + 1), result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// (0, 1, 2) => (2, 0, 1)
|
|
|
|
|
std::swap(row, rowBelow);
|
|
|
|
|
std::swap(row, rowAbove);
|
|
|
|
|
std::fill_n(rowAbove, width, false);
|
|
|
|
|
|
|
|
|
|
std::swap(solidRunAbove, solidRun);
|
|
|
|
|
solidRunAbove->clear();
|
|
|
|
|
|
|
|
|
|
if (x > static_cast<int>(width + 1)) {
|
|
|
|
|
// Transparent overrun.
|
|
|
|
|
const unsigned numWholeTransparentLines = (x - 1) / width;
|
|
|
|
|
if (numWholeTransparentLines > 1) {
|
|
|
|
|
if (!solidRun->empty()) {
|
|
|
|
|
PopulateOutlinePixelsForRow(*solidRun, rowBelow, row, rowAbove, y, result);
|
|
|
|
|
}
|
|
|
|
|
solidRun->clear();
|
|
|
|
|
std::fill_n(row, width, false);
|
|
|
|
|
}
|
|
|
|
|
if (numWholeTransparentLines > 2) std::fill_n(rowBelow, width, false);
|
|
|
|
|
y -= static_cast<uint8_t>(numWholeTransparentLines);
|
|
|
|
|
x = static_cast<int>((x - 1) % width) + 1;
|
|
|
|
|
} else {
|
|
|
|
|
--y;
|
|
|
|
|
x = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::fill_n(rowAbove, width, false);
|
|
|
|
|
if (!solidRun->empty()) {
|
|
|
|
|
PopulateOutlinePixelsForRow(*solidRun, rowBelow, row, rowAbove, static_cast<uint8_t>(y + 1), result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <bool SkipColorIndexZero>
|
|
|
|
|
void UpdateOutlinePixelsCache(ClxSprite sprite)
|
|
|
|
|
{
|
|
|
|
|
if (OutlinePixelsCache.spriteData == sprite.pixelData()
|
|
|
|
|
&& OutlinePixelsCache.skipColorIndexZero == SkipColorIndexZero) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
OutlinePixelsCache.skipColorIndexZero = SkipColorIndexZero;
|
|
|
|
|
OutlinePixelsCache.spriteData = sprite.pixelData();
|
|
|
|
|
OutlinePixelsCache.outlinePixels.clear();
|
|
|
|
|
GetOutline<SkipColorIndexZero>(sprite, OutlinePixelsCache.outlinePixels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
template <bool SkipColorIndexZero>
|
|
|
|
|
void RenderClxOutline(const Surface &out, Point position, ClxSprite sprite, uint8_t color)
|
|
|
|
|
{
|
|
|
|
|
UpdateOutlinePixelsCache<SkipColorIndexZero>(sprite);
|
|
|
|
|
--position.x;
|
|
|
|
|
position.y -= sprite.height();
|
|
|
|
|
if (position.x >= 0 && position.x + sprite.width() + 2 < out.w()
|
|
|
|
|
&& position.y >= 0 && position.y + sprite.height() + 2 < out.h()) {
|
|
|
|
|
for (const auto &[x, y] : OutlinePixelsCache.outlinePixels) {
|
|
|
|
|
*out.at(position.x + x, position.y + y) = color;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (const auto &[x, y] : OutlinePixelsCache.outlinePixels) {
|
|
|
|
|
out.SetPixel(Point(position.x + x, position.y + y), color);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClxApplyTrans(ClxSprite sprite, const uint8_t *trn)
|
|
|
|
|
{
|
|
|
|
|
// A bit of a hack but this is the only place in the code where we need mutable sprites.
|
|
|
|
|
auto *dst = const_cast<uint8_t *>(sprite.pixelData());
|
|
|
|
|
uint16_t remaining = sprite.pixelDataSize();
|
|
|
|
|
while (remaining != 0) {
|
|
|
|
|
uint8_t val = *dst++;
|
|
|
|
|
--remaining;
|
|
|
|
|
if (!IsClxOpaque(val))
|
|
|
|
|
continue;
|
|
|
|
|
if (IsClxOpaqueFill(val)) {
|
|
|
|
|
--remaining;
|
|
|
|
|
*dst = trn[*dst];
|
|
|
|
|
dst++;
|
|
|
|
|
} else {
|
|
|
|
|
val = GetClxOpaquePixelsWidth(val);
|
|
|
|
|
remaining -= val;
|
|
|
|
|
while (val-- > 0) {
|
|
|
|
|
*dst = trn[*dst];
|
|
|
|
|
dst++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
|
|
void ClxApplyTrans(ClxSpriteList list, const uint8_t *trn)
|
|
|
|
|
{
|
|
|
|
|
for (const ClxSprite sprite : list) {
|
|
|
|
|
ClxApplyTrans(sprite, trn);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClxApplyTrans(ClxSpriteSheet sheet, const uint8_t *trn)
|
|
|
|
|
{
|
|
|
|
|
for (const ClxSpriteList list : sheet) {
|
|
|
|
|
ClxApplyTrans(list, trn);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool IsPointWithinClx(Point position, ClxSprite clx)
|
|
|
|
|
{
|
|
|
|
|
const uint8_t *src = clx.pixelData();
|
|
|
|
|
const uint8_t *end = src + clx.pixelDataSize();
|
|
|
|
|
const uint16_t width = clx.width();
|
|
|
|
|
|
|
|
|
|
int xCur = 0;
|
|
|
|
|
int yCur = clx.height() - 1;
|
|
|
|
|
while (src < end) {
|
|
|
|
|
if (yCur != position.y) {
|
|
|
|
|
SkipSize skipSize {};
|
|
|
|
|
skipSize.xOffset = xCur;
|
|
|
|
|
src = SkipRestOfLineWithOverrun(src, width, skipSize);
|
|
|
|
|
yCur -= skipSize.wholeLines;
|
|
|
|
|
xCur = skipSize.xOffset;
|
|
|
|
|
if (yCur < position.y)
|
|
|
|
|
return false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (xCur < width) {
|
|
|
|
|
uint8_t val = *src++;
|
|
|
|
|
if (!IsClxOpaque(val)) {
|
|
|
|
|
// ignore transparent
|
|
|
|
|
xCur += val;
|
|
|
|
|
if (xCur > position.x)
|
|
|
|
|
return false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (IsClxOpaqueFill(val)) {
|
|
|
|
|
val = GetClxOpaqueFillWidth(val);
|
|
|
|
|
const uint8_t color = *src++;
|
|
|
|
|
if (xCur <= position.x && position.x < xCur + val)
|
|
|
|
|
return color != 0; // ignore shadows
|
|
|
|
|
xCur += val;
|
|
|
|
|
} else {
|
|
|
|
|
val = GetClxOpaquePixelsWidth(val);
|
|
|
|
|
for (uint8_t pixel = 0; pixel < val; pixel++) {
|
|
|
|
|
const uint8_t color = *src++;
|
|
|
|
|
if (xCur == position.x)
|
|
|
|
|
return color != 0; // ignore shadows
|
|
|
|
|
xCur++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::pair<int, int> ClxMeasureSolidHorizontalBounds(ClxSprite clx)
|
|
|
|
|
{
|
|
|
|
|
const uint8_t *src = clx.pixelData();
|
|
|
|
|
const uint8_t *end = src + clx.pixelDataSize();
|
|
|
|
|
const uint16_t width = clx.width();
|
|
|
|
|
|
|
|
|
|
int xBegin = width;
|
|
|
|
|
int xEnd = 0;
|
|
|
|
|
int xCur = 0;
|
|
|
|
|
while (src < end) {
|
|
|
|
|
while (xCur < width) {
|
|
|
|
|
auto val = *src++;
|
|
|
|
|
if (!IsClxOpaque(val)) {
|
|
|
|
|
xCur += val;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (IsClxOpaqueFill(val)) {
|
|
|
|
|
val = GetClxOpaqueFillWidth(val);
|
|
|
|
|
++src;
|
|
|
|
|
} else {
|
|
|
|
|
val = GetClxOpaquePixelsWidth(val);
|
|
|
|
|
src += val;
|
|
|
|
|
}
|
|
|
|
|
xBegin = std::min(xBegin, xCur);
|
|
|
|
|
xCur += val;
|
|
|
|
|
xEnd = std::max(xEnd, xCur);
|
|
|
|
|
}
|
|
|
|
|
while (xCur >= width)
|
|
|
|
|
xCur -= width;
|
|
|
|
|
if (xBegin == 0 && xEnd == width)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return { xBegin, xEnd };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef DEBUG_CLX
|
|
|
|
|
std::string ClxDescribe(ClxSprite clx)
|
|
|
|
|
{
|
|
|
|
|
std::string out = StrCat(
|
|
|
|
|
"CLX sprite: ", clx.width(), "x", clx.height(), " pixelDataSize=", clx.pixelDataSize(),
|
|
|
|
|
"b\n\n"
|
|
|
|
|
"command | width | bytes | color(s)\n"
|
|
|
|
|
"--------|------:|------:|---------\n");
|
|
|
|
|
const uint8_t *src = clx.pixelData();
|
|
|
|
|
const uint8_t *end = src + clx.pixelDataSize();
|
|
|
|
|
while (src < end) {
|
|
|
|
|
const uint8_t control = *src++;
|
|
|
|
|
if (IsClxOpaque(control)) {
|
|
|
|
|
if (IsClxOpaqueFill(control)) {
|
|
|
|
|
const uint8_t length = GetClxOpaqueFillWidth(control);
|
|
|
|
|
out.append(fmt::format("Fill | {:>5} | {:>5} | {}\n", length, 2, src[1]));
|
|
|
|
|
++src;
|
|
|
|
|
} else {
|
|
|
|
|
const uint8_t length = GetClxOpaquePixelsWidth(control);
|
|
|
|
|
out.append(fmt::format("Pixels | {:>5} | {:>5} | {}\n", length, length + 1, fmt::join(src + 1, src + 1 + length, " ")));
|
|
|
|
|
src += length;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
out.append(fmt::format("Transp. | {:>5} | {:>5} |\n", control, 1));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
#endif // DEBUG_CLX
|
|
|
|
|
|
|
|
|
|
void ClxDraw(const Surface &out, Point position, ClxSprite clx)
|
|
|
|
|
{
|
|
|
|
|
DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitDirect {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClxDrawTRN(const Surface &out, Point position, ClxSprite clx, const uint8_t *trn)
|
|
|
|
|
{
|
|
|
|
|
DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitWithMap { trn });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClxDrawBlended(const Surface &out, Point position, ClxSprite clx)
|
|
|
|
|
{
|
|
|
|
|
DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlended {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClxDrawBlendedTRN(const Surface &out, Point position, ClxSprite clx, const uint8_t *trn)
|
|
|
|
|
{
|
|
|
|
|
DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlendedWithMap { trn });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClxDrawOutline(const Surface &out, uint8_t col, Point position, ClxSprite clx)
|
|
|
|
|
{
|
|
|
|
|
RenderClxOutline</*SkipColorIndexZero=*/false>(out, position, clx, col);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClxDrawOutlineSkipColorZero(const Surface &out, uint8_t col, Point position, ClxSprite clx)
|
|
|
|
|
{
|
|
|
|
|
RenderClxOutline</*SkipColorIndexZero=*/true>(out, position, clx, col);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ClearClxDrawCache()
|
|
|
|
|
{
|
|
|
|
|
OutlinePixelsCache.spriteData = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace devilution
|