1#include "render_layer.h"
2
3#include <base/dbg.h>
4#include <base/log.h>
5#include <base/mem.h>
6#include <base/str.h>
7#include <base/time.h>
8
9#include <engine/graphics.h>
10#include <engine/map.h>
11#include <engine/shared/config.h>
12#include <engine/storage.h>
13
14#include <game/localization.h>
15#include <game/mapitems.h>
16
17#include <array>
18#include <chrono>
19
20/************************
21 * Render Buffer Helper *
22 ************************/
23class CTexCoords
24{
25public:
26 std::array<uint8_t, 4> m_aTexX;
27 std::array<uint8_t, 4> m_aTexY;
28};
29
30constexpr static CTexCoords CalculateTexCoords(unsigned int Flags)
31{
32 CTexCoords TexCoord;
33 TexCoord.m_aTexX = {0, 1, 1, 0};
34 TexCoord.m_aTexY = {0, 0, 1, 1};
35
36 if(Flags & TILEFLAG_XFLIP)
37 std::rotate(first: std::begin(cont&: TexCoord.m_aTexX), middle: std::begin(cont&: TexCoord.m_aTexX) + 2, last: std::end(cont&: TexCoord.m_aTexX));
38
39 if(Flags & TILEFLAG_YFLIP)
40 std::rotate(first: std::begin(cont&: TexCoord.m_aTexY), middle: std::begin(cont&: TexCoord.m_aTexY) + 2, last: std::end(cont&: TexCoord.m_aTexY));
41
42 if(Flags & (TILEFLAG_ROTATE >> 1))
43 {
44 std::rotate(first: std::begin(cont&: TexCoord.m_aTexX), middle: std::begin(cont&: TexCoord.m_aTexX) + 3, last: std::end(cont&: TexCoord.m_aTexX));
45 std::rotate(first: std::begin(cont&: TexCoord.m_aTexY), middle: std::begin(cont&: TexCoord.m_aTexY) + 3, last: std::end(cont&: TexCoord.m_aTexY));
46 }
47 return TexCoord;
48}
49
50template<std::size_t N>
51constexpr static std::array<CTexCoords, N> MakeTexCoordsTable()
52{
53 std::array<CTexCoords, N> aTexCoords = {};
54 for(std::size_t i = 0; i < N; ++i)
55 aTexCoords[i] = CalculateTexCoords(Flags: i);
56 return aTexCoords;
57}
58
59constexpr std::array<CTexCoords, 8> TEX_COORDS_TABLE = MakeTexCoordsTable<8>();
60
61static void FillTmpTile(CGraphicTile *pTmpTile, CGraphicTileTextureCoords *pTmpTex, unsigned char Flags, unsigned char Index, int x, int y, const ivec2 &Offset, int Scale)
62{
63 if(pTmpTex)
64 {
65 uint8_t TableFlag = (Flags & (TILEFLAG_XFLIP | TILEFLAG_YFLIP)) + ((Flags & TILEFLAG_ROTATE) >> 1);
66 const auto &aTexX = TEX_COORDS_TABLE[TableFlag].m_aTexX;
67 const auto &aTexY = TEX_COORDS_TABLE[TableFlag].m_aTexY;
68
69 pTmpTex->m_TexCoordTopLeft.x = aTexX[0];
70 pTmpTex->m_TexCoordTopLeft.y = aTexY[0];
71 pTmpTex->m_TexCoordBottomLeft.x = aTexX[3];
72 pTmpTex->m_TexCoordBottomLeft.y = aTexY[3];
73 pTmpTex->m_TexCoordTopRight.x = aTexX[1];
74 pTmpTex->m_TexCoordTopRight.y = aTexY[1];
75 pTmpTex->m_TexCoordBottomRight.x = aTexX[2];
76 pTmpTex->m_TexCoordBottomRight.y = aTexY[2];
77
78 pTmpTex->m_TexCoordTopLeft.z = Index;
79 pTmpTex->m_TexCoordBottomLeft.z = Index;
80 pTmpTex->m_TexCoordTopRight.z = Index;
81 pTmpTex->m_TexCoordBottomRight.z = Index;
82
83 bool HasRotation = (Flags & TILEFLAG_ROTATE) != 0;
84 pTmpTex->m_TexCoordTopLeft.w = HasRotation;
85 pTmpTex->m_TexCoordBottomLeft.w = HasRotation;
86 pTmpTex->m_TexCoordTopRight.w = HasRotation;
87 pTmpTex->m_TexCoordBottomRight.w = HasRotation;
88 }
89
90 vec2 TopLeft(x * Scale + Offset.x, y * Scale + Offset.y);
91 vec2 BottomRight(x * Scale + Scale + Offset.x, y * Scale + Scale + Offset.y);
92 pTmpTile->m_TopLeft = TopLeft;
93 pTmpTile->m_BottomLeft.x = TopLeft.x;
94 pTmpTile->m_BottomLeft.y = BottomRight.y;
95 pTmpTile->m_TopRight.x = BottomRight.x;
96 pTmpTile->m_TopRight.y = TopLeft.y;
97 pTmpTile->m_BottomRight = BottomRight;
98}
99
100static void FillTmpTileSpeedup(CGraphicTile *pTmpTile, CGraphicTileTextureCoords *pTmpTex, unsigned char Flags, int x, int y, const ivec2 &Offset, int Scale, short AngleRotate)
101{
102 int Angle = AngleRotate % 360;
103 FillTmpTile(pTmpTile, pTmpTex, Flags: Angle >= 270 ? ROTATION_270 : (Angle >= 180 ? ROTATION_180 : (Angle >= 90 ? ROTATION_90 : 0)), Index: AngleRotate % 90, x, y, Offset, Scale);
104}
105
106static bool AddTile(std::vector<CGraphicTile> &vTmpTiles, std::vector<CGraphicTileTextureCoords> &vTmpTileTexCoords, unsigned char Index, unsigned char Flags, int x, int y, bool DoTextureCoords, bool FillSpeedup = false, int AngleRotate = -1, const ivec2 &Offset = ivec2{0, 0}, int Scale = 32)
107{
108 if(Index <= 0)
109 return false;
110
111 vTmpTiles.emplace_back();
112 CGraphicTile &Tile = vTmpTiles.back();
113 CGraphicTileTextureCoords *pTileTex = nullptr;
114 if(DoTextureCoords)
115 {
116 vTmpTileTexCoords.emplace_back();
117 CGraphicTileTextureCoords &TileTex = vTmpTileTexCoords.back();
118 pTileTex = &TileTex;
119 }
120 if(FillSpeedup)
121 FillTmpTileSpeedup(pTmpTile: &Tile, pTmpTex: pTileTex, Flags, x, y, Offset, Scale, AngleRotate);
122 else
123 FillTmpTile(pTmpTile: &Tile, pTmpTex: pTileTex, Flags, Index, x, y, Offset, Scale);
124
125 return true;
126}
127
128class CTmpQuadVertexTextured
129{
130public:
131 float m_X, m_Y, m_CenterX, m_CenterY;
132 unsigned char m_R, m_G, m_B, m_A;
133 float m_U, m_V;
134};
135
136class CTmpQuadVertex
137{
138public:
139 float m_X, m_Y, m_CenterX, m_CenterY;
140 unsigned char m_R, m_G, m_B, m_A;
141};
142
143class CTmpQuad
144{
145public:
146 CTmpQuadVertex m_aVertices[4];
147};
148
149class CTmpQuadTextured
150{
151public:
152 CTmpQuadVertexTextured m_aVertices[4];
153};
154
155bool CRenderLayerTile::CTileLayerVisuals::Init(unsigned int Width, unsigned int Height)
156{
157 m_Width = Width;
158 m_Height = Height;
159 if(Width == 0 || Height == 0)
160 return false;
161 if constexpr(sizeof(unsigned int) >= sizeof(ptrdiff_t))
162 if(Width >= std::numeric_limits<std::ptrdiff_t>::max() || Height >= std::numeric_limits<std::ptrdiff_t>::max())
163 return false;
164
165 m_vTilesOfLayer.resize(new_size: (size_t)Height * (size_t)Width);
166
167 m_vBorderTop.resize(new_size: Width);
168 m_vBorderBottom.resize(new_size: Width);
169
170 m_vBorderLeft.resize(new_size: Height);
171 m_vBorderRight.resize(new_size: Height);
172 return true;
173}
174
175/**************
176 * Base Layer *
177 **************/
178
179CRenderLayer::CRenderLayer(int GroupId, int LayerId, int Flags) :
180 m_GroupId(GroupId), m_LayerId(LayerId), m_Flags(Flags) {}
181
182void CRenderLayer::OnInit(IGraphics *pGraphics, ITextRender *pTextRender, CRenderMap *pRenderMap, std::shared_ptr<CEnvelopeManager> &pEnvelopeManager, IMap *pMap, IMapImages *pMapImages, std::optional<FRenderUploadCallback> &FRenderUploadCallbackOptional)
183{
184 CRenderComponent::OnInit(pGraphics, pTextRender, pRenderMap);
185 m_pMap = pMap;
186 m_pMapImages = pMapImages;
187 m_RenderUploadCallback = FRenderUploadCallbackOptional;
188 m_pEnvelopeManager = pEnvelopeManager;
189}
190
191void CRenderLayer::UseTexture(IGraphics::CTextureHandle TextureHandle)
192{
193 if(TextureHandle.IsValid())
194 Graphics()->TextureSet(Texture: TextureHandle);
195 else
196 Graphics()->TextureClear();
197}
198
199void CRenderLayer::RenderLoading() const
200{
201 const char *pLoadingTitle = Localize(pStr: "Loading map");
202 const char *pLoadingMessage = Localize(pStr: "Uploading map data to GPU");
203 if(m_RenderUploadCallback.has_value())
204 (*m_RenderUploadCallback)(pLoadingTitle, pLoadingMessage, 0);
205}
206
207bool CRenderLayer::IsVisibleInClipRegion(const std::optional<CClipRegion> &ClipRegion) const
208{
209 // always show unclipped regions
210 if(!ClipRegion.has_value())
211 return true;
212
213 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
214 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
215 float Left = ClipRegion->m_X;
216 float Top = ClipRegion->m_Y;
217 float Right = ClipRegion->m_X + ClipRegion->m_Width;
218 float Bottom = ClipRegion->m_Y + ClipRegion->m_Height;
219
220 return Right >= ScreenX0 && Left <= ScreenX1 && Bottom >= ScreenY0 && Top <= ScreenY1;
221}
222
223/**************
224 * Group *
225 **************/
226
227CRenderLayerGroup::CRenderLayerGroup(int GroupId, CMapItemGroup *pGroup) :
228 CRenderLayer(GroupId, 0, 0), m_pGroup(pGroup) {}
229
230bool CRenderLayerGroup::DoRender(const CRenderLayerParams &Params)
231{
232 if(!g_Config.m_GfxNoclip || Params.m_RenderType == ERenderType::RENDERTYPE_FULL_DESIGN)
233 {
234 Graphics()->ClipDisable();
235 if(m_pGroup->m_Version >= 2 && m_pGroup->m_UseClipping)
236 {
237 // set clipping
238 Graphics()->MapScreenToInterface(CenterX: Params.m_Center.x, CenterY: Params.m_Center.y, Zoom: Params.m_Zoom);
239
240 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
241 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
242 float ScreenWidth = ScreenX1 - ScreenX0;
243 float ScreenHeight = ScreenY1 - ScreenY0;
244 float Left = m_pGroup->m_ClipX - ScreenX0;
245 float Top = m_pGroup->m_ClipY - ScreenY0;
246 float Right = m_pGroup->m_ClipX + m_pGroup->m_ClipW - ScreenX0;
247 float Bottom = m_pGroup->m_ClipY + m_pGroup->m_ClipH - ScreenY0;
248
249 if(Right < 0.0f || Left > ScreenWidth || Bottom < 0.0f || Top > ScreenHeight)
250 return false;
251
252 // Render debug before enabling the clip
253 if(Params.m_DebugRenderGroupClips)
254 {
255 char aDebugText[32];
256 str_format(buffer: aDebugText, buffer_size: sizeof(aDebugText), format: "Group %d", m_GroupId);
257 RenderMap()->RenderDebugClip(ClipX: m_pGroup->m_ClipX, ClipY: m_pGroup->m_ClipY, ClipW: m_pGroup->m_ClipW, ClipH: m_pGroup->m_ClipH, Color: ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), Zoom: Params.m_Zoom, pLabel: aDebugText);
258 }
259
260 int ClipX = (int)std::round(x: Left * Graphics()->ScreenWidth() / ScreenWidth);
261 int ClipY = (int)std::round(x: Top * Graphics()->ScreenHeight() / ScreenHeight);
262
263 Graphics()->ClipEnable(
264 x: ClipX,
265 y: ClipY,
266 w: (int)std::round(x: Right * Graphics()->ScreenWidth() / ScreenWidth) - ClipX,
267 h: (int)std::round(x: Bottom * Graphics()->ScreenHeight() / ScreenHeight) - ClipY);
268 }
269 }
270 return true;
271}
272
273void CRenderLayerGroup::Render(const CRenderLayerParams &Params)
274{
275 int ParallaxZoom = std::clamp(val: (maximum(a: m_pGroup->m_ParallaxX, b: m_pGroup->m_ParallaxY)), lo: 0, hi: 100);
276 float aPoints[4];
277 Graphics()->MapScreenToWorld(CenterX: Params.m_Center.x, CenterY: Params.m_Center.y, ParallaxX: m_pGroup->m_ParallaxX, ParallaxY: m_pGroup->m_ParallaxY, ParallaxZoom: (float)ParallaxZoom,
278 OffsetX: m_pGroup->m_OffsetX, OffsetY: m_pGroup->m_OffsetY, Aspect: Graphics()->ScreenAspect(), Zoom: Params.m_Zoom, pPoints: aPoints);
279 Graphics()->MapScreen(TopLeftX: aPoints[0], TopLeftY: aPoints[1], BottomRightX: aPoints[2], BottomRightY: aPoints[3]);
280}
281
282/**************
283 * Tile Layer *
284 **************/
285
286CRenderLayerTile::CRenderLayerTile(int GroupId, int LayerId, int Flags, CMapItemLayerTilemap *pLayerTilemap) :
287 CRenderLayer(GroupId, LayerId, Flags)
288{
289 m_pLayerTilemap = pLayerTilemap;
290 m_Color = ColorRGBA(m_pLayerTilemap->m_Color.r / 255.0f, m_pLayerTilemap->m_Color.g / 255.0f, m_pLayerTilemap->m_Color.b / 255.0f, pLayerTilemap->m_Color.a / 255.0f);
291 m_pTiles = nullptr;
292}
293
294void CRenderLayerTile::RenderTileLayer(const ColorRGBA &Color, const CRenderLayerParams &Params, CTileLayerVisuals *pTileLayerVisuals)
295{
296 CTileLayerVisuals &Visuals = pTileLayerVisuals ? *pTileLayerVisuals : m_VisualTiles.value();
297 if(Visuals.m_BufferContainerIndex == -1)
298 return; // no visuals were created
299
300 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
301 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
302
303 int ScreenRectY0 = std::floor(x: ScreenY0 / 32);
304 int ScreenRectX0 = std::floor(x: ScreenX0 / 32);
305 int ScreenRectY1 = std::ceil(x: ScreenY1 / 32);
306 int ScreenRectX1 = std::ceil(x: ScreenX1 / 32);
307
308 if(IsVisibleInClipRegion(ClipRegion: m_LayerClip))
309 {
310 // create the indice buffers we want to draw -- reuse them
311 std::vector<char *> vpIndexOffsets;
312 std::vector<unsigned int> vDrawCounts;
313
314 int X0 = std::max(a: ScreenRectX0, b: 0);
315 int X1 = std::min(a: ScreenRectX1, b: (int)Visuals.m_Width);
316 int XR = X1 == std::numeric_limits<int>::min() ? X1 : X1 - 1;
317 if(X0 <= XR)
318 {
319 int Y0 = std::max(a: ScreenRectY0, b: 0);
320 int Y1 = std::min(a: ScreenRectY1, b: (int)Visuals.m_Height);
321
322 unsigned long long Reserve = absolute(a: Y1 - Y0) + 1;
323 vpIndexOffsets.reserve(n: Reserve);
324 vDrawCounts.reserve(n: Reserve);
325
326 for(int y = Y0; y < Y1; ++y)
327 {
328 dbg_assert(Visuals.m_vTilesOfLayer[y * Visuals.m_Width + XR].IndexBufferByteOffset() >= Visuals.m_vTilesOfLayer[y * Visuals.m_Width + X0].IndexBufferByteOffset(), "Tile offsets are not monotone.");
329 unsigned int NumVertices = ((Visuals.m_vTilesOfLayer[y * Visuals.m_Width + XR].IndexBufferByteOffset() - Visuals.m_vTilesOfLayer[y * Visuals.m_Width + X0].IndexBufferByteOffset()) / sizeof(unsigned int)) + (Visuals.m_vTilesOfLayer[y * Visuals.m_Width + XR].DoDraw() ? 6lu : 0lu);
330
331 if(NumVertices)
332 {
333 vpIndexOffsets.push_back(x: (offset_ptr_size)Visuals.m_vTilesOfLayer[y * Visuals.m_Width + X0].IndexBufferByteOffset());
334 vDrawCounts.push_back(x: NumVertices);
335 }
336 }
337
338 int DrawCount = vpIndexOffsets.size();
339 if(DrawCount != 0)
340 {
341 Graphics()->RenderTileLayer(BufferContainerIndex: Visuals.m_BufferContainerIndex, Color, pOffsets: vpIndexOffsets.data(), pIndicedVertexDrawNum: vDrawCounts.data(), NumIndicesOffset: DrawCount);
342 }
343 }
344 }
345
346 if(Params.m_RenderTileBorder && (ScreenRectX1 > (int)Visuals.m_Width || ScreenRectY1 > (int)Visuals.m_Height || ScreenRectX0 < 0 || ScreenRectY0 < 0))
347 {
348 RenderTileBorder(Color, BorderX0: ScreenRectX0, BorderY0: ScreenRectY0, BorderX1: ScreenRectX1, BorderY1: ScreenRectY1, pTileLayerVisuals: &Visuals);
349 }
350}
351
352void CRenderLayerTile::RenderTileBorder(const ColorRGBA &Color, int BorderX0, int BorderY0, int BorderX1, int BorderY1, CTileLayerVisuals *pTileLayerVisuals)
353{
354 CTileLayerVisuals &Visuals = *pTileLayerVisuals;
355
356 int Y0 = std::max(a: 0, b: BorderY0);
357 int X0 = std::max(a: 0, b: BorderX0);
358 int Y1 = std::min(a: (int)Visuals.m_Height, b: BorderY1);
359 int X1 = std::min(a: (int)Visuals.m_Width, b: BorderX1);
360
361 // corners
362 auto DrawCorner = [&](vec2 Offset, vec2 Scale, CTileLayerVisuals::CTileVisual &Visual) {
363 Offset *= 32.0f;
364 Graphics()->RenderBorderTiles(BufferContainerIndex: Visuals.m_BufferContainerIndex, Color, pIndexBufferOffset: (offset_ptr_size)Visual.IndexBufferByteOffset(), Offset, Scale, DrawNum: 1);
365 };
366
367 if(BorderX0 < 0)
368 {
369 // Draw corners on left side
370 if(BorderY0 < 0 && Visuals.m_BorderTopLeft.DoDraw())
371 {
372 DrawCorner(
373 vec2(0, 0),
374 vec2(std::abs(x: BorderX0), std::abs(x: BorderY0)),
375 Visuals.m_BorderTopLeft);
376 }
377 if(BorderY1 > (int)Visuals.m_Height && Visuals.m_BorderBottomLeft.DoDraw())
378 {
379 DrawCorner(
380 vec2(0, Visuals.m_Height),
381 vec2(std::abs(x: BorderX0), BorderY1 - Visuals.m_Height),
382 Visuals.m_BorderBottomLeft);
383 }
384 }
385 if(BorderX1 > (int)Visuals.m_Width)
386 {
387 // Draw corners on right side
388 if(BorderY0 < 0 && Visuals.m_BorderTopRight.DoDraw())
389 {
390 DrawCorner(
391 vec2(Visuals.m_Width, 0),
392 vec2(BorderX1 - Visuals.m_Width, std::abs(x: BorderY0)),
393 Visuals.m_BorderTopRight);
394 }
395 if(BorderY1 > (int)Visuals.m_Height && Visuals.m_BorderBottomRight.DoDraw())
396 {
397 DrawCorner(
398 vec2(Visuals.m_Width, Visuals.m_Height),
399 vec2(BorderX1 - Visuals.m_Width, BorderY1 - Visuals.m_Height),
400 Visuals.m_BorderBottomRight);
401 }
402 }
403
404 // borders
405 auto DrawBorder = [&](vec2 Offset, vec2 Scale, CTileLayerVisuals::CTileVisual &StartVisual, CTileLayerVisuals::CTileVisual &EndVisual) {
406 unsigned int DrawNum = ((EndVisual.IndexBufferByteOffset() - StartVisual.IndexBufferByteOffset()) / (sizeof(unsigned int) * 6)) + (EndVisual.DoDraw() ? 1lu : 0lu);
407 offset_ptr_size pOffset = (offset_ptr_size)StartVisual.IndexBufferByteOffset();
408 Offset *= 32.0f;
409 Graphics()->RenderBorderTiles(BufferContainerIndex: Visuals.m_BufferContainerIndex, Color, pIndexBufferOffset: pOffset, Offset, Scale, DrawNum);
410 };
411
412 if(Y0 < (int)Visuals.m_Height && Y1 > 0)
413 {
414 if(BorderX1 > (int)Visuals.m_Width)
415 {
416 // Draw right border
417 DrawBorder(
418 vec2(Visuals.m_Width, 0),
419 vec2(BorderX1 - Visuals.m_Width, 1.f),
420 Visuals.m_vBorderRight[Y0], Visuals.m_vBorderRight[Y1 - 1]);
421 }
422 if(BorderX0 < 0)
423 {
424 // Draw left border
425 DrawBorder(
426 vec2(0, 0),
427 vec2(std::abs(x: BorderX0), 1),
428 Visuals.m_vBorderLeft[Y0], Visuals.m_vBorderLeft[Y1 - 1]);
429 }
430 }
431
432 if(X0 < (int)Visuals.m_Width && X1 > 0)
433 {
434 if(BorderY0 < 0)
435 {
436 // Draw top border
437 DrawBorder(
438 vec2(0, 0),
439 vec2(1, std::abs(x: BorderY0)),
440 Visuals.m_vBorderTop[X0], Visuals.m_vBorderTop[X1 - 1]);
441 }
442 if(BorderY1 > (int)Visuals.m_Height)
443 {
444 // Draw bottom border
445 DrawBorder(
446 vec2(0, Visuals.m_Height),
447 vec2(1, BorderY1 - Visuals.m_Height),
448 Visuals.m_vBorderBottom[X0], Visuals.m_vBorderBottom[X1 - 1]);
449 }
450 }
451}
452
453void CRenderLayerTile::RenderKillTileBorder(const ColorRGBA &Color)
454{
455 CTileLayerVisuals &Visuals = m_VisualTiles.value();
456 if(Visuals.m_BufferContainerIndex == -1)
457 return; // no visuals were created
458
459 float ScreenX0, ScreenY0, ScreenX1, ScreenY1;
460 Graphics()->GetScreen(pTopLeftX: &ScreenX0, pTopLeftY: &ScreenY0, pBottomRightX: &ScreenX1, pBottomRightY: &ScreenY1);
461
462 int BorderY0 = std::floor(x: ScreenY0 / 32);
463 int BorderX0 = std::floor(x: ScreenX0 / 32);
464 int BorderY1 = std::ceil(x: ScreenY1 / 32);
465 int BorderX1 = std::ceil(x: ScreenX1 / 32);
466
467 if(BorderX0 >= -BorderRenderDistance && BorderY0 >= -BorderRenderDistance && BorderX1 <= (int)Visuals.m_Width + BorderRenderDistance && BorderY1 <= (int)Visuals.m_Height + BorderRenderDistance)
468 return;
469 if(!Visuals.m_BorderKillTile.DoDraw())
470 return;
471
472 BorderX0 = std::clamp(val: BorderX0, lo: -300, hi: (int)Visuals.m_Width + 299);
473 BorderY0 = std::clamp(val: BorderY0, lo: -300, hi: (int)Visuals.m_Height + 299);
474 BorderX1 = std::clamp(val: BorderX1, lo: -300, hi: (int)Visuals.m_Width + 299);
475 BorderY1 = std::clamp(val: BorderY1, lo: -300, hi: (int)Visuals.m_Height + 299);
476
477 auto DrawKillBorder = [&](vec2 Offset, vec2 Scale) {
478 offset_ptr_size pOffset = (offset_ptr_size)Visuals.m_BorderKillTile.IndexBufferByteOffset();
479 Offset *= 32.0f;
480 Graphics()->RenderBorderTiles(BufferContainerIndex: Visuals.m_BufferContainerIndex, Color, pIndexBufferOffset: pOffset, Offset, Scale, DrawNum: 1);
481 };
482
483 // Draw left kill tile border
484 if(BorderX0 < -BorderRenderDistance)
485 {
486 DrawKillBorder(
487 vec2(BorderX0, BorderY0),
488 vec2(-BorderRenderDistance - BorderX0, BorderY1 - BorderY0));
489 }
490 // Draw top kill tile border
491 if(BorderY0 < -BorderRenderDistance)
492 {
493 DrawKillBorder(
494 vec2(std::max(a: BorderX0, b: -BorderRenderDistance), BorderY0),
495 vec2(std::min(a: BorderX1, b: (int)Visuals.m_Width + BorderRenderDistance) - std::max(a: BorderX0, b: -BorderRenderDistance), -BorderRenderDistance - BorderY0));
496 }
497 // Draw right kill tile border
498 if(BorderX1 > (int)Visuals.m_Width + BorderRenderDistance)
499 {
500 DrawKillBorder(
501 vec2(Visuals.m_Width + BorderRenderDistance, BorderY0),
502 vec2(BorderX1 - (Visuals.m_Width + BorderRenderDistance), BorderY1 - BorderY0));
503 }
504 // Draw bottom kill tile border
505 if(BorderY1 > (int)Visuals.m_Height + BorderRenderDistance)
506 {
507 DrawKillBorder(
508 vec2(std::max(a: BorderX0, b: -BorderRenderDistance), Visuals.m_Height + BorderRenderDistance),
509 vec2(std::min(a: BorderX1, b: (int)Visuals.m_Width + BorderRenderDistance) - std::max(a: BorderX0, b: -BorderRenderDistance), BorderY1 - (Visuals.m_Height + BorderRenderDistance)));
510 }
511}
512
513ColorRGBA CRenderLayerTile::GetRenderColor(const CRenderLayerParams &Params) const
514{
515 ColorRGBA Color = m_Color;
516 if(Params.m_EntityOverlayVal && Params.m_RenderType != ERenderType::RENDERTYPE_BACKGROUND_FORCE)
517 Color.a *= (100 - Params.m_EntityOverlayVal) / 100.0f;
518
519 ColorRGBA ColorEnv = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
520 m_pEnvelopeManager->EnvelopeEval()->EnvelopeEval(TimeOffsetMillis: m_pLayerTilemap->m_ColorEnvOffset, EnvelopeIndex: m_pLayerTilemap->m_ColorEnv, Result&: ColorEnv, Channels: 4);
521 Color = Color.Multiply(Other: ColorEnv);
522 return Color;
523}
524
525void CRenderLayerTile::Render(const CRenderLayerParams &Params)
526{
527 UseTexture(TextureHandle: GetTexture());
528 ColorRGBA Color = GetRenderColor(Params);
529 if(Graphics()->IsTileBufferingEnabled() && Params.m_TileAndQuadBuffering)
530 {
531 RenderTileLayerWithTileBuffer(Color, Params);
532 }
533 else
534 {
535 RenderTileLayerNoTileBuffer(Color, Params);
536 }
537
538 if(Params.m_DebugRenderTileClips && m_LayerClip.has_value())
539 {
540 const CClipRegion &Clip = m_LayerClip.value();
541 char aDebugText[32];
542 str_format(buffer: aDebugText, buffer_size: sizeof(aDebugText), format: "Group %d LayerId %d", m_GroupId, m_LayerId);
543 RenderMap()->RenderDebugClip(ClipX: Clip.m_X, ClipY: Clip.m_Y, ClipW: Clip.m_Width, ClipH: Clip.m_Height, Color: ColorRGBA(1.0f, 0.5f, 0.0f, 1.0f), Zoom: Params.m_Zoom, pLabel: aDebugText);
544 }
545}
546
547bool CRenderLayerTile::DoRender(const CRenderLayerParams &Params)
548{
549 // skip rendering if we render background force, but deactivated tile layer and want to render a tilelayer
550 if(!g_Config.m_ClBackgroundShowTilesLayers && Params.m_RenderType == ERenderType::RENDERTYPE_BACKGROUND_FORCE)
551 return false;
552
553 // skip rendering anything but entities if we only want to render entities
554 if(Params.m_EntityOverlayVal == 100 && Params.m_RenderType != ERenderType::RENDERTYPE_BACKGROUND_FORCE)
555 return false;
556
557 // skip rendering if detail layers if not wanted
558 if(m_Flags & LAYERFLAG_DETAIL && !g_Config.m_GfxHighDetail && Params.m_RenderType != ERenderType::RENDERTYPE_FULL_DESIGN) // detail but no details
559 return false;
560 return true;
561}
562
563void CRenderLayerTile::RenderTileLayerWithTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
564{
565 Graphics()->BlendNormal();
566 RenderTileLayer(Color, Params);
567}
568
569void CRenderLayerTile::RenderTileLayerNoTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
570{
571 Graphics()->BlendNone();
572 RenderMap()->RenderTilemap(pTiles: m_pTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_OPAQUE);
573 Graphics()->BlendNormal();
574 RenderMap()->RenderTilemap(pTiles: m_pTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_TRANSPARENT);
575}
576
577void CRenderLayerTile::Init()
578{
579 if(m_pLayerTilemap->m_Image >= 0 && m_pLayerTilemap->m_Image < m_pMapImages->Num())
580 m_TextureHandle = m_pMapImages->Get(Index: m_pLayerTilemap->m_Image);
581 else
582 m_TextureHandle.Invalidate();
583 UploadTileData(VisualsOptional&: m_VisualTiles, CurOverlay: 0, AddAsSpeedup: false);
584}
585
586void CRenderLayerTile::UploadTileData(std::optional<CTileLayerVisuals> &VisualsOptional, int CurOverlay, bool AddAsSpeedup, bool IsGameLayer)
587{
588 if(!Graphics()->IsTileBufferingEnabled())
589 return;
590
591 // prepare all visuals for all tile layers
592 std::vector<CGraphicTile> vTmpTiles;
593 std::vector<CGraphicTileTextureCoords> vTmpTileTexCoords;
594 std::vector<CGraphicTile> vTmpBorderTopTiles;
595 std::vector<CGraphicTileTextureCoords> vTmpBorderTopTilesTexCoords;
596 std::vector<CGraphicTile> vTmpBorderLeftTiles;
597 std::vector<CGraphicTileTextureCoords> vTmpBorderLeftTilesTexCoords;
598 std::vector<CGraphicTile> vTmpBorderRightTiles;
599 std::vector<CGraphicTileTextureCoords> vTmpBorderRightTilesTexCoords;
600 std::vector<CGraphicTile> vTmpBorderBottomTiles;
601 std::vector<CGraphicTileTextureCoords> vTmpBorderBottomTilesTexCoords;
602 std::vector<CGraphicTile> vTmpBorderCorners;
603 std::vector<CGraphicTileTextureCoords> vTmpBorderCornersTexCoords;
604
605 const bool DoTextureCoords = GetTexture().IsValid();
606
607 // create the visual and set it in the optional, afterwards get it
608 CTileLayerVisuals v;
609 v.OnInit(pRenderComponent: this);
610 VisualsOptional = v;
611 CTileLayerVisuals &Visuals = VisualsOptional.value();
612
613 if(!Visuals.Init(Width: m_pLayerTilemap->m_Width, Height: m_pLayerTilemap->m_Height))
614 return;
615
616 Visuals.m_IsTextured = DoTextureCoords;
617
618 if(!DoTextureCoords)
619 {
620 vTmpTiles.reserve(n: (size_t)m_pLayerTilemap->m_Width * m_pLayerTilemap->m_Height);
621 vTmpBorderTopTiles.reserve(n: (size_t)m_pLayerTilemap->m_Width);
622 vTmpBorderBottomTiles.reserve(n: (size_t)m_pLayerTilemap->m_Width);
623 vTmpBorderLeftTiles.reserve(n: (size_t)m_pLayerTilemap->m_Height);
624 vTmpBorderRightTiles.reserve(n: (size_t)m_pLayerTilemap->m_Height);
625 vTmpBorderCorners.reserve(n: (size_t)4);
626 }
627 else
628 {
629 vTmpTileTexCoords.reserve(n: (size_t)m_pLayerTilemap->m_Width * m_pLayerTilemap->m_Height);
630 vTmpBorderTopTilesTexCoords.reserve(n: (size_t)m_pLayerTilemap->m_Width);
631 vTmpBorderBottomTilesTexCoords.reserve(n: (size_t)m_pLayerTilemap->m_Width);
632 vTmpBorderLeftTilesTexCoords.reserve(n: (size_t)m_pLayerTilemap->m_Height);
633 vTmpBorderRightTilesTexCoords.reserve(n: (size_t)m_pLayerTilemap->m_Height);
634 vTmpBorderCornersTexCoords.reserve(n: (size_t)4);
635 }
636
637 int DrawLeft = m_pLayerTilemap->m_Width;
638 int DrawRight = 0;
639 int DrawTop = m_pLayerTilemap->m_Height;
640 int DrawBottom = 0;
641
642 int x = 0;
643 int y = 0;
644 for(y = 0; y < m_pLayerTilemap->m_Height; ++y)
645 {
646 for(x = 0; x < m_pLayerTilemap->m_Width; ++x)
647 {
648 unsigned char Index = 0;
649 unsigned char Flags = 0;
650 int AngleRotate = -1;
651 GetTileData(pIndex: &Index, pFlags: &Flags, pAngleRotate: &AngleRotate, x, y, CurOverlay);
652
653 // the amount of tiles handled before this tile
654 int TilesHandledCount = vTmpTiles.size();
655 Visuals.m_vTilesOfLayer[y * m_pLayerTilemap->m_Width + x].SetIndexBufferByteOffset((offset_ptr32)(TilesHandledCount));
656
657 if(AddTile(vTmpTiles, vTmpTileTexCoords, Index, Flags, x, y, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate))
658 {
659 Visuals.m_vTilesOfLayer[y * m_pLayerTilemap->m_Width + x].Draw(SetDraw: true);
660
661 // calculate clip region boundaries based on draws
662 DrawLeft = std::min(a: DrawLeft, b: x);
663 DrawRight = std::max(a: DrawRight, b: x);
664 DrawTop = std::min(a: DrawTop, b: y);
665 DrawBottom = std::max(a: DrawBottom, b: y);
666 }
667
668 // do the border tiles
669 if(x == 0)
670 {
671 if(y == 0)
672 {
673 Visuals.m_BorderTopLeft.SetIndexBufferByteOffset((offset_ptr32)(vTmpBorderCorners.size()));
674 if(AddTile(vTmpTiles&: vTmpBorderCorners, vTmpTileTexCoords&: vTmpBorderCornersTexCoords, Index, Flags, x: 0, y: 0, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate, Offset: ivec2{-32, -32}))
675 Visuals.m_BorderTopLeft.Draw(SetDraw: true);
676 }
677 else if(y == m_pLayerTilemap->m_Height - 1)
678 {
679 Visuals.m_BorderBottomLeft.SetIndexBufferByteOffset((offset_ptr32)(vTmpBorderCorners.size()));
680 if(AddTile(vTmpTiles&: vTmpBorderCorners, vTmpTileTexCoords&: vTmpBorderCornersTexCoords, Index, Flags, x: 0, y: 0, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate, Offset: ivec2{-32, 0}))
681 Visuals.m_BorderBottomLeft.Draw(SetDraw: true);
682 }
683 Visuals.m_vBorderLeft[y].SetIndexBufferByteOffset((offset_ptr32)(vTmpBorderLeftTiles.size()));
684 if(AddTile(vTmpTiles&: vTmpBorderLeftTiles, vTmpTileTexCoords&: vTmpBorderLeftTilesTexCoords, Index, Flags, x: 0, y, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate, Offset: ivec2{-32, 0}))
685 Visuals.m_vBorderLeft[y].Draw(SetDraw: true);
686 }
687 else if(x == m_pLayerTilemap->m_Width - 1)
688 {
689 if(y == 0)
690 {
691 Visuals.m_BorderTopRight.SetIndexBufferByteOffset((offset_ptr32)(vTmpBorderCorners.size()));
692 if(AddTile(vTmpTiles&: vTmpBorderCorners, vTmpTileTexCoords&: vTmpBorderCornersTexCoords, Index, Flags, x: 0, y: 0, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate, Offset: ivec2{0, -32}))
693 Visuals.m_BorderTopRight.Draw(SetDraw: true);
694 }
695 else if(y == m_pLayerTilemap->m_Height - 1)
696 {
697 Visuals.m_BorderBottomRight.SetIndexBufferByteOffset((offset_ptr32)(vTmpBorderCorners.size()));
698 if(AddTile(vTmpTiles&: vTmpBorderCorners, vTmpTileTexCoords&: vTmpBorderCornersTexCoords, Index, Flags, x: 0, y: 0, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate, Offset: ivec2{0, 0}))
699 Visuals.m_BorderBottomRight.Draw(SetDraw: true);
700 }
701 Visuals.m_vBorderRight[y].SetIndexBufferByteOffset((offset_ptr32)(vTmpBorderRightTiles.size()));
702 if(AddTile(vTmpTiles&: vTmpBorderRightTiles, vTmpTileTexCoords&: vTmpBorderRightTilesTexCoords, Index, Flags, x: 0, y, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate, Offset: ivec2{0, 0}))
703 Visuals.m_vBorderRight[y].Draw(SetDraw: true);
704 }
705 if(y == 0)
706 {
707 Visuals.m_vBorderTop[x].SetIndexBufferByteOffset((offset_ptr32)(vTmpBorderTopTiles.size()));
708 if(AddTile(vTmpTiles&: vTmpBorderTopTiles, vTmpTileTexCoords&: vTmpBorderTopTilesTexCoords, Index, Flags, x, y: 0, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate, Offset: ivec2{0, -32}))
709 Visuals.m_vBorderTop[x].Draw(SetDraw: true);
710 }
711 else if(y == m_pLayerTilemap->m_Height - 1)
712 {
713 Visuals.m_vBorderBottom[x].SetIndexBufferByteOffset((offset_ptr32)(vTmpBorderBottomTiles.size()));
714 if(AddTile(vTmpTiles&: vTmpBorderBottomTiles, vTmpTileTexCoords&: vTmpBorderBottomTilesTexCoords, Index, Flags, x, y: 0, DoTextureCoords, FillSpeedup: AddAsSpeedup, AngleRotate, Offset: ivec2{0, 0}))
715 Visuals.m_vBorderBottom[x].Draw(SetDraw: true);
716 }
717 }
718 }
719
720 // shrink clip region
721 // we only apply the clip once for the first overlay type (tile visuals). Physic layers can have multiple layers for text, e.g. speedup force
722 // the first overlay is always the largest and you will never find an overlay, where the text is written over AIR
723 if(CurOverlay == 0)
724 {
725 if(DrawLeft > DrawRight || DrawTop > DrawBottom)
726 {
727 // we are drawing nothing, layer is empty
728 m_LayerClip->m_Height = 0.0f;
729 m_LayerClip->m_Width = 0.0f;
730 }
731 else
732 {
733 m_LayerClip->m_X = DrawLeft * 32.0f;
734 m_LayerClip->m_Y = DrawTop * 32.0f;
735 m_LayerClip->m_Width = (DrawRight - DrawLeft + 1) * 32.0f;
736 m_LayerClip->m_Height = (DrawBottom - DrawTop + 1) * 32.0f;
737 }
738 }
739
740 // append one kill tile to the gamelayer
741 if(IsGameLayer)
742 {
743 Visuals.m_BorderKillTile.SetIndexBufferByteOffset((offset_ptr32)(vTmpTiles.size()));
744 if(AddTile(vTmpTiles, vTmpTileTexCoords, Index: TILE_DEATH, Flags: 0, x: 0, y: 0, DoTextureCoords))
745 Visuals.m_BorderKillTile.Draw(SetDraw: true);
746 }
747
748 // inserts and clears tiles and tile texture coords
749 auto InsertTiles = [&](std::vector<CGraphicTile> &vTiles, std::vector<CGraphicTileTextureCoords> &vTexCoords) {
750 vTmpTiles.insert(position: vTmpTiles.end(), first: vTiles.begin(), last: vTiles.end());
751 vTmpTileTexCoords.insert(position: vTmpTileTexCoords.end(), first: vTexCoords.begin(), last: vTexCoords.end());
752 vTiles.clear();
753 vTexCoords.clear();
754 };
755
756 // add the border corners, then the borders and fix their byte offsets
757 int TilesHandledCount = vTmpTiles.size();
758 Visuals.m_BorderTopLeft.AddIndexBufferByteOffset(IndexBufferByteOff: TilesHandledCount);
759 Visuals.m_BorderTopRight.AddIndexBufferByteOffset(IndexBufferByteOff: TilesHandledCount);
760 Visuals.m_BorderBottomLeft.AddIndexBufferByteOffset(IndexBufferByteOff: TilesHandledCount);
761 Visuals.m_BorderBottomRight.AddIndexBufferByteOffset(IndexBufferByteOff: TilesHandledCount);
762
763 // add the Corners to the tiles
764 InsertTiles(vTmpBorderCorners, vTmpBorderCornersTexCoords);
765
766 // now the borders
767 int TilesHandledCountTop = vTmpTiles.size();
768 int TilesHandledCountBottom = TilesHandledCountTop + vTmpBorderTopTiles.size();
769 int TilesHandledCountLeft = TilesHandledCountBottom + vTmpBorderBottomTiles.size();
770 int TilesHandledCountRight = TilesHandledCountLeft + vTmpBorderLeftTiles.size();
771
772 if(m_pLayerTilemap->m_Width > 0 && m_pLayerTilemap->m_Height > 0)
773 {
774 for(int i = 0; i < std::max(a: m_pLayerTilemap->m_Width, b: m_pLayerTilemap->m_Height); ++i)
775 {
776 if(i < m_pLayerTilemap->m_Width)
777 {
778 Visuals.m_vBorderTop[i].AddIndexBufferByteOffset(IndexBufferByteOff: TilesHandledCountTop);
779 Visuals.m_vBorderBottom[i].AddIndexBufferByteOffset(IndexBufferByteOff: TilesHandledCountBottom);
780 }
781 if(i < m_pLayerTilemap->m_Height)
782 {
783 Visuals.m_vBorderLeft[i].AddIndexBufferByteOffset(IndexBufferByteOff: TilesHandledCountLeft);
784 Visuals.m_vBorderRight[i].AddIndexBufferByteOffset(IndexBufferByteOff: TilesHandledCountRight);
785 }
786 }
787 }
788
789 InsertTiles(vTmpBorderTopTiles, vTmpBorderTopTilesTexCoords);
790 InsertTiles(vTmpBorderBottomTiles, vTmpBorderBottomTilesTexCoords);
791 InsertTiles(vTmpBorderLeftTiles, vTmpBorderLeftTilesTexCoords);
792 InsertTiles(vTmpBorderRightTiles, vTmpBorderRightTilesTexCoords);
793
794 Visuals.m_BufferContainerIndex = -1;
795
796 // upload data to gpu
797 size_t UploadDataSize = vTmpTileTexCoords.size() * sizeof(CGraphicTileTextureCoords) + vTmpTiles.size() * sizeof(CGraphicTile);
798 if(UploadDataSize == 0)
799 {
800 RenderLoading();
801 return;
802 }
803
804 void *pUploadData = malloc(size: UploadDataSize);
805
806 if(DoTextureCoords)
807 {
808 class CVertex
809 {
810 public:
811 vec2 m_Pos;
812 ubvec4 m_Tex;
813 };
814
815 static_assert(sizeof(CVertex) == sizeof(vec2) + sizeof(ubvec4)); // no padding
816
817 CVertex *pDst = static_cast<CVertex *>(pUploadData);
818 dbg_assert(UploadDataSize == vTmpTiles.size() * sizeof(*pDst) * 4, "invalid upload size");
819
820 for(size_t TileIndex = 0; TileIndex < vTmpTiles.size(); ++TileIndex)
821 {
822 const auto &GraphicTile = vTmpTiles[TileIndex];
823 const auto &GraphicCoords = vTmpTileTexCoords[TileIndex];
824
825 *pDst++ = {.m_Pos: GraphicTile.m_TopLeft, .m_Tex: GraphicCoords.m_TexCoordTopLeft};
826 *pDst++ = {.m_Pos: GraphicTile.m_TopRight, .m_Tex: GraphicCoords.m_TexCoordTopRight};
827 *pDst++ = {.m_Pos: GraphicTile.m_BottomRight, .m_Tex: GraphicCoords.m_TexCoordBottomRight};
828 *pDst++ = {.m_Pos: GraphicTile.m_BottomLeft, .m_Tex: GraphicCoords.m_TexCoordBottomLeft};
829 }
830 }
831 else
832 {
833 // we don't have texture coords, so we can optimize
834 dbg_assert(UploadDataSize == vTmpTiles.size() * sizeof(CGraphicTile), "invalid upload size");
835 mem_copy(dest: pUploadData, source: vTmpTiles.data(), size: vTmpTiles.size() * sizeof(CGraphicTile));
836 }
837
838 // first create the buffer object
839 int BufferObjectIndex = Graphics()->CreateBufferObject(UploadDataSize, pUploadData, CreateFlags: 0, IsMovedPointer: true);
840
841 // then create the buffer container
842 SBufferContainerInfo ContainerInfo;
843 ContainerInfo.m_Stride = (DoTextureCoords ? (sizeof(float) * 2 + sizeof(ubvec4)) : 0);
844 ContainerInfo.m_VertBufferBindingIndex = BufferObjectIndex;
845 ContainerInfo.m_vAttributes.emplace_back();
846 SBufferContainerInfo::SAttribute *pAttr = &ContainerInfo.m_vAttributes.back();
847 pAttr->m_DataTypeCount = 2;
848 pAttr->m_Type = GRAPHICS_TYPE_FLOAT;
849 pAttr->m_Normalized = false;
850 pAttr->m_pOffset = nullptr;
851 pAttr->m_FuncType = 0;
852 if(DoTextureCoords)
853 {
854 ContainerInfo.m_vAttributes.emplace_back();
855 pAttr = &ContainerInfo.m_vAttributes.back();
856 pAttr->m_DataTypeCount = 4;
857 pAttr->m_Type = GRAPHICS_TYPE_UNSIGNED_BYTE;
858 pAttr->m_Normalized = false;
859 pAttr->m_pOffset = (void *)(sizeof(vec2));
860 pAttr->m_FuncType = 1;
861 }
862
863 Visuals.m_BufferContainerIndex = Graphics()->CreateBufferContainer(pContainerInfo: &ContainerInfo);
864 // and finally inform the backend how many indices are required
865 Graphics()->IndicesNumRequiredNotify(RequiredIndicesCount: vTmpTiles.size() * 6);
866
867 RenderLoading();
868}
869
870void CRenderLayerTile::Unload()
871{
872 if(m_VisualTiles.has_value())
873 {
874 m_VisualTiles->Unload();
875 m_VisualTiles = std::nullopt;
876 }
877}
878
879void CRenderLayerTile::CTileLayerVisuals::Unload()
880{
881 Graphics()->DeleteBufferContainer(ContainerIndex&: m_BufferContainerIndex);
882}
883
884int CRenderLayerTile::GetDataIndex(unsigned int &TileSize) const
885{
886 TileSize = sizeof(CTile);
887 return m_pLayerTilemap->m_Data;
888}
889
890void *CRenderLayerTile::GetRawData() const
891{
892 unsigned int TileSize;
893 unsigned int DataIndex = GetDataIndex(TileSize);
894 void *pTiles = m_pMap->GetData(Index: DataIndex);
895 int Size = m_pMap->GetDataSize(Index: DataIndex);
896
897 if(!pTiles || Size < m_pLayerTilemap->m_Width * m_pLayerTilemap->m_Height * (int)TileSize)
898 return nullptr;
899
900 return pTiles;
901}
902
903void CRenderLayerTile::OnInit(IGraphics *pGraphics, ITextRender *pTextRender, CRenderMap *pRenderMap, std::shared_ptr<CEnvelopeManager> &pEnvelopeManager, IMap *pMap, IMapImages *pMapImages, std::optional<FRenderUploadCallback> &FRenderUploadCallbackOptional)
904{
905 CRenderLayer::OnInit(pGraphics, pTextRender, pRenderMap, pEnvelopeManager, pMap, pMapImages, FRenderUploadCallbackOptional);
906 InitTileData();
907 m_LayerClip = CClipRegion(0.0f, 0.0f, m_pLayerTilemap->m_Width * 32.0f, m_pLayerTilemap->m_Height * 32.0f);
908}
909
910void CRenderLayerTile::InitTileData()
911{
912 m_pTiles = GetData<CTile>();
913}
914
915template<class T>
916T *CRenderLayerTile::GetData() const
917{
918 return (T *)GetRawData();
919}
920
921void CRenderLayerTile::GetTileData(unsigned char *pIndex, unsigned char *pFlags, int *pAngleRotate, unsigned int x, unsigned int y, int CurOverlay) const
922{
923 *pIndex = m_pTiles[y * m_pLayerTilemap->m_Width + x].m_Index;
924 *pFlags = m_pTiles[y * m_pLayerTilemap->m_Width + x].m_Flags;
925}
926
927/**************
928 * Quad Layer *
929 **************/
930
931CRenderLayerQuads::CRenderLayerQuads(int GroupId, int LayerId, int Flags, CMapItemLayerQuads *pLayerQuads) :
932 CRenderLayer(GroupId, LayerId, Flags)
933{
934 m_pLayerQuads = pLayerQuads;
935 m_pQuads = nullptr;
936}
937
938void CRenderLayerQuads::RenderQuadLayer(float Alpha, const CRenderLayerParams &Params)
939{
940 CQuadLayerVisuals &Visuals = m_VisualQuad.value();
941 if(Visuals.m_BufferContainerIndex == -1)
942 return; // no visuals were created
943
944 for(auto &QuadCluster : m_vQuadClusters)
945 {
946 if(!IsVisibleInClipRegion(ClipRegion: QuadCluster.m_ClipRegion))
947 continue;
948
949 if(!QuadCluster.m_Grouped)
950 {
951 bool AnyVisible = false;
952 for(int QuadClusterId = 0; QuadClusterId < QuadCluster.m_NumQuads; ++QuadClusterId)
953 {
954 CQuad *pQuad = &m_pQuads[QuadCluster.m_StartIndex + QuadClusterId];
955
956 ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
957 if(pQuad->m_ColorEnv >= 0)
958 {
959 m_pEnvelopeManager->EnvelopeEval()->EnvelopeEval(TimeOffsetMillis: pQuad->m_ColorEnvOffset, EnvelopeIndex: pQuad->m_ColorEnv, Result&: Color, Channels: 4);
960 }
961 Color.a *= Alpha;
962
963 SQuadRenderInfo &QInfo = QuadCluster.m_vQuadRenderInfo[QuadClusterId];
964 if(Color.a < 0.0f)
965 Color.a = 0.0f;
966 QInfo.m_Color = Color;
967 const bool IsVisible = Color.a >= 0.0f;
968 AnyVisible |= IsVisible;
969
970 if(IsVisible)
971 {
972 ColorRGBA Position = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
973 m_pEnvelopeManager->EnvelopeEval()->EnvelopeEval(TimeOffsetMillis: pQuad->m_PosEnvOffset, EnvelopeIndex: pQuad->m_PosEnv, Result&: Position, Channels: 3);
974 QInfo.m_Offsets.x = Position.r;
975 QInfo.m_Offsets.y = Position.g;
976 QInfo.m_Rotation = Position.b / 180.0f * pi;
977 }
978 }
979 if(AnyVisible)
980 Graphics()->RenderQuadLayer(BufferContainerIndex: Visuals.m_BufferContainerIndex, pQuadInfo: QuadCluster.m_vQuadRenderInfo.data(), QuadNum: QuadCluster.m_NumQuads, QuadOffset: QuadCluster.m_StartIndex);
981 }
982 else
983 {
984 SQuadRenderInfo &QInfo = QuadCluster.m_vQuadRenderInfo[0];
985
986 ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
987 if(QuadCluster.m_ColorEnv >= 0)
988 {
989 m_pEnvelopeManager->EnvelopeEval()->EnvelopeEval(TimeOffsetMillis: QuadCluster.m_ColorEnvOffset, EnvelopeIndex: QuadCluster.m_ColorEnv, Result&: Color, Channels: 4);
990 }
991
992 Color.a *= Alpha;
993 if(Color.a <= 0.0f)
994 continue;
995 QInfo.m_Color = Color;
996
997 if(QuadCluster.m_PosEnv >= 0)
998 {
999 ColorRGBA Position = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f);
1000 m_pEnvelopeManager->EnvelopeEval()->EnvelopeEval(TimeOffsetMillis: QuadCluster.m_PosEnvOffset, EnvelopeIndex: QuadCluster.m_PosEnv, Result&: Position, Channels: 3);
1001
1002 QInfo.m_Offsets.x = Position.r;
1003 QInfo.m_Offsets.y = Position.g;
1004 QInfo.m_Rotation = Position.b / 180.0f * pi;
1005 }
1006 Graphics()->RenderQuadLayer(BufferContainerIndex: Visuals.m_BufferContainerIndex, pQuadInfo: &QInfo, QuadNum: (size_t)QuadCluster.m_NumQuads, QuadOffset: QuadCluster.m_StartIndex, Grouped: true);
1007 }
1008 }
1009
1010 if(Params.m_DebugRenderClusterClips)
1011 {
1012 for(auto &QuadCluster : m_vQuadClusters)
1013 {
1014 if(!IsVisibleInClipRegion(ClipRegion: QuadCluster.m_ClipRegion) || !QuadCluster.m_ClipRegion.has_value())
1015 continue;
1016
1017 char aDebugText[64];
1018 str_format(buffer: aDebugText, buffer_size: sizeof(aDebugText), format: "Group %d, quad layer %d, quad start %d, grouped %d", m_GroupId, m_LayerId, QuadCluster.m_StartIndex, QuadCluster.m_Grouped);
1019 RenderMap()->RenderDebugClip(ClipX: QuadCluster.m_ClipRegion->m_X, ClipY: QuadCluster.m_ClipRegion->m_Y, ClipW: QuadCluster.m_ClipRegion->m_Width, ClipH: QuadCluster.m_ClipRegion->m_Height, Color: ColorRGBA(1.0f, 0.0f, 1.0f, 1.0f), Zoom: Params.m_Zoom, pLabel: aDebugText);
1020 }
1021 }
1022}
1023
1024void CRenderLayerQuads::OnInit(IGraphics *pGraphics, ITextRender *pTextRender, CRenderMap *pRenderMap, std::shared_ptr<CEnvelopeManager> &pEnvelopeManager, IMap *pMap, IMapImages *pMapImages, std::optional<FRenderUploadCallback> &FRenderUploadCallbackOptional)
1025{
1026 CRenderLayer::OnInit(pGraphics, pTextRender, pRenderMap, pEnvelopeManager, pMap, pMapImages, FRenderUploadCallbackOptional);
1027 int DataSize = m_pMap->GetDataSize(Index: m_pLayerQuads->m_Data);
1028 if(m_pLayerQuads->m_NumQuads > 0 && DataSize / (int)sizeof(CQuad) >= m_pLayerQuads->m_NumQuads)
1029 m_pQuads = (CQuad *)m_pMap->GetDataSwapped(Index: m_pLayerQuads->m_Data);
1030}
1031
1032void CRenderLayerQuads::Init()
1033{
1034 if(m_pLayerQuads->m_Image >= 0 && m_pLayerQuads->m_Image < m_pMapImages->Num())
1035 m_TextureHandle = m_pMapImages->Get(Index: m_pLayerQuads->m_Image);
1036 else
1037 m_TextureHandle.Invalidate();
1038
1039 if(!Graphics()->IsQuadBufferingEnabled())
1040 return;
1041
1042 std::vector<CTmpQuad> vTmpQuads;
1043 std::vector<CTmpQuadTextured> vTmpQuadsTextured;
1044 CQuadLayerVisuals v;
1045 v.OnInit(pRenderComponent: this);
1046 m_VisualQuad = v;
1047 CQuadLayerVisuals *pQLayerVisuals = &(m_VisualQuad.value());
1048
1049 const bool Textured = m_pLayerQuads->m_Image >= 0 && m_pLayerQuads->m_Image < m_pMapImages->Num();
1050
1051 if(Textured)
1052 vTmpQuadsTextured.resize(new_size: m_pLayerQuads->m_NumQuads);
1053 else
1054 vTmpQuads.resize(new_size: m_pLayerQuads->m_NumQuads);
1055
1056 auto SetQuadRenderInfo = [&](SQuadRenderInfo &QInfo, int QuadId, bool InitInfo) {
1057 CQuad *pQuad = &m_pQuads[QuadId];
1058
1059 // init for envelopeless quad layers
1060 if(InitInfo)
1061 {
1062 QInfo.m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f);
1063 QInfo.m_Offsets.x = 0;
1064 QInfo.m_Offsets.y = 0;
1065 QInfo.m_Rotation = 0;
1066 }
1067
1068 for(int j = 0; j < 4; ++j)
1069 {
1070 int QuadIdX = j;
1071 if(j == 2)
1072 QuadIdX = 3;
1073 else if(j == 3)
1074 QuadIdX = 2;
1075 if(!Textured)
1076 {
1077 // ignore the conversion for the position coordinates
1078 vTmpQuads[QuadId].m_aVertices[j].m_X = fx2f(v: pQuad->m_aPoints[QuadIdX].x);
1079 vTmpQuads[QuadId].m_aVertices[j].m_Y = fx2f(v: pQuad->m_aPoints[QuadIdX].y);
1080 vTmpQuads[QuadId].m_aVertices[j].m_CenterX = fx2f(v: pQuad->m_aPoints[4].x);
1081 vTmpQuads[QuadId].m_aVertices[j].m_CenterY = fx2f(v: pQuad->m_aPoints[4].y);
1082 vTmpQuads[QuadId].m_aVertices[j].m_R = (unsigned char)pQuad->m_aColors[QuadIdX].r;
1083 vTmpQuads[QuadId].m_aVertices[j].m_G = (unsigned char)pQuad->m_aColors[QuadIdX].g;
1084 vTmpQuads[QuadId].m_aVertices[j].m_B = (unsigned char)pQuad->m_aColors[QuadIdX].b;
1085 vTmpQuads[QuadId].m_aVertices[j].m_A = (unsigned char)pQuad->m_aColors[QuadIdX].a;
1086 }
1087 else
1088 {
1089 // ignore the conversion for the position coordinates
1090 vTmpQuadsTextured[QuadId].m_aVertices[j].m_X = fx2f(v: pQuad->m_aPoints[QuadIdX].x);
1091 vTmpQuadsTextured[QuadId].m_aVertices[j].m_Y = fx2f(v: pQuad->m_aPoints[QuadIdX].y);
1092 vTmpQuadsTextured[QuadId].m_aVertices[j].m_CenterX = fx2f(v: pQuad->m_aPoints[4].x);
1093 vTmpQuadsTextured[QuadId].m_aVertices[j].m_CenterY = fx2f(v: pQuad->m_aPoints[4].y);
1094 vTmpQuadsTextured[QuadId].m_aVertices[j].m_U = fx2f(v: pQuad->m_aTexcoords[QuadIdX].x);
1095 vTmpQuadsTextured[QuadId].m_aVertices[j].m_V = fx2f(v: pQuad->m_aTexcoords[QuadIdX].y);
1096 vTmpQuadsTextured[QuadId].m_aVertices[j].m_R = (unsigned char)pQuad->m_aColors[QuadIdX].r;
1097 vTmpQuadsTextured[QuadId].m_aVertices[j].m_G = (unsigned char)pQuad->m_aColors[QuadIdX].g;
1098 vTmpQuadsTextured[QuadId].m_aVertices[j].m_B = (unsigned char)pQuad->m_aColors[QuadIdX].b;
1099 vTmpQuadsTextured[QuadId].m_aVertices[j].m_A = (unsigned char)pQuad->m_aColors[QuadIdX].a;
1100 }
1101 }
1102 };
1103
1104 m_vQuadClusters.clear();
1105 CQuadCluster QuadCluster;
1106
1107 // create quad clusters
1108 int QuadStart = 0;
1109 while(QuadStart < m_pLayerQuads->m_NumQuads)
1110 {
1111 QuadCluster.m_StartIndex = QuadStart;
1112 QuadCluster.m_Grouped = true;
1113 QuadCluster.m_ColorEnv = m_pQuads[QuadStart].m_ColorEnv;
1114 QuadCluster.m_ColorEnvOffset = m_pQuads[QuadStart].m_ColorEnvOffset;
1115 QuadCluster.m_PosEnv = m_pQuads[QuadStart].m_PosEnv;
1116 QuadCluster.m_PosEnvOffset = m_pQuads[QuadStart].m_PosEnvOffset;
1117
1118 int QuadOffset = 0;
1119 for(int QuadClusterId = 0; QuadClusterId < m_pLayerQuads->m_NumQuads - QuadStart; ++QuadClusterId)
1120 {
1121 const CQuad *pQuad = &m_pQuads[QuadStart + QuadClusterId];
1122 bool IsGrouped = QuadCluster.m_Grouped && pQuad->m_ColorEnv == QuadCluster.m_ColorEnv && pQuad->m_ColorEnvOffset == QuadCluster.m_ColorEnvOffset && pQuad->m_PosEnv == QuadCluster.m_PosEnv && pQuad->m_PosEnvOffset == QuadCluster.m_PosEnvOffset;
1123
1124 // we are reaching gpu batch limit, here we break and close the QuadCluster if it's ungrouped
1125 if(QuadClusterId >= (int)GRAPHICS_MAX_QUADS_RENDER_COUNT)
1126 {
1127 // expand a cluster, if it's grouped
1128 if(!IsGrouped)
1129 break;
1130 }
1131 QuadOffset++;
1132 QuadCluster.m_Grouped = IsGrouped;
1133 }
1134 QuadCluster.m_NumQuads = QuadOffset;
1135
1136 // fill cluster info
1137 if(QuadCluster.m_Grouped)
1138 {
1139 // grouped quads only need one render info, because all their envs and env offsets are equal
1140 QuadCluster.m_vQuadRenderInfo.resize(new_size: 1);
1141 for(int QuadClusterId = 0; QuadClusterId < QuadCluster.m_NumQuads; ++QuadClusterId)
1142 SetQuadRenderInfo(QuadCluster.m_vQuadRenderInfo[0], QuadCluster.m_StartIndex + QuadClusterId, QuadClusterId == 0);
1143 }
1144 else
1145 {
1146 QuadCluster.m_vQuadRenderInfo.resize(new_size: QuadCluster.m_NumQuads);
1147 for(int QuadClusterId = 0; QuadClusterId < QuadCluster.m_NumQuads; ++QuadClusterId)
1148 SetQuadRenderInfo(QuadCluster.m_vQuadRenderInfo[QuadClusterId], QuadCluster.m_StartIndex + QuadClusterId, true);
1149 }
1150
1151 CalculateClipping(QuadCluster);
1152
1153 m_vQuadClusters.push_back(x: QuadCluster);
1154 QuadStart += QuadOffset;
1155 }
1156
1157 // gpu upload
1158 size_t UploadDataSize = 0;
1159 if(Textured)
1160 UploadDataSize = vTmpQuadsTextured.size() * sizeof(CTmpQuadTextured);
1161 else
1162 UploadDataSize = vTmpQuads.size() * sizeof(CTmpQuad);
1163
1164 if(UploadDataSize > 0)
1165 {
1166 void *pUploadData = nullptr;
1167 if(Textured)
1168 pUploadData = vTmpQuadsTextured.data();
1169 else
1170 pUploadData = vTmpQuads.data();
1171 // create the buffer object
1172 int BufferObjectIndex = Graphics()->CreateBufferObject(UploadDataSize, pUploadData, CreateFlags: 0);
1173 // then create the buffer container
1174 SBufferContainerInfo ContainerInfo;
1175 ContainerInfo.m_Stride = (Textured ? (sizeof(CTmpQuadTextured) / 4) : (sizeof(CTmpQuad) / 4));
1176 ContainerInfo.m_VertBufferBindingIndex = BufferObjectIndex;
1177 ContainerInfo.m_vAttributes.emplace_back();
1178 SBufferContainerInfo::SAttribute *pAttr = &ContainerInfo.m_vAttributes.back();
1179 pAttr->m_DataTypeCount = 4;
1180 pAttr->m_Type = GRAPHICS_TYPE_FLOAT;
1181 pAttr->m_Normalized = false;
1182 pAttr->m_pOffset = nullptr;
1183 pAttr->m_FuncType = 0;
1184 ContainerInfo.m_vAttributes.emplace_back();
1185 pAttr = &ContainerInfo.m_vAttributes.back();
1186 pAttr->m_DataTypeCount = 4;
1187 pAttr->m_Type = GRAPHICS_TYPE_UNSIGNED_BYTE;
1188 pAttr->m_Normalized = true;
1189 pAttr->m_pOffset = (void *)(sizeof(float) * 4);
1190 pAttr->m_FuncType = 0;
1191 if(Textured)
1192 {
1193 ContainerInfo.m_vAttributes.emplace_back();
1194 pAttr = &ContainerInfo.m_vAttributes.back();
1195 pAttr->m_DataTypeCount = 2;
1196 pAttr->m_Type = GRAPHICS_TYPE_FLOAT;
1197 pAttr->m_Normalized = false;
1198 pAttr->m_pOffset = (void *)(sizeof(float) * 4 + sizeof(unsigned char) * 4);
1199 pAttr->m_FuncType = 0;
1200 }
1201
1202 pQLayerVisuals->m_BufferContainerIndex = Graphics()->CreateBufferContainer(pContainerInfo: &ContainerInfo);
1203 // and finally inform the backend how many indices are required
1204 Graphics()->IndicesNumRequiredNotify(RequiredIndicesCount: m_pLayerQuads->m_NumQuads * 6);
1205 }
1206 RenderLoading();
1207}
1208
1209void CRenderLayerQuads::Unload()
1210{
1211 if(m_VisualQuad.has_value())
1212 {
1213 m_VisualQuad->Unload();
1214 m_VisualQuad = std::nullopt;
1215 }
1216}
1217
1218void CRenderLayerQuads::CQuadLayerVisuals::Unload()
1219{
1220 Graphics()->DeleteBufferContainer(ContainerIndex&: m_BufferContainerIndex);
1221}
1222
1223bool CRenderLayerQuads::CalculateQuadClipping(const CQuadCluster &QuadCluster, float aQuadOffsetMin[2], float aQuadOffsetMax[2]) const
1224{
1225 // check if the grouped clipping is available for early exit
1226 if(QuadCluster.m_Grouped)
1227 {
1228 const CEnvelopeExtrema::CEnvelopeExtremaItem &Extrema = m_pEnvelopeManager->EnvelopeExtrema()->GetExtrema(EnvelopeIndex: QuadCluster.m_PosEnv);
1229 if(!Extrema.m_Available)
1230 return false;
1231 }
1232
1233 // calculate quad position offsets
1234 for(int Channel = 0; Channel < 2; ++Channel)
1235 {
1236 aQuadOffsetMin[Channel] = std::numeric_limits<float>::max(); // minimum of channel
1237 aQuadOffsetMax[Channel] = std::numeric_limits<float>::min(); // maximum of channel
1238 }
1239
1240 for(int QuadId = QuadCluster.m_StartIndex; QuadId < QuadCluster.m_StartIndex + QuadCluster.m_NumQuads; ++QuadId)
1241 {
1242 const CQuad *pQuad = &m_pQuads[QuadId];
1243
1244 const CEnvelopeExtrema::CEnvelopeExtremaItem &Extrema = m_pEnvelopeManager->EnvelopeExtrema()->GetExtrema(EnvelopeIndex: pQuad->m_PosEnv);
1245 if(!Extrema.m_Available)
1246 return false;
1247
1248 // calculate clip region
1249 if(!Extrema.m_Rotating)
1250 {
1251 for(int QuadIdPoint = 0; QuadIdPoint < 4; ++QuadIdPoint)
1252 {
1253 for(int Channel = 0; Channel < 2; ++Channel)
1254 {
1255 float OffsetMinimum = fx2f(v: pQuad->m_aPoints[QuadIdPoint][Channel]);
1256 float OffsetMaximum = fx2f(v: pQuad->m_aPoints[QuadIdPoint][Channel]);
1257
1258 // calculate env offsets for every ungrouped quad
1259 if(!QuadCluster.m_Grouped && pQuad->m_PosEnv >= 0)
1260 {
1261 OffsetMinimum += fx2f(v: Extrema.m_Minima[Channel]);
1262 OffsetMaximum += fx2f(v: Extrema.m_Maxima[Channel]);
1263 }
1264 aQuadOffsetMin[Channel] = std::min(a: aQuadOffsetMin[Channel], b: OffsetMinimum);
1265 aQuadOffsetMax[Channel] = std::max(a: aQuadOffsetMax[Channel], b: OffsetMaximum);
1266 }
1267 }
1268 }
1269 else
1270 {
1271 const CPoint &CenterFX = pQuad->m_aPoints[4];
1272 vec2 Center(fx2f(v: CenterFX.x), fx2f(v: CenterFX.y));
1273 float MaxDistance = 0;
1274 for(int QuadIdPoint = 0; QuadIdPoint < 4; ++QuadIdPoint)
1275 {
1276 const CPoint &QuadPointFX = pQuad->m_aPoints[QuadIdPoint];
1277 vec2 QuadPoint(fx2f(v: QuadPointFX.x), fx2f(v: QuadPointFX.y));
1278 float Distance = length(a: Center - QuadPoint);
1279 MaxDistance = std::max(a: Distance, b: MaxDistance);
1280 }
1281
1282 for(int Channel = 0; Channel < 2; ++Channel)
1283 {
1284 float OffsetMinimum = Center[Channel] - MaxDistance;
1285 float OffsetMaximum = Center[Channel] + MaxDistance;
1286 if(!QuadCluster.m_Grouped && pQuad->m_PosEnv >= 0)
1287 {
1288 OffsetMinimum += fx2f(v: Extrema.m_Minima[Channel]);
1289 OffsetMaximum += fx2f(v: Extrema.m_Maxima[Channel]);
1290 }
1291 aQuadOffsetMin[Channel] = std::min(a: aQuadOffsetMin[Channel], b: OffsetMinimum);
1292 aQuadOffsetMax[Channel] = std::max(a: aQuadOffsetMax[Channel], b: OffsetMaximum);
1293 }
1294 }
1295 }
1296
1297 // add env offsets for the quad group
1298 if(QuadCluster.m_Grouped && QuadCluster.m_PosEnv >= 0)
1299 {
1300 const CEnvelopeExtrema::CEnvelopeExtremaItem &Extrema = m_pEnvelopeManager->EnvelopeExtrema()->GetExtrema(EnvelopeIndex: QuadCluster.m_PosEnv);
1301
1302 for(int Channel = 0; Channel < 2; ++Channel)
1303 {
1304 aQuadOffsetMin[Channel] += fx2f(v: Extrema.m_Minima[Channel]);
1305 aQuadOffsetMax[Channel] += fx2f(v: Extrema.m_Maxima[Channel]);
1306 }
1307 }
1308 return true;
1309}
1310
1311void CRenderLayerQuads::CalculateClipping(CQuadCluster &QuadCluster)
1312{
1313 float aQuadOffsetMin[2];
1314 float aQuadOffsetMax[2];
1315
1316 bool CreateClip = CalculateQuadClipping(QuadCluster, aQuadOffsetMin, aQuadOffsetMax);
1317
1318 if(!CreateClip)
1319 return;
1320
1321 QuadCluster.m_ClipRegion = std::make_optional<CClipRegion>();
1322 std::optional<CClipRegion> &ClipRegion = QuadCluster.m_ClipRegion;
1323
1324 // X channel
1325 ClipRegion->m_X = aQuadOffsetMin[0];
1326 ClipRegion->m_Width = aQuadOffsetMax[0] - aQuadOffsetMin[0];
1327
1328 // Y channel
1329 ClipRegion->m_Y = aQuadOffsetMin[1];
1330 ClipRegion->m_Height = aQuadOffsetMax[1] - aQuadOffsetMin[1];
1331
1332 // update layer clip
1333 if(!m_LayerClip.has_value())
1334 {
1335 m_LayerClip = ClipRegion;
1336 }
1337 else
1338 {
1339 float ClipRight = std::max(a: ClipRegion->m_X + ClipRegion->m_Width, b: m_LayerClip->m_X + m_LayerClip->m_Width);
1340 float ClipBottom = std::max(a: ClipRegion->m_Y + ClipRegion->m_Height, b: m_LayerClip->m_Y + m_LayerClip->m_Height);
1341 m_LayerClip->m_X = std::min(a: ClipRegion->m_X, b: m_LayerClip->m_X);
1342 m_LayerClip->m_Y = std::min(a: ClipRegion->m_Y, b: m_LayerClip->m_Y);
1343 m_LayerClip->m_Width = ClipRight - m_LayerClip->m_X;
1344 m_LayerClip->m_Height = ClipBottom - m_LayerClip->m_Y;
1345 }
1346}
1347
1348void CRenderLayerQuads::Render(const CRenderLayerParams &Params)
1349{
1350 UseTexture(TextureHandle: GetTexture());
1351
1352 bool Force = Params.m_RenderType == ERenderType::RENDERTYPE_BACKGROUND_FORCE || Params.m_RenderType == ERenderType::RENDERTYPE_FULL_DESIGN;
1353 float Alpha = Force ? 1.f : (100 - Params.m_EntityOverlayVal) / 100.0f;
1354 if(!Graphics()->IsQuadBufferingEnabled() || !Params.m_TileAndQuadBuffering)
1355 {
1356 Graphics()->BlendNormal();
1357 RenderMap()->ForceRenderQuads(pQuads: m_pQuads, NumQuads: m_pLayerQuads->m_NumQuads, Flags: LAYERRENDERFLAG_TRANSPARENT, pEnvEval: m_pEnvelopeManager->EnvelopeEval(), Alpha);
1358 }
1359 else
1360 {
1361 RenderQuadLayer(Alpha, Params);
1362 }
1363
1364 if(Params.m_DebugRenderQuadClips && m_LayerClip.has_value())
1365 {
1366 char aDebugText[64];
1367 str_format(buffer: aDebugText, buffer_size: sizeof(aDebugText), format: "Group %d, quad layer %d", m_GroupId, m_LayerId);
1368 RenderMap()->RenderDebugClip(ClipX: m_LayerClip->m_X, ClipY: m_LayerClip->m_Y, ClipW: m_LayerClip->m_Width, ClipH: m_LayerClip->m_Height, Color: ColorRGBA(1.0f, 0.0f, 0.5f, 1.0f), Zoom: Params.m_Zoom, pLabel: aDebugText);
1369 }
1370}
1371
1372bool CRenderLayerQuads::DoRender(const CRenderLayerParams &Params)
1373{
1374 // skip rendering anything but entities if we only want to render entities
1375 if(Params.m_EntityOverlayVal == 100 && Params.m_RenderType != ERenderType::RENDERTYPE_BACKGROUND_FORCE)
1376 return false;
1377
1378 // skip rendering if detail layers if not wanted
1379 if(m_Flags & LAYERFLAG_DETAIL && !g_Config.m_GfxHighDetail && Params.m_RenderType != ERenderType::RENDERTYPE_FULL_DESIGN) // detail but no details
1380 return false;
1381
1382 // this option only deactivates quads in the background
1383 if(Params.m_RenderType == ERenderType::RENDERTYPE_BACKGROUND || Params.m_RenderType == ERenderType::RENDERTYPE_BACKGROUND_FORCE)
1384 {
1385 if(!g_Config.m_ClShowQuads)
1386 return false;
1387 }
1388
1389 return IsVisibleInClipRegion(ClipRegion: m_LayerClip);
1390}
1391
1392/****************
1393 * Entity Layer *
1394 ****************/
1395// BASE
1396CRenderLayerEntityBase::CRenderLayerEntityBase(int GroupId, int LayerId, int Flags, CMapItemLayerTilemap *pLayerTilemap) :
1397 CRenderLayerTile(GroupId, LayerId, Flags, pLayerTilemap) {}
1398
1399bool CRenderLayerEntityBase::DoRender(const CRenderLayerParams &Params)
1400{
1401 // skip rendering if we render background force or full design
1402 if(Params.m_RenderType == ERenderType::RENDERTYPE_BACKGROUND_FORCE || Params.m_RenderType == ERenderType::RENDERTYPE_FULL_DESIGN)
1403 return false;
1404
1405 // skip rendering of entities if don't want them
1406 if(!Params.m_EntityOverlayVal)
1407 return false;
1408
1409 return true;
1410}
1411
1412IGraphics::CTextureHandle CRenderLayerEntityBase::GetTexture() const
1413{
1414 return m_pMapImages->GetEntities(EntityLayerType: MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH);
1415}
1416
1417// GAME
1418CRenderLayerEntityGame::CRenderLayerEntityGame(int GroupId, int LayerId, int Flags, CMapItemLayerTilemap *pLayerTilemap) :
1419 CRenderLayerEntityBase(GroupId, LayerId, Flags, pLayerTilemap) {}
1420
1421void CRenderLayerEntityGame::Init()
1422{
1423 UploadTileData(VisualsOptional&: m_VisualTiles, CurOverlay: 0, AddAsSpeedup: false, IsGameLayer: true);
1424}
1425
1426void CRenderLayerEntityGame::RenderTileLayerWithTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1427{
1428 Graphics()->BlendNormal();
1429 if(Params.m_RenderTileBorder)
1430 RenderKillTileBorder(Color: Color.Multiply(Other: GetDeathBorderColor()));
1431 RenderTileLayer(Color, Params);
1432}
1433
1434void CRenderLayerEntityGame::RenderTileLayerNoTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1435{
1436 Graphics()->BlendNone();
1437 RenderMap()->RenderTilemap(pTiles: m_pTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_OPAQUE);
1438 Graphics()->BlendNormal();
1439
1440 if(Params.m_RenderTileBorder)
1441 {
1442 RenderMap()->RenderTileRectangle(RectX: -BorderRenderDistance, RectY: -BorderRenderDistance, RectW: m_pLayerTilemap->m_Width + 2 * BorderRenderDistance, RectH: m_pLayerTilemap->m_Height + 2 * BorderRenderDistance,
1443 IndexIn: TILE_AIR, IndexOut: TILE_DEATH, // display air inside, death outside
1444 Scale: 32.0f, Color: Color.Multiply(Other: GetDeathBorderColor()), RenderFlags: TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_TRANSPARENT);
1445 }
1446
1447 RenderMap()->RenderTilemap(pTiles: m_pTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_TRANSPARENT);
1448}
1449
1450ColorRGBA CRenderLayerEntityGame::GetDeathBorderColor() const
1451{
1452 // draw kill tiles outside the entity clipping rectangle
1453 // slow blinking to hint that it's not a part of the map
1454 float Seconds = time_get() / (float)time_freq();
1455 float Alpha = 0.3f + 0.35f * (1.f + std::sin(x: 2.f * pi * Seconds / 3.f));
1456 return ColorRGBA(1.f, 1.f, 1.f, Alpha);
1457}
1458
1459// FRONT
1460CRenderLayerEntityFront::CRenderLayerEntityFront(int GroupId, int LayerId, int Flags, CMapItemLayerTilemap *pLayerTilemap) :
1461 CRenderLayerEntityBase(GroupId, LayerId, Flags, pLayerTilemap) {}
1462
1463int CRenderLayerEntityFront::GetDataIndex(unsigned int &TileSize) const
1464{
1465 TileSize = sizeof(CTile);
1466 return m_pLayerTilemap->m_Front;
1467}
1468
1469// TELE
1470CRenderLayerEntityTele::CRenderLayerEntityTele(int GroupId, int LayerId, int Flags, CMapItemLayerTilemap *pLayerTilemap) :
1471 CRenderLayerEntityBase(GroupId, LayerId, Flags, pLayerTilemap) {}
1472
1473int CRenderLayerEntityTele::GetDataIndex(unsigned int &TileSize) const
1474{
1475 TileSize = sizeof(CTeleTile);
1476 return m_pLayerTilemap->m_Tele;
1477}
1478
1479void CRenderLayerEntityTele::Init()
1480{
1481 UploadTileData(VisualsOptional&: m_VisualTiles, CurOverlay: 0, AddAsSpeedup: false);
1482 UploadTileData(VisualsOptional&: m_VisualTeleNumbers, CurOverlay: 1, AddAsSpeedup: false);
1483}
1484
1485void CRenderLayerEntityTele::InitTileData()
1486{
1487 m_pTeleTiles = GetData<CTeleTile>();
1488}
1489
1490void CRenderLayerEntityTele::Unload()
1491{
1492 CRenderLayerTile::Unload();
1493 if(m_VisualTeleNumbers.has_value())
1494 {
1495 m_VisualTeleNumbers->Unload();
1496 m_VisualTeleNumbers = std::nullopt;
1497 }
1498}
1499
1500void CRenderLayerEntityTele::RenderTileLayerWithTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1501{
1502 Graphics()->BlendNormal();
1503 RenderTileLayer(Color, Params);
1504 if(Params.m_RenderText)
1505 {
1506 Graphics()->TextureSet(Texture: m_pMapImages->GetOverlayCenter());
1507 RenderTileLayer(Color, Params, pTileLayerVisuals: &m_VisualTeleNumbers.value());
1508 }
1509}
1510
1511void CRenderLayerEntityTele::RenderTileLayerNoTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1512{
1513 Graphics()->BlendNone();
1514 RenderMap()->RenderTelemap(pTele: m_pTeleTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_OPAQUE);
1515 Graphics()->BlendNormal();
1516 RenderMap()->RenderTelemap(pTele: m_pTeleTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_TRANSPARENT);
1517 int OverlayRenderFlags = (Params.m_RenderText ? OVERLAYRENDERFLAG_TEXT : 0) | (Params.m_RenderInvalidTiles ? OVERLAYRENDERFLAG_EDITOR : 0);
1518 RenderMap()->RenderTeleOverlay(pTele: m_pTeleTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, OverlayRenderFlags, Alpha: Color.a);
1519}
1520
1521void CRenderLayerEntityTele::GetTileData(unsigned char *pIndex, unsigned char *pFlags, int *pAngleRotate, unsigned int x, unsigned int y, int CurOverlay) const
1522{
1523 *pIndex = m_pTeleTiles[y * m_pLayerTilemap->m_Width + x].m_Type;
1524 *pFlags = 0;
1525 if(CurOverlay == 1)
1526 {
1527 if(IsTeleTileNumberUsedAny(Index: *pIndex))
1528 *pIndex = m_pTeleTiles[y * m_pLayerTilemap->m_Width + x].m_Number;
1529 else
1530 *pIndex = 0;
1531 }
1532}
1533
1534// SPEEDUP
1535CRenderLayerEntitySpeedup::CRenderLayerEntitySpeedup(int GroupId, int LayerId, int Flags, CMapItemLayerTilemap *pLayerTilemap) :
1536 CRenderLayerEntityBase(GroupId, LayerId, Flags, pLayerTilemap) {}
1537
1538IGraphics::CTextureHandle CRenderLayerEntitySpeedup::GetTexture() const
1539{
1540 return m_pMapImages->GetSpeedupArrow();
1541}
1542
1543int CRenderLayerEntitySpeedup::GetDataIndex(unsigned int &TileSize) const
1544{
1545 TileSize = sizeof(CSpeedupTile);
1546 return m_pLayerTilemap->m_Speedup;
1547}
1548
1549void CRenderLayerEntitySpeedup::Init()
1550{
1551 UploadTileData(VisualsOptional&: m_VisualTiles, CurOverlay: 0, AddAsSpeedup: true);
1552 UploadTileData(VisualsOptional&: m_VisualForce, CurOverlay: 1, AddAsSpeedup: false);
1553 UploadTileData(VisualsOptional&: m_VisualMaxSpeed, CurOverlay: 2, AddAsSpeedup: false);
1554}
1555
1556void CRenderLayerEntitySpeedup::InitTileData()
1557{
1558 m_pSpeedupTiles = GetData<CSpeedupTile>();
1559}
1560
1561void CRenderLayerEntitySpeedup::Unload()
1562{
1563 CRenderLayerTile::Unload();
1564 if(m_VisualForce.has_value())
1565 {
1566 m_VisualForce->Unload();
1567 m_VisualForce = std::nullopt;
1568 }
1569 if(m_VisualMaxSpeed.has_value())
1570 {
1571 m_VisualMaxSpeed->Unload();
1572 m_VisualMaxSpeed = std::nullopt;
1573 }
1574}
1575
1576void CRenderLayerEntitySpeedup::GetTileData(unsigned char *pIndex, unsigned char *pFlags, int *pAngleRotate, unsigned int x, unsigned int y, int CurOverlay) const
1577{
1578 *pIndex = m_pSpeedupTiles[y * m_pLayerTilemap->m_Width + x].m_Type;
1579 unsigned char Force = m_pSpeedupTiles[y * m_pLayerTilemap->m_Width + x].m_Force;
1580 unsigned char MaxSpeed = m_pSpeedupTiles[y * m_pLayerTilemap->m_Width + x].m_MaxSpeed;
1581 *pFlags = 0;
1582 *pAngleRotate = m_pSpeedupTiles[y * m_pLayerTilemap->m_Width + x].m_Angle;
1583 if((Force == 0 && *pIndex == TILE_SPEED_BOOST_OLD) || (Force == 0 && MaxSpeed == 0 && *pIndex == TILE_SPEED_BOOST) || !IsValidSpeedupTile(Index: *pIndex))
1584 *pIndex = 0;
1585 else if(CurOverlay == 1)
1586 *pIndex = Force;
1587 else if(CurOverlay == 2)
1588 *pIndex = MaxSpeed;
1589}
1590
1591void CRenderLayerEntitySpeedup::RenderTileLayerWithTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1592{
1593 // draw arrow -- clamp to the edge of the arrow image
1594 Graphics()->WrapClamp();
1595 UseTexture(TextureHandle: GetTexture());
1596 RenderTileLayer(Color, Params);
1597 Graphics()->WrapNormal();
1598
1599 if(Params.m_RenderText)
1600 {
1601 Graphics()->TextureSet(Texture: m_pMapImages->GetOverlayBottom());
1602 RenderTileLayer(Color, Params, pTileLayerVisuals: &m_VisualForce.value());
1603 Graphics()->TextureSet(Texture: m_pMapImages->GetOverlayTop());
1604 RenderTileLayer(Color, Params, pTileLayerVisuals: &m_VisualMaxSpeed.value());
1605 }
1606}
1607
1608void CRenderLayerEntitySpeedup::RenderTileLayerNoTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1609{
1610 int OverlayRenderFlags = (Params.m_RenderText ? OVERLAYRENDERFLAG_TEXT : 0) | (Params.m_RenderInvalidTiles ? OVERLAYRENDERFLAG_EDITOR : 0);
1611 RenderMap()->RenderSpeedupOverlay(pSpeedup: m_pSpeedupTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, OverlayRenderFlags, Alpha: Color.a);
1612}
1613
1614// SWITCH
1615CRenderLayerEntitySwitch::CRenderLayerEntitySwitch(int GroupId, int LayerId, int Flags, CMapItemLayerTilemap *pLayerTilemap) :
1616 CRenderLayerEntityBase(GroupId, LayerId, Flags, pLayerTilemap) {}
1617
1618IGraphics::CTextureHandle CRenderLayerEntitySwitch::GetTexture() const
1619{
1620 return m_pMapImages->GetEntities(EntityLayerType: MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH);
1621}
1622
1623int CRenderLayerEntitySwitch::GetDataIndex(unsigned int &TileSize) const
1624{
1625 TileSize = sizeof(CSwitchTile);
1626 return m_pLayerTilemap->m_Switch;
1627}
1628
1629void CRenderLayerEntitySwitch::Init()
1630{
1631 UploadTileData(VisualsOptional&: m_VisualTiles, CurOverlay: 0, AddAsSpeedup: false);
1632 UploadTileData(VisualsOptional&: m_VisualSwitchNumberTop, CurOverlay: 1, AddAsSpeedup: false);
1633 UploadTileData(VisualsOptional&: m_VisualSwitchNumberBottom, CurOverlay: 2, AddAsSpeedup: false);
1634}
1635
1636void CRenderLayerEntitySwitch::InitTileData()
1637{
1638 m_pSwitchTiles = GetData<CSwitchTile>();
1639}
1640
1641void CRenderLayerEntitySwitch::Unload()
1642{
1643 CRenderLayerTile::Unload();
1644 if(m_VisualSwitchNumberTop.has_value())
1645 {
1646 m_VisualSwitchNumberTop->Unload();
1647 m_VisualSwitchNumberTop = std::nullopt;
1648 }
1649 if(m_VisualSwitchNumberBottom.has_value())
1650 {
1651 m_VisualSwitchNumberBottom->Unload();
1652 m_VisualSwitchNumberBottom = std::nullopt;
1653 }
1654}
1655
1656void CRenderLayerEntitySwitch::GetTileData(unsigned char *pIndex, unsigned char *pFlags, int *pAngleRotate, unsigned int x, unsigned int y, int CurOverlay) const
1657{
1658 *pFlags = 0;
1659 *pIndex = m_pSwitchTiles[y * m_pLayerTilemap->m_Width + x].m_Type;
1660 if(CurOverlay == 0)
1661 {
1662 *pFlags = m_pSwitchTiles[y * m_pLayerTilemap->m_Width + x].m_Flags;
1663 if(*pIndex == TILE_SWITCHTIMEDOPEN)
1664 *pIndex = 8;
1665 }
1666 else if(CurOverlay == 1)
1667 *pIndex = m_pSwitchTiles[y * m_pLayerTilemap->m_Width + x].m_Number;
1668 else if(CurOverlay == 2)
1669 *pIndex = m_pSwitchTiles[y * m_pLayerTilemap->m_Width + x].m_Delay;
1670}
1671
1672void CRenderLayerEntitySwitch::RenderTileLayerWithTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1673{
1674 Graphics()->BlendNormal();
1675 RenderTileLayer(Color, Params);
1676 if(Params.m_RenderText)
1677 {
1678 Graphics()->TextureSet(Texture: m_pMapImages->GetOverlayTop());
1679 RenderTileLayer(Color, Params, pTileLayerVisuals: &m_VisualSwitchNumberTop.value());
1680 Graphics()->TextureSet(Texture: m_pMapImages->GetOverlayBottom());
1681 RenderTileLayer(Color, Params, pTileLayerVisuals: &m_VisualSwitchNumberBottom.value());
1682 }
1683}
1684
1685void CRenderLayerEntitySwitch::RenderTileLayerNoTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1686{
1687 Graphics()->BlendNone();
1688 RenderMap()->RenderSwitchmap(pSwitch: m_pSwitchTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_OPAQUE);
1689 Graphics()->BlendNormal();
1690 RenderMap()->RenderSwitchmap(pSwitch: m_pSwitchTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_TRANSPARENT);
1691 int OverlayRenderFlags = (Params.m_RenderText ? OVERLAYRENDERFLAG_TEXT : 0) | (Params.m_RenderInvalidTiles ? OVERLAYRENDERFLAG_EDITOR : 0);
1692 RenderMap()->RenderSwitchOverlay(pSwitch: m_pSwitchTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, OverlayRenderFlags, Alpha: Color.a);
1693}
1694
1695// TUNE
1696CRenderLayerEntityTune::CRenderLayerEntityTune(int GroupId, int LayerId, int Flags, CMapItemLayerTilemap *pLayerTilemap) :
1697 CRenderLayerEntityBase(GroupId, LayerId, Flags, pLayerTilemap) {}
1698
1699void CRenderLayerEntityTune::GetTileData(unsigned char *pIndex, unsigned char *pFlags, int *pAngleRotate, unsigned int x, unsigned int y, int CurOverlay) const
1700{
1701 *pIndex = m_pTuneTiles[y * m_pLayerTilemap->m_Width + x].m_Type;
1702 *pFlags = 0;
1703}
1704
1705int CRenderLayerEntityTune::GetDataIndex(unsigned int &TileSize) const
1706{
1707 TileSize = sizeof(CTuneTile);
1708 return m_pLayerTilemap->m_Tune;
1709}
1710
1711void CRenderLayerEntityTune::InitTileData()
1712{
1713 m_pTuneTiles = GetData<CTuneTile>();
1714}
1715
1716void CRenderLayerEntityTune::RenderTileLayerNoTileBuffer(const ColorRGBA &Color, const CRenderLayerParams &Params)
1717{
1718 Graphics()->BlendNone();
1719 RenderMap()->RenderTunemap(pTune: m_pTuneTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_OPAQUE);
1720 Graphics()->BlendNormal();
1721 RenderMap()->RenderTunemap(pTune: m_pTuneTiles, w: m_pLayerTilemap->m_Width, h: m_pLayerTilemap->m_Height, Scale: 32.0f, Color, RenderFlags: (Params.m_RenderTileBorder ? TILERENDERFLAG_EXTEND : 0) | LAYERRENDERFLAG_TRANSPARENT);
1722}
1723