Repository: diasurgical/DevilutionX
Pull request #7807: Per-pixel lighting using software renderer
Author: login: StephenCWills
Created: 2025-03-03T05:01:55Z | Merged: 2025-03-04T14:29:53Z

The idea is to use the marching squares algorithm to create isolines for each of the 16 light levels using the tile-based light data from `dLight`. The contours generated by marching squares are rendered onto a "lightmap" buffer which is used to look up light levels for individual pixels during rendering.

Because this uses the data from `dLight` to generate the isolines, the result should be fundamentally similar to the existing lighting system, just with a more natural gradient. And because the renderer still uses `LightTables` to look up colors in the 8-bit palette, the color banding due to the limited color selection should also be preserved. Furthermore, by hooking into the existing lighting system this way, the code footprint was small enough that I was easily able to provide this as an option in the settings menu.

I would have done some performance testing with 3DS, but I just found out that async loading is broken on that platform so I can't even get in game. :grimacing: 

Anyway, here's a short demonstration of me using `DebugToggle` to switch back and forth on PC.

https://github.com/user-attachments/assets/6321b96a-e2d4-41aa-8677-aae020ad0677


Diff:
diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index c45f0bfbf01..ac0a2beecf8 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -90,6 +90,7 @@ set(libdevilutionx_SRCS
 
   engine/render/automap_render.cpp
   engine/render/dun_render.cpp
+  engine/render/light_render.cpp
   engine/render/primitive_render.cpp
   engine/render/scrollrt.cpp
   engine/render/text_render.cpp
diff --git a/Source/engine/render/blit_impl.hpp b/Source/engine/render/blit_impl.hpp
index f352b5a19fd..9b0f9376400 100644
--- a/Source/engine/render/blit_impl.hpp
+++ b/Source/engine/render/blit_impl.hpp
@@ -6,6 +6,7 @@
 #include <version>
 
 #include "engine/palette.h"
+#include "engine/render/light_render.hpp"
 #include "utils/attributes.h"
 
 namespace devilution {
@@ -64,6 +65,37 @@ struct BlitWithMap {
 	}
 };
 
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillWithLightmap(uint8_t *dst, unsigned length, uint8_t color, const Lightmap &lightmap)
+{
+	DVL_ASSUME(length != 0);
+	const uint8_t *light = lightmap.getLightingAt(dst);
+	std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY light, light + length, dst, [color, &lightmap](uint8_t lightLevel) {
+		return lightmap.adjustColor(color, lightLevel);
+	});
+}
+
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitPixelsWithLightmap(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, unsigned length, const Lightmap &lightmap)
+{
+	DVL_ASSUME(length != 0);
+	const uint8_t *light = lightmap.getLightingAt(dst);
+	std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY src, src + length, light, dst, [&lightmap](uint8_t srcColor, uint8_t lightLevel) {
+		return lightmap.adjustColor(srcColor, lightLevel);
+	});
+}
+
+struct BlitWithLightmap {
+	const Lightmap &lightmap;
+
+	DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src) const
+	{
+		BlitPixelsWithLightmap(dst, src, length, lightmap);
+	}
+	DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t color, uint8_t *DVL_RESTRICT dst) const
+	{
+		BlitFillWithLightmap(dst, length, color, lightmap);
+	}
+};
+
 DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillBlended(uint8_t *dst, unsigned length, uint8_t color)
 {
 	DVL_ASSUME(length != 0);
@@ -112,4 +144,52 @@ struct BlitBlendedWithMap {
 	}
 };
 
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillBlendedWithLightmap(uint8_t *dst, unsigned length, uint8_t color, const Lightmap &lightmap)
+{
+	DVL_ASSUME(length != 0);
+	const uint8_t *light = lightmap.getLightingAt(dst);
+	std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY light, light + length, dst, dst, [color, &lightmap, pal = paletteTransparencyLookup](uint8_t lightLevel, uint8_t dstColor) {
+		uint8_t srcColor = lightmap.adjustColor(color, lightLevel);
+		return pal[srcColor][dstColor];
+	});
+}
+
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitPixelsBlendedWithLightmap(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, unsigned length, const Lightmap &lightmap)
+{
+	DVL_ASSUME(length != 0);
+	const uint8_t *light = lightmap.getLightingAt(dst);
+
+	if (length < 1024) {
+		uint8_t litSrc[1024];
+		std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY src, src + length, light, litSrc, [&lightmap](uint8_t srcColor, uint8_t lightLevel) {
+			return lightmap.adjustColor(srcColor, lightLevel);
+		});
+		std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY litSrc, litSrc + length, dst, dst, [pal = paletteTransparencyLookup](uint8_t srcColor, uint8_t dstColor) {
+			return pal[dstColor][srcColor];
+		});
+		return;
+	}
+
+	for (size_t i = 0; i < length; i++) {
+		uint8_t srcColor = src[i];
+		uint8_t dstColor = dst[i];
+		uint8_t lightLevel = light[i];
+		uint8_t litColor = lightmap.adjustColor(srcColor, lightLevel);
+		dst[i] = paletteTransparencyLookup[dstColor][litColor];
+	}
+}
+
+struct BlitBlendedWithLightmap {
+	const Lightmap &lightmap;
+
+	DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src) const
+	{
+		BlitPixelsBlendedWithLightmap(dst, src, length, lightmap);
+	}
+	DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t color, uint8_t *DVL_RESTRICT dst) const
+	{
+		BlitFillBlendedWithLightmap(dst, length, color, lightmap);
+	}
+};
+
 } // namespace devilution
diff --git a/Source/engine/render/clx_render.cpp b/Source/engine/render/clx_render.cpp
index 475a5c556ce..af2fddc4fe9 100644
--- a/Source/engine/render/clx_render.cpp
+++ b/Source/engine/render/clx_render.cpp
@@ -588,6 +588,11 @@ void ClxDrawTRN(const Surface &out, Point position, ClxSprite clx, const uint8_t
 	DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitWithMap { trn });
 }
 
+void ClxDrawWithLightmap(const Surface &out, Point position, ClxSprite clx, const Lightmap &lightmap)
+{
+	DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitWithLightmap { lightmap });
+}
+
 void ClxDrawBlended(const Surface &out, Point position, ClxSprite clx)
 {
 	DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlended {});
@@ -598,6 +603,11 @@ void ClxDrawBlendedTRN(const Surface &out, Point position, ClxSprite clx, const
 	DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlendedWithMap { trn });
 }
 
+void ClxDrawBlendedWithLightmap(const Surface &out, Point position, ClxSprite clx, const Lightmap &lightmap)
+{
+	DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlendedWithLightmap { lightmap });
+}
+
 void ClxDrawOutline(const Surface &out, uint8_t col, Point position, ClxSprite clx)
 {
 	RenderClxOutline</*SkipColorIndexZero=*/false>(out, position, clx, col);
diff --git a/Source/engine/render/clx_render.hpp b/Source/engine/render/clx_render.hpp
index d7010624d0b..62a62ca0ff2 100644
--- a/Source/engine/render/clx_render.hpp
+++ b/Source/engine/render/clx_render.hpp
@@ -16,6 +16,7 @@
 
 #include "engine/clx_sprite.hpp"
 #include "engine/point.hpp"
+#include "engine/render/light_render.hpp"
 #include "engine/surface.hpp"
 #include "lighting.h"
 
@@ -105,6 +106,15 @@ inline void ClxDrawLight(const Surface &out, Point position, ClxSprite clx, int
 	}
 }
 
+/**
+ * @brief Blit CL2 sprite, and apply lighting, to the given buffer at the given coordinates
+ * @param out Output buffer
+ * @param position Target buffer coordinate
+ * @param clx CLX frame
+ * @param lightmap Per-pixel light buffer
+ */
+void ClxDrawWithLightmap(const Surface &out, Point position, ClxSprite clx, const Lightmap &lightmap);
+
 /**
  * @brief Blit CL2 sprite, and apply lighting and transparency blending, to the given buffer at the given coordinates
  * @param out Output buffer
@@ -120,6 +130,15 @@ inline void ClxDrawLightBlended(const Surface &out, Point position, ClxSprite cl
 	}
 }
 
+/**
+ * @brief Blit CL2 sprite, and apply lighting and transparency blending, to the given buffer at the given coordinates
+ * @param out Output buffer
+ * @param position Target buffer coordinate
+ * @param clx CLX frame
+ * @param lightmap Per-pixel light buffer
+ */
+void ClxDrawBlendedWithLightmap(const Surface &out, Point position, ClxSprite clx, const Lightmap &lightmap);
+
 /**
  * Returns if cursor is within the CLX sprite (ignores shadow)
  */
diff --git a/Source/engine/render/dun_render.cpp b/Source/engine/render/dun_render.cpp
index 8ff90d2c0fd..556f9fe7c3d 100644
--- a/Source/engine/render/dun_render.cpp
+++ b/Source/engine/render/dun_render.cpp
@@ -114,19 +114,20 @@ enum class LightType : uint8_t {
 	FullyDark,
 	PartiallyLit,
 	FullyLit,
+	PerPixel,
 };
 
 template <LightType Light>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl);
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap);
 
 template <>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::FullyDark>(uint8_t *DVL_RESTRICT dst, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::FullyDark>(uint8_t *DVL_RESTRICT dst, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 	BlitFillDirect(dst, n, 0);
 }
 
 template <>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::FullyLit>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::FullyLit>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 #ifndef DEBUG_RENDER_COLOR
 	BlitPixelsDirect(dst, src, n);
@@ -136,7 +137,7 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::FullyLit>(u
 }
 
 template <>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::PartiallyLit>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::PartiallyLit>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 #ifndef DEBUG_RENDER_COLOR
 	BlitPixelsWithMap(dst, src, n, tbl);
@@ -145,30 +146,46 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::PartiallyLi
 #endif
 }
 
+template <>
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque<LightType::PerPixel>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
+{
+#ifndef DEBUG_RENDER_COLOR
+	BlitPixelsWithLightmap(dst, src, n, *lightmap);
+#else
+	BlitFillWithLightmap(dst, n, DBGCOLOR, *lightmap);
+#endif
+}
+
 #ifndef DEBUG_RENDER_COLOR
 template <LightType Light>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl);
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap);
 
 template <>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent<LightType::FullyDark>(uint8_t *DVL_RESTRICT dst, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent<LightType::FullyDark>(uint8_t *DVL_RESTRICT dst, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 	BlitFillBlended(dst, n, 0);
 }
 
 template <>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent<LightType::FullyLit>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent<LightType::FullyLit>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 	BlitPixelsBlended(dst, src, n);
 }
 
 template <>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent<LightType::PartiallyLit>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent<LightType::PartiallyLit>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 	BlitPixelsBlendedWithMap(dst, src, n, tbl);
 }
+
+template <>
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent<LightType::PerPixel>(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
+{
+	BlitPixelsBlendedWithLightmap(dst, src, n, *lightmap);
+}
 #else // DEBUG_RENDER_COLOR
 template <LightType Light>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 	for (size_t i = 0; i < n; i++) {
 		dst[i] = paletteTransparencyLookup[dst[i]][tbl[DBGCOLOR + 4]];
@@ -177,47 +194,47 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_REST
 #endif
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparentOrOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t width, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparentOrOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t width, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	if constexpr (Transparent) {
-		RenderLineTransparent<Light>(dst, src, width, tbl);
+		RenderLineTransparent<Light>(dst, src, width, tbl, lightmap);
 	} else {
-		RenderLineOpaque<Light>(dst, src, width, tbl);
+		RenderLineOpaque<Light>(dst, src, width, tbl, lightmap);
 	}
 }
 
 template <LightType Light, bool OpaqueFirst>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparentAndOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t prefixWidth, uint_fast8_t width, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparentAndOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t prefixWidth, uint_fast8_t width, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	if constexpr (OpaqueFirst) {
-		RenderLineOpaque<Light>(dst, src, prefixWidth, tbl);
-		RenderLineTransparent<Light>(dst + prefixWidth, src + prefixWidth, width - prefixWidth, tbl);
+		RenderLineOpaque<Light>(dst, src, prefixWidth, tbl, lightmap);
+		RenderLineTransparent<Light>(dst + prefixWidth, src + prefixWidth, width - prefixWidth, tbl, lightmap);
 	} else {
-		RenderLineTransparent<Light>(dst, src, prefixWidth, tbl);
-		RenderLineOpaque<Light>(dst + prefixWidth, src + prefixWidth, width - prefixWidth, tbl);
+		RenderLineTransparent<Light>(dst, src, prefixWidth, tbl, lightmap);
+		RenderLineOpaque<Light>(dst + prefixWidth, src + prefixWidth, width - prefixWidth, tbl, lightmap);
 	}
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLine(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, int8_t prefix)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLine(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, int8_t prefix)
 {
 	if constexpr (Mask == MaskType::Solid || Mask == MaskType::Transparent) {
-		RenderLineTransparentOrOpaque<Light, /*Transparent=*/Mask == MaskType::Transparent>(dst, src, n, tbl);
+		RenderLineTransparentOrOpaque<Light, /*Transparent=*/Mask == MaskType::Transparent>(dst, src, n, tbl, lightmap);
 	} else if (prefix >= static_cast<int8_t>(n)) {
 		// We std::clamp the prefix to (0, n] and avoid calling `RenderLineTransparent/Opaque` with width=0.
 		if constexpr (Mask == MaskType::Right) {
-			RenderLineOpaque<Light>(dst, src, n, tbl);
+			RenderLineOpaque<Light>(dst, src, n, tbl, lightmap);
 		} else {
-			RenderLineTransparent<Light>(dst, src, n, tbl);
+			RenderLineTransparent<Light>(dst, src, n, tbl, lightmap);
 		}
 	} else if (prefix <= 0) {
 		if constexpr (Mask == MaskType::Left) {
-			RenderLineOpaque<Light>(dst, src, n, tbl);
+			RenderLineOpaque<Light>(dst, src, n, tbl, lightmap);
 		} else {
-			RenderLineTransparent<Light>(dst, src, n, tbl);
+			RenderLineTransparent<Light>(dst, src, n, tbl, lightmap);
 		}
 	} else {
-		RenderLineTransparentAndOpaque<Light, /*OpaqueFirst=*/Mask == MaskType::Right>(dst, src, prefix, n, tbl);
+		RenderLineTransparentAndOpaque<Light, /*OpaqueFirst=*/Mask == MaskType::Right>(dst, src, prefix, n, tbl, lightmap);
 	}
 }
 
@@ -253,36 +270,36 @@ DVL_ALWAYS_INLINE bool IsFullyLit(const uint8_t *DVL_RESTRICT tbl)
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquareFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquareFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	for (auto i = 0; i < Height; ++i, dst -= dstPitch) {
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, Width, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, Width, tbl, lightmap);
 		src += Width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquareClipped(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquareClipped(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	src += clip.bottom * Height + clip.left;
 	for (auto i = 0; i < clip.height; ++i, dst -= dstPitch) {
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, clip.width, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, clip.width, tbl, lightmap);
 		src += Width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquare(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquare(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	if (clip.width == Width && clip.height == Height) {
-		RenderSquareFull<Light, Transparent>(dst, dstPitch, src, tbl);
+		RenderSquareFull<Light, Transparent>(dst, dstPitch, src, tbl, lightmap);
 	} else {
-		RenderSquareClipped<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+		RenderSquareClipped<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, unsigned height)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, unsigned height)
 {
 	int8_t prefix = InitialPrefix<Mask>;
 	DVL_ASSUME(height >= 16);
@@ -292,7 +309,7 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareFull(uint8_t *DV
 		while (drawWidth > 0) {
 			auto v = static_cast<int8_t>(*src++);
 			if (v > 0) {
-				RenderLine<Light, Mask>(dst, src, v, tbl, prefix - (Width - drawWidth));
+				RenderLine<Light, Mask>(dst, src, v, tbl, lightmap, prefix - (Width - drawWidth));
 				src += v;
 			} else {
 				v = -v;
@@ -306,7 +323,7 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareFull(uint8_t *DV
 
 template <LightType Light, MaskType Mask>
 // NOLINTNEXTLINE(readability-function-cognitive-complexity): Actually complex and has to be fast.
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareClipped(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareClipped(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const auto skipRestOfTheLine = [&src](int_fast16_t remainingWidth) {
 		while (remainingWidth > 0) {
@@ -338,7 +355,7 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareClipped(uint8_t
 			if (v > 0) {
 				if (v > remainingLeftClip) {
 					const auto overshoot = v - remainingLeftClip;
-					RenderLine<Light, Mask>(dst, src + remainingLeftClip, overshoot, tbl, prefix - (Width - remainingLeftClip));
+					RenderLine<Light, Mask>(dst, src + remainingLeftClip, overshoot, tbl, lightmap, prefix - (Width - remainingLeftClip));
 					dst += overshoot;
 					drawWidth -= overshoot;
 				}
@@ -359,13 +376,13 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareClipped(uint8_t
 			auto v = static_cast<int8_t>(*src++);
 			if (v > 0) {
 				if (v > drawWidth) {
-					RenderLine<Light, Mask>(dst, src, drawWidth, tbl, prefix - (Width - drawWidth));
+					RenderLine<Light, Mask>(dst, src, drawWidth, tbl, lightmap, prefix - (Width - drawWidth));
 					src += v;
 					dst += drawWidth;
 					drawWidth -= v;
 					break;
 				}
-				RenderLine<Light, Mask>(dst, src, v, tbl, prefix - (Width - drawWidth));
+				RenderLine<Light, Mask>(dst, src, v, tbl, lightmap, prefix - (Width - drawWidth));
 				src += v;
 			} else {
 				v = -v;
@@ -387,12 +404,12 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareClipped(uint8_t
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquare(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquare(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	if (clip.width == Width && clip.bottom == 0 && clip.top == 0) {
-		RenderTransparentSquareFull<Light, Mask>(dst, dstPitch, src, tbl, clip.height);
+		RenderTransparentSquareFull<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip.height);
 	} else {
-		RenderTransparentSquareClipped<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderTransparentSquareClipped<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
@@ -435,30 +452,30 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT std::size_t CalculateTriangleSourceSkipUpper
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleLower(uint8_t *DVL_RESTRICT &dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
-{
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 0 * dstLineOffset, src + 0, 2, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 1 * dstLineOffset, src + 2, 4, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 2 * dstLineOffset, src + 6, 6, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 3 * dstLineOffset, src + 12, 8, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 4 * dstLineOffset, src + 20, 10, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 5 * dstLineOffset, src + 30, 12, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 6 * dstLineOffset, src + 42, 14, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 7 * dstLineOffset, src + 56, 16, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 8 * dstLineOffset, src + 72, 18, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 9 * dstLineOffset, src + 90, 20, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 10 * dstLineOffset, src + 110, 22, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 11 * dstLineOffset, src + 132, 24, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 12 * dstLineOffset, src + 156, 26, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 13 * dstLineOffset, src + 182, 28, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 14 * dstLineOffset, src + 210, 30, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 15 * dstLineOffset, src + 240, 32, tbl);
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleLower(uint8_t *DVL_RESTRICT &dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
+{
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 0 * dstLineOffset, src + 0, 2, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 1 * dstLineOffset, src + 2, 4, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 2 * dstLineOffset, src + 6, 6, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 3 * dstLineOffset, src + 12, 8, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 4 * dstLineOffset, src + 20, 10, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 5 * dstLineOffset, src + 30, 12, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 6 * dstLineOffset, src + 42, 14, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 7 * dstLineOffset, src + 56, 16, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 8 * dstLineOffset, src + 72, 18, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 9 * dstLineOffset, src + 90, 20, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 10 * dstLineOffset, src + 110, 22, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 11 * dstLineOffset, src + 132, 24, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 12 * dstLineOffset, src + 156, 26, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 13 * dstLineOffset, src + 182, 28, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 14 * dstLineOffset, src + 210, 30, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 15 * dstLineOffset, src + 240, 32, tbl, lightmap);
 	src += 272;
 	dst -= 16 * dstLineOffset;
 }
 
 template <>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleLower<LightType::FullyDark, /*Transparent=*/false>(uint8_t *DVL_RESTRICT &dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT &src, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleLower<LightType::FullyDark, /*Transparent=*/false>(uint8_t *DVL_RESTRICT &dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT &src, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 	unsigned width = XStep;
 	for (unsigned i = 0; i < LowerHeight; ++i) {
@@ -470,27 +487,27 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleLower<LightType::FullyDar
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleUpper(uint8_t *DVL_RESTRICT dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
-{
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 0 * dstLineOffset, src + 0, 30, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 1 * dstLineOffset, src + 30, 28, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 2 * dstLineOffset, src + 58, 26, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 3 * dstLineOffset, src + 84, 24, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 4 * dstLineOffset, src + 108, 22, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 5 * dstLineOffset, src + 130, 20, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 6 * dstLineOffset, src + 150, 18, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 7 * dstLineOffset, src + 168, 16, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 8 * dstLineOffset, src + 184, 14, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 9 * dstLineOffset, src + 198, 12, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 10 * dstLineOffset, src + 210, 10, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 11 * dstLineOffset, src + 220, 8, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 12 * dstLineOffset, src + 228, 6, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 13 * dstLineOffset, src + 234, 4, tbl);
-	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 14 * dstLineOffset, src + 238, 2, tbl);
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleUpper(uint8_t *DVL_RESTRICT dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
+{
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 0 * dstLineOffset, src + 0, 30, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 1 * dstLineOffset, src + 30, 28, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 2 * dstLineOffset, src + 58, 26, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 3 * dstLineOffset, src + 84, 24, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 4 * dstLineOffset, src + 108, 22, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 5 * dstLineOffset, src + 130, 20, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 6 * dstLineOffset, src + 150, 18, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 7 * dstLineOffset, src + 168, 16, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 8 * dstLineOffset, src + 184, 14, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 9 * dstLineOffset, src + 198, 12, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 10 * dstLineOffset, src + 210, 10, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 11 * dstLineOffset, src + 220, 8, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 12 * dstLineOffset, src + 228, 6, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 13 * dstLineOffset, src + 234, 4, tbl, lightmap);
+	RenderLineTransparentOrOpaque<Light, Transparent>(dst - 14 * dstLineOffset, src + 238, 2, tbl, lightmap);
 }
 
 template <>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleUpper<LightType::FullyDark, /*Transparent=*/false>(uint8_t *DVL_RESTRICT dst, ptrdiff_t dstLineOffset, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleUpper<LightType::FullyDark, /*Transparent=*/false>(uint8_t *DVL_RESTRICT dst, ptrdiff_t dstLineOffset, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap)
 {
 	unsigned width = Width - XStep;
 	for (unsigned i = 0; i < TriangleUpperHeight; ++i) {
@@ -501,27 +518,27 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleUpper<LightType::FullyDar
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLower(uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLower(uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	dst += XStep * (LowerHeight - 1);
-	RenderTriangleLower<Light, Transparent>(dst, dstPitch + XStep, src, tbl);
+	RenderTriangleLower<Light, Transparent>(dst, dstPitch + XStep, src, tbl, lightmap);
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipVertical(const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipVertical(const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom);
 	dst += XStep * (LowerHeight - clipY.lowerBottom - 1);
 	const auto lowerMax = LowerHeight - clipY.lowerTop;
 	for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch + XStep) {
 		const auto width = XStep * i;
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipLeftAndVertical(int_fast16_t clipLeft, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipLeftAndVertical(int_fast16_t clipLeft, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom);
 	dst += XStep * (LowerHeight - clipY.lowerBottom - 1) - clipLeft;
@@ -531,13 +548,13 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipLeftAndVerti
 		const auto startX = Width - XStep * i;
 		const auto skip = startX < clipLeft ? clipLeft - startX : 0;
 		if (width > skip)
-			RenderLineTransparentOrOpaque<Light, Transparent>(dst + skip, src + skip, width - skip, tbl);
+			RenderLineTransparentOrOpaque<Light, Transparent>(dst + skip, src + skip, width - skip, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipRightAndVertical(int_fast16_t clipRight, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipRightAndVertical(int_fast16_t clipRight, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom);
 	dst += XStep * (LowerHeight - clipY.lowerBottom - 1);
@@ -545,40 +562,40 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipRightAndVert
 	for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch + XStep) {
 		const auto width = XStep * i;
 		if (width > clipRight)
-			RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width - clipRight, tbl);
+			RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width - clipRight, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
-	RenderLeftTriangleLower<Light, Transparent>(dst, dstPitch, src, tbl);
+	RenderLeftTriangleLower<Light, Transparent>(dst, dstPitch, src, tbl, lightmap);
 	dst += 2 * XStep;
-	RenderTriangleUpper<Light, Transparent>(dst, dstPitch - XStep, src, tbl);
+	RenderTriangleUpper<Light, Transparent>(dst, dstPitch - XStep, src, tbl, lightmap);
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY(clip);
-	RenderLeftTriangleLowerClipVertical<Light, Transparent>(clipY, dst, dstPitch, src, tbl);
+	RenderLeftTriangleLowerClipVertical<Light, Transparent>(clipY, dst, dstPitch, src, tbl, lightmap);
 	src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom);
 	dst += 2 * XStep + XStep * clipY.upperBottom;
 	const auto upperMax = TriangleUpperHeight - clipY.upperTop;
 	for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch - XStep) {
 		const auto width = Width - XStep * i;
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY(clip);
 	const int_fast16_t clipLeft = clip.left;
-	RenderLeftTriangleLowerClipLeftAndVertical<Light, Transparent>(clipLeft, clipY, dst, dstPitch, src, tbl);
+	RenderLeftTriangleLowerClipLeftAndVertical<Light, Transparent>(clipLeft, clipY, dst, dstPitch, src, tbl, lightmap);
 	src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom);
 	dst += 2 * XStep + XStep * clipY.upperBottom;
 	const auto upperMax = TriangleUpperHeight - clipY.upperTop;
@@ -586,17 +603,17 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipLeftAndVertical(u
 		const auto width = Width - XStep * i;
 		const auto startX = XStep * i;
 		const auto skip = startX < clipLeft ? clipLeft - startX : 0;
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst + skip, src + skip, width > skip ? width - skip : 0, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst + skip, src + skip, width > skip ? width - skip : 0, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY(clip);
 	const int_fast16_t clipRight = clip.right;
-	RenderLeftTriangleLowerClipRightAndVertical<Light, Transparent>(clipRight, clipY, dst, dstPitch, src, tbl);
+	RenderLeftTriangleLowerClipRightAndVertical<Light, Transparent>(clipRight, clipY, dst, dstPitch, src, tbl, lightmap);
 	src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom);
 	dst += 2 * XStep + XStep * clipY.upperBottom;
 	const auto upperMax = TriangleUpperHeight - clipY.upperTop;
@@ -604,60 +621,60 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipRightAndVertical(
 		const auto width = Width - XStep * i;
 		if (width <= clipRight)
 			break;
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width - clipRight, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width - clipRight, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangle(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangle(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	if (clip.width == Width) {
 		if (clip.height == TriangleHeight) {
-			RenderLeftTriangleFull<Light, Transparent>(dst, dstPitch, src, tbl);
+			RenderLeftTriangleFull<Light, Transparent>(dst, dstPitch, src, tbl, lightmap);
 		} else {
-			RenderLeftTriangleClipVertical<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+			RenderLeftTriangleClipVertical<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 		}
 	} else if (clip.right == 0) {
-		RenderLeftTriangleClipLeftAndVertical<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+		RenderLeftTriangleClipLeftAndVertical<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 	} else {
-		RenderLeftTriangleClipRightAndVertical<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+		RenderLeftTriangleClipRightAndVertical<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLower(uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLower(uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
-	RenderTriangleLower<Light, Transparent>(dst, dstPitch, src, tbl);
+	RenderTriangleLower<Light, Transparent>(dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipVertical(const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipVertical(const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom);
 	const auto lowerMax = LowerHeight - clipY.lowerTop;
 	for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch) {
 		const auto width = XStep * i;
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipLeftAndVertical(int_fast16_t clipLeft, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipLeftAndVertical(int_fast16_t clipLeft, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom);
 	const auto lowerMax = LowerHeight - clipY.lowerTop;
 	for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch) {
 		const auto width = XStep * i;
 		if (width > clipLeft)
-			RenderLineTransparentOrOpaque<Light, Transparent>(dst, src + clipLeft, width - clipLeft, tbl);
+			RenderLineTransparentOrOpaque<Light, Transparent>(dst, src + clipLeft, width - clipLeft, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipRightAndVertical(int_fast16_t clipRight, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipRightAndVertical(int_fast16_t clipRight, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom);
 	const auto lowerMax = LowerHeight - clipY.lowerTop;
@@ -665,94 +682,94 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipRightAndVer
 		const auto width = XStep * i;
 		const auto skip = Width - width < clipRight ? clipRight - (Width - width) : 0;
 		if (width > skip)
-			RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width - skip, tbl);
+			RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width - skip, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
-	RenderRightTriangleLower<Light, Transparent>(dst, dstPitch, src, tbl);
-	RenderTriangleUpper<Light, Transparent>(dst, dstPitch, src, tbl);
+	RenderRightTriangleLower<Light, Transparent>(dst, dstPitch, src, tbl, lightmap);
+	RenderTriangleUpper<Light, Transparent>(dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY(clip);
-	RenderRightTriangleLowerClipVertical<Light, Transparent>(clipY, dst, dstPitch, src, tbl);
+	RenderRightTriangleLowerClipVertical<Light, Transparent>(clipY, dst, dstPitch, src, tbl, lightmap);
 	src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom);
 	const auto upperMax = TriangleUpperHeight - clipY.upperTop;
 	for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) {
 		const auto width = Width - XStep * i;
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY(clip);
 	const int_fast16_t clipLeft = clip.left;
-	RenderRightTriangleLowerClipLeftAndVertical<Light, Transparent>(clipLeft, clipY, dst, dstPitch, src, tbl);
+	RenderRightTriangleLowerClipLeftAndVertical<Light, Transparent>(clipLeft, clipY, dst, dstPitch, src, tbl, lightmap);
 	src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom);
 	const auto upperMax = TriangleUpperHeight - clipY.upperTop;
 	for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) {
 		const auto width = Width - XStep * i;
 		if (width <= clipLeft)
 			break;
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src + clipLeft, width - clipLeft, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src + clipLeft, width - clipLeft, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY(clip);
 	const int_fast16_t clipRight = clip.right;
-	RenderRightTriangleLowerClipRightAndVertical<Light, Transparent>(clipRight, clipY, dst, dstPitch, src, tbl);
+	RenderRightTriangleLowerClipRightAndVertical<Light, Transparent>(clipRight, clipY, dst, dstPitch, src, tbl, lightmap);
 	src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom);
 	const auto upperMax = TriangleUpperHeight - clipY.upperTop;
 	for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) {
 		const auto width = Width - XStep * i;
 		const auto skip = Width - width < clipRight ? clipRight - (Width - width) : 0;
-		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width > skip ? width - skip : 0, tbl);
+		RenderLineTransparentOrOpaque<Light, Transparent>(dst, src, width > skip ? width - skip : 0, tbl, lightmap);
 		src += width;
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangle(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangle(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	if (clip.width == Width) {
 		if (clip.height == TriangleHeight) {
-			RenderRightTriangleFull<Light, Transparent>(dst, dstPitch, src, tbl);
+			RenderRightTriangleFull<Light, Transparent>(dst, dstPitch, src, tbl, lightmap);
 		} else {
-			RenderRightTriangleClipVertical<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+			RenderRightTriangleClipVertical<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 		}
 	} else if (clip.right == 0) {
-		RenderRightTriangleClipLeftAndVertical<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+		RenderRightTriangleClipLeftAndVertical<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 	} else {
-		RenderRightTriangleClipRightAndVertical<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+		RenderRightTriangleClipRightAndVertical<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalf(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalf(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	if constexpr (Mask == MaskType::Left || Mask == MaskType::Right) {
 		// The first line is always fully opaque.
 		// We handle it specially to avoid calling the blitter with width=0.
 		const uint8_t *srcEnd = src + Width * TrapezoidUpperHeight;
-		RenderLineOpaque<Light>(dst, src, Width, tbl);
+		RenderLineOpaque<Light>(dst, src, Width, tbl, lightmap);
 		src += Width;
 		dst -= dstPitch;
 		uint8_t prefixWidth = (PrefixIncrement<Mask> < 0 ? 32 : 0) + PrefixIncrement<Mask>;
 		do {
-			RenderLineTransparentAndOpaque<Light, /*OpaqueFirst=*/Mask == MaskType::Right>(dst, src, prefixWidth, Width, tbl);
+			RenderLineTransparentAndOpaque<Light, /*OpaqueFirst=*/Mask == MaskType::Right>(dst, src, prefixWidth, Width, tbl, lightmap);
 			prefixWidth += PrefixIncrement<Mask>;
 			src += Width;
 			dst -= dstPitch;
@@ -760,7 +777,7 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalf(uint8_t *DVL_R
 	} else { // Mask == MaskType::Solid || Mask == MaskType::Transparent
 		const uint8_t *srcEnd = src + Width * TrapezoidUpperHeight;
 		do {
-			RenderLineTransparentOrOpaque<Light, /*Transparent=*/Mask == MaskType::Transparent>(dst, src, Width, tbl);
+			RenderLineTransparentOrOpaque<Light, /*Transparent=*/Mask == MaskType::Transparent>(dst, src, Width, tbl, lightmap);
 			src += Width;
 			dst -= dstPitch;
 		} while (src != srcEnd);
@@ -768,179 +785,179 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalf(uint8_t *DVL_R
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	const auto upperMax = TrapezoidUpperHeight - clipY.upperTop;
 	int8_t prefix = InitPrefix<Mask>(clip.bottom);
 	for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) {
-		RenderLine<Light, Mask>(dst, src, Width, tbl, prefix);
+		RenderLine<Light, Mask>(dst, src, Width, tbl, lightmap, prefix);
 		src += Width;
 		prefix += PrefixIncrement<Mask>;
 	}
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipLeftAndVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipLeftAndVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	const auto upperMax = TrapezoidUpperHeight - clipY.upperTop;
 	int8_t prefix = InitPrefix<Mask>(clip.bottom);
 	for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) {
-		RenderLine<Light, Mask>(dst, src, clip.width, tbl, prefix - clip.left);
+		RenderLine<Light, Mask>(dst, src, clip.width, tbl, lightmap, prefix - clip.left);
 		src += Width;
 		prefix += PrefixIncrement<Mask>;
 	}
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipRightAndVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipRightAndVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
 	const auto upperMax = TrapezoidUpperHeight - clipY.upperTop;
 	int8_t prefix = InitPrefix<Mask>(clip.bottom);
 	for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) {
-		RenderLine<Light, Mask>(dst, src, clip.width, tbl, prefix);
+		RenderLine<Light, Mask>(dst, src, clip.width, tbl, lightmap, prefix);
 		src += Width;
 		prefix += PrefixIncrement<Mask>;
 	}
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
-	RenderLeftTriangleLower<Light, /*Transparent=*/Mask == MaskType::Transparent>(dst, dstPitch, src, tbl);
+	RenderLeftTriangleLower<Light, /*Transparent=*/Mask == MaskType::Transparent>(dst, dstPitch, src, tbl, lightmap);
 	dst += XStep;
-	RenderTrapezoidUpperHalf<Light, Mask>(dst, dstPitch, src, tbl);
+	RenderTrapezoidUpperHalf<Light, Mask>(dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY<TrapezoidUpperHeight>(clip);
-	RenderLeftTriangleLowerClipVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clipY, dst, dstPitch, src, tbl);
+	RenderLeftTriangleLowerClipVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clipY, dst, dstPitch, src, tbl, lightmap);
 	src += clipY.upperBottom * Width;
 	dst += XStep;
-	RenderTrapezoidUpperHalfClipVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl);
+	RenderTrapezoidUpperHalfClipVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY<TrapezoidUpperHeight>(clip);
-	RenderLeftTriangleLowerClipLeftAndVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clip.left, clipY, dst, dstPitch, src, tbl);
+	RenderLeftTriangleLowerClipLeftAndVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clip.left, clipY, dst, dstPitch, src, tbl, lightmap);
 	src += clipY.upperBottom * Width + clip.left;
 	dst += XStep + clip.left;
-	RenderTrapezoidUpperHalfClipLeftAndVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl);
+	RenderTrapezoidUpperHalfClipLeftAndVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY<TrapezoidUpperHeight>(clip);
-	RenderLeftTriangleLowerClipRightAndVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clip.right, clipY, dst, dstPitch, src, tbl);
+	RenderLeftTriangleLowerClipRightAndVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clip.right, clipY, dst, dstPitch, src, tbl, lightmap);
 	src += clipY.upperBottom * Width;
 	dst += XStep;
-	RenderTrapezoidUpperHalfClipRightAndVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl);
+	RenderTrapezoidUpperHalfClipRightAndVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoid(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoid(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	if (clip.width == Width) {
 		if (clip.height == Height) {
-			RenderLeftTrapezoidFull<Light, Mask>(dst, dstPitch, src, tbl);
+			RenderLeftTrapezoidFull<Light, Mask>(dst, dstPitch, src, tbl, lightmap);
 		} else {
-			RenderLeftTrapezoidClipVertical<Light, Mask>(dst, dstPitch, src, tbl, clip);
+			RenderLeftTrapezoidClipVertical<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 		}
 	} else if (clip.right == 0) {
-		RenderLeftTrapezoidClipLeftAndVertical<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderLeftTrapezoidClipLeftAndVertical<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 	} else {
-		RenderLeftTrapezoidClipRightAndVertical<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderLeftTrapezoidClipRightAndVertical<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap)
 {
-	RenderRightTriangleLower<Light, /*Transparent=*/Mask == MaskType::Transparent>(dst, dstPitch, src, tbl);
-	RenderTrapezoidUpperHalf<Light, Mask>(dst, dstPitch, src, tbl);
+	RenderRightTriangleLower<Light, /*Transparent=*/Mask == MaskType::Transparent>(dst, dstPitch, src, tbl, lightmap);
+	RenderTrapezoidUpperHalf<Light, Mask>(dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY<TrapezoidUpperHeight>(clip);
-	RenderRightTriangleLowerClipVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clipY, dst, dstPitch, src, tbl);
+	RenderRightTriangleLowerClipVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clipY, dst, dstPitch, src, tbl, lightmap);
 	src += clipY.upperBottom * Width;
-	RenderTrapezoidUpperHalfClipVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl);
+	RenderTrapezoidUpperHalfClipVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY<TrapezoidUpperHeight>(clip);
-	RenderRightTriangleLowerClipLeftAndVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clip.left, clipY, dst, dstPitch, src, tbl);
+	RenderRightTriangleLowerClipLeftAndVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clip.left, clipY, dst, dstPitch, src, tbl, lightmap);
 	src += clipY.upperBottom * Width + clip.left;
-	RenderTrapezoidUpperHalfClipLeftAndVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl);
+	RenderTrapezoidUpperHalfClipLeftAndVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	const DiamondClipY clipY = CalculateDiamondClipY<TrapezoidUpperHeight>(clip);
-	RenderRightTriangleLowerClipRightAndVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clip.right, clipY, dst, dstPitch, src, tbl);
+	RenderRightTriangleLowerClipRightAndVertical<Light, /*Transparent=*/Mask == MaskType::Transparent>(clip.right, clipY, dst, dstPitch, src, tbl, lightmap);
 	src += clipY.upperBottom * Width;
-	RenderTrapezoidUpperHalfClipRightAndVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl);
+	RenderTrapezoidUpperHalfClipRightAndVertical<Light, Mask>(clip, clipY, dst, dstPitch, src, tbl, lightmap);
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoid(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoid(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	if (clip.width == Width) {
 		if (clip.height == Height) {
-			RenderRightTrapezoidFull<Light, Mask>(dst, dstPitch, src, tbl);
+			RenderRightTrapezoidFull<Light, Mask>(dst, dstPitch, src, tbl, lightmap);
 		} else {
-			RenderRightTrapezoidClipVertical<Light, Mask>(dst, dstPitch, src, tbl, clip);
+			RenderRightTrapezoidClipVertical<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 		}
 	} else if (clip.right == 0) {
-		RenderRightTrapezoidClipLeftAndVertical<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderRightTrapezoidClipLeftAndVertical<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 	} else {
-		RenderRightTrapezoidClipRightAndVertical<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderRightTrapezoidClipRightAndVertical<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
 template <LightType Light, bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTileType(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTileType(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	switch (tile) {
 	case TileType::Square:
-		RenderSquare<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+		RenderSquare<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	case TileType::TransparentSquare:
-		RenderTransparentSquare<Light, Transparent ? MaskType::Transparent : MaskType::Solid>(dst, dstPitch, src, tbl, clip);
+		RenderTransparentSquare<Light, Transparent ? MaskType::Transparent : MaskType::Solid>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	case TileType::LeftTriangle:
-		RenderLeftTriangle<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+		RenderLeftTriangle<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	case TileType::RightTriangle:
-		RenderRightTriangle<Light, Transparent>(dst, dstPitch, src, tbl, clip);
+		RenderRightTriangle<Light, Transparent>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	case TileType::LeftTrapezoid:
-		RenderLeftTrapezoid<Light, Transparent ? MaskType::Transparent : MaskType::Solid>(dst, dstPitch, src, tbl, clip);
+		RenderLeftTrapezoid<Light, Transparent ? MaskType::Transparent : MaskType::Solid>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	case TileType::RightTrapezoid:
-		RenderRightTrapezoid<Light, Transparent ? MaskType::Transparent : MaskType::Solid>(dst, dstPitch, src, tbl, clip);
+		RenderRightTrapezoid<Light, Transparent ? MaskType::Transparent : MaskType::Solid>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	}
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidOrTransparentSquare(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidOrTransparentSquare(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	switch (tile) {
 	case TileType::TransparentSquare:
-		RenderTransparentSquare<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderTransparentSquare<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	case TileType::LeftTrapezoid:
-		RenderLeftTrapezoid<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderLeftTrapezoid<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	default:
 		app_fatal("Given mask can only be applied to TransparentSquare or LeftTrapezoid tiles");
@@ -948,14 +965,14 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidOrTransparentSquare(
 }
 
 template <LightType Light, MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidOrTransparentSquare(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidOrTransparentSquare(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
 	switch (tile) {
 	case TileType::TransparentSquare:
-		RenderTransparentSquare<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderTransparentSquare<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	case TileType::RightTrapezoid:
-		RenderRightTrapezoid<Light, Mask>(dst, dstPitch, src, tbl, clip);
+		RenderRightTrapezoid<Light, Mask>(dst, dstPitch, src, tbl, lightmap, clip);
 		break;
 	default:
 		app_fatal("Given mask can only be applied to TransparentSquare or LeftTrapezoid tiles");
@@ -963,38 +980,44 @@ DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidOrTransparentSquare
 }
 
 template <MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidOrTransparentSquareDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidOrTransparentSquareDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
-	if (IsFullyDark(tbl)) {
-		RenderLeftTrapezoidOrTransparentSquare<LightType::FullyDark, Mask>(tile, dst, dstPitch, src, tbl, clip);
+	if (*GetOptions().Graphics.perPixelLighting) {
+		RenderLeftTrapezoidOrTransparentSquare<LightType::PerPixel, Mask>(tile, dst, dstPitch, src, tbl, lightmap, clip);
+	} else if (IsFullyDark(tbl)) {
+		RenderLeftTrapezoidOrTransparentSquare<LightType::FullyDark, Mask>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	} else if (IsFullyLit(tbl)) {
-		RenderLeftTrapezoidOrTransparentSquare<LightType::FullyLit, Mask>(tile, dst, dstPitch, src, tbl, clip);
+		RenderLeftTrapezoidOrTransparentSquare<LightType::FullyLit, Mask>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	} else {
-		RenderLeftTrapezoidOrTransparentSquare<LightType::PartiallyLit, Mask>(tile, dst, dstPitch, src, tbl, clip);
+		RenderLeftTrapezoidOrTransparentSquare<LightType::PartiallyLit, Mask>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
 template <MaskType Mask>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidOrTransparentSquareDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidOrTransparentSquareDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
-	if (IsFullyDark(tbl)) {
-		RenderRightTrapezoidOrTransparentSquare<LightType::FullyDark, Mask>(tile, dst, dstPitch, src, tbl, clip);
+	if (*GetOptions().Graphics.perPixelLighting) {
+		RenderRightTrapezoidOrTransparentSquare<LightType::PerPixel, Mask>(tile, dst, dstPitch, src, tbl, lightmap, clip);
+	} else if (IsFullyDark(tbl)) {
+		RenderRightTrapezoidOrTransparentSquare<LightType::FullyDark, Mask>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	} else if (IsFullyLit(tbl)) {
-		RenderRightTrapezoidOrTransparentSquare<LightType::FullyLit, Mask>(tile, dst, dstPitch, src, tbl, clip);
+		RenderRightTrapezoidOrTransparentSquare<LightType::FullyLit, Mask>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	} else {
-		RenderRightTrapezoidOrTransparentSquare<LightType::PartiallyLit, Mask>(tile, dst, dstPitch, src, tbl, clip);
+		RenderRightTrapezoidOrTransparentSquare<LightType::PartiallyLit, Mask>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
 template <bool Transparent>
-DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTileDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, Clip clip)
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTileDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip)
 {
-	if (IsFullyDark(tbl)) {
-		RenderTileType<LightType::FullyDark, Transparent>(tile, dst, dstPitch, src, tbl, clip);
+	if (*GetOptions().Graphics.perPixelLighting) {
+		RenderTileType<LightType::PerPixel, Transparent>(tile, dst, dstPitch, src, tbl, lightmap, clip);
+	} else if (IsFullyDark(tbl)) {
+		RenderTileType<LightType::FullyDark, Transparent>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	} else if (IsFullyLit(tbl)) {
-		RenderTileType<LightType::FullyLit, Transparent>(tile, dst, dstPitch, src, tbl, clip);
+		RenderTileType<LightType::FullyLit, Transparent>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	} else {
-		RenderTileType<LightType::PartiallyLit, Transparent>(tile, dst, dstPitch, src, tbl, clip);
+		RenderTileType<LightType::PartiallyLit, Transparent>(tile, dst, dstPitch, src, tbl, lightmap, clip);
 	}
 }
 
@@ -1034,7 +1057,7 @@ std::string_view MaskTypeToString(MaskType maskType)
 }
 #endif
 
-DVL_ATTRIBUTE_HOT void RenderTileFrame(const Surface &out, const Point &position, TileType tile, const uint8_t *src, int_fast16_t height,
+DVL_ATTRIBUTE_HOT void RenderTileFrame(const Surface &out, const Lightmap &lightmap, const Point &position, TileType tile, const uint8_t *src, int_fast16_t height,
     MaskType maskType, const uint8_t *tbl)
 {
 #ifdef DEBUG_RENDER_OFFSET_X
@@ -1055,16 +1078,16 @@ DVL_ATTRIBUTE_HOT void RenderTileFrame(const Surface &out, const Point &position
 
 	switch (maskType) {
 	case MaskType::Solid:
-		RenderTileDispatch</*Transparent=*/false>(tile, dst, dstPitch, src, tbl, clip);
+		RenderTileDispatch</*Transparent=*/false>(tile, dst, dstPitch, src, tbl, &lightmap, clip);
 		break;
 	case MaskType::Transparent:
-		RenderTileDispatch</*Transparent=*/true>(tile, dst, dstPitch, src, tbl, clip);
+		RenderTileDispatch</*Transparent=*/true>(tile, dst, dstPitch, src, tbl, &lightmap, clip);
 		break;
 	case MaskType::Left:
-		RenderLeftTrapezoidOrTransparentSquareDispatch<MaskType::Left>(tile, dst, dstPitch, src, tbl, clip);
+		RenderLeftTrapezoidOrTransparentSquareDispatch<MaskType::Left>(tile, dst, dstPitch, src, tbl, &lightmap, clip);
 		break;
 	case MaskType::Right:
-		RenderRightTrapezoidOrTransparentSquareDispatch<MaskType::Right>(tile, dst, dstPitch, src, tbl, clip);
+		RenderRightTrapezoidOrTransparentSquareDispatch<MaskType::Right>(tile, dst, dstPitch, src, tbl, &lightmap, clip);
 		break;
 	}
 
@@ -1095,11 +1118,11 @@ void world_draw_black_tile(const Surface &out, int sx, int sy)
 	const uint16_t dstPitch = out.pitch();
 	if (clipLeft.width > 0) {
 		uint8_t *dst = out.at(static_cast<int>(sx + clipLeft.left), static_cast<int>(sy - clipLeft.bottom));
-		RenderLeftTriangle<LightType::FullyDark, /*Transparent=*/false>(dst, dstPitch, nullptr, nullptr, clipLeft);
+		RenderLeftTriangle<LightType::FullyDark, /*Transparent=*/false>(dst, dstPitch, nullptr, nullptr, nullptr, clipLeft);
 	}
 	if (clipRight.width > 0) {
 		uint8_t *dst = out.at(static_cast<int>(sx + Width + clipRight.left), static_cast<int>(sy - clipRight.bottom));
-		RenderRightTriangle<LightType::FullyDark, /*Transparent=*/false>(dst, dstPitch, nullptr, nullptr, clipRight);
+		RenderRightTriangle<LightType::FullyDark, /*Transparent=*/false>(dst, dstPitch, nullptr, nullptr, nullptr, clipRight);
 	}
 }
 
diff --git a/Source/engine/render/dun_render.hpp b/Source/engine/render/dun_render.hpp
index 98ab8c101db..042fa48c521 100644
--- a/Source/engine/render/dun_render.hpp
+++ b/Source/engine/render/dun_render.hpp
@@ -8,6 +8,7 @@
 #include <cstdint>
 
 #include "engine/point.hpp"
+#include "engine/render/light_render.hpp"
 #include "engine/surface.hpp"
 #include "levels/dun_tile.hpp"
 #include "levels/gendung.h"
@@ -112,22 +113,23 @@ std::string_view MaskTypeToString(MaskType maskType);
 /**
  * @brief Low-level tile rendering function.
  */
-void RenderTileFrame(const Surface &out, const Point &position, TileType tile, const uint8_t *src, int_fast16_t height,
+void RenderTileFrame(const Surface &out, const Lightmap &lightmap, const Point &position, TileType tile, const uint8_t *src, int_fast16_t height,
     MaskType maskType, const uint8_t *tbl);
 
 /**
  * @brief Blit current world CEL to the given buffer
  * @param out Target buffer
+ * @param lightmap Per-pixel light buffer
  * @param position Target buffer coordinates
  * @param levelCelBlock The MIN block of the level CEL file.
  * @param maskType The mask to use,
  * @param tbl LightTable or TRN for a tile.
  */
-DVL_ALWAYS_INLINE void RenderTile(const Surface &out, const Point &position,
+DVL_ALWAYS_INLINE void RenderTile(const Surface &out, const Lightmap &lightmap, const Point &position,
     LevelCelBlock levelCelBlock, MaskType maskType, const uint8_t *tbl)
 {
 	const TileType tileType = levelCelBlock.type();
-	RenderTileFrame(out, position, tileType,
+	RenderTileFrame(out, lightmap, position, tileType,
 	    GetDunFrame(levelCelBlock.frame()),
 	    (tileType == TileType::LeftTriangle || tileType == TileType::RightTriangle)
 	        ? DunFrameTriangleHeight
@@ -138,9 +140,9 @@ DVL_ALWAYS_INLINE void RenderTile(const Surface &out, const Point &position,
 /**
  * @brief Renders a floor foliage tile.
  */
-DVL_ALWAYS_INLINE void RenderTileFoliage(const Surface &out, const Point &position, LevelCelBlock levelCelBlock, const uint8_t *tbl)
+DVL_ALWAYS_INLINE void RenderTileFoliage(const Surface &out, const Lightmap &lightmap, const Point &position, LevelCelBlock levelCelBlock, const uint8_t *tbl)
 {
-	RenderTileFrame(out, Point { position.x, position.y - 16 }, TileType::TransparentSquare,
+	RenderTileFrame(out, lightmap, Point { position.x, position.y - 16 }, TileType::TransparentSquare,
 	    GetDunFrameFoliage(levelCelBlock.frame()), /*height=*/16, MaskType::Solid, tbl);
 }
 
diff --git a/Source/engine/render/light_render.cpp b/Source/engine/render/light_render.cpp
new file mode 100644
index 00000000000..e6f68c0a56b
--- /dev/null
+++ b/Source/engine/render/light_render.cpp
@@ -0,0 +1,445 @@
+#include "engine/render/light_render.hpp"
+
+#include <vector>
+
+#include "engine/displacement.hpp"
+#include "engine/point.hpp"
+#include "levels/dun_tile.hpp"
+#include "lighting.h"
+#include "options.h"
+
+namespace devilution {
+
+namespace {
+
+std::vector<uint8_t> LightmapBuffer;
+
+// Half-space method for drawing triangles
+// Points must be provided using counter-clockwise rotation
+// https://web.archive.org/web/20050408192410/http://sw-shader.sourceforge.net/rasterizer.html
+void RenderTriangle(Point p1, Point p2, Point p3, uint8_t lightLevel, uint8_t *lightmap, uint16_t pitch, uint16_t scanLines)
+{
+	// Deltas (points are already 28.4 fixed-point)
+	int dx12 = p1.x - p2.x;
+	int dx23 = p2.x - p3.x;
+	int dx31 = p3.x - p1.x;
+
+	int dy12 = p1.y - p2.y;
+	int dy23 = p2.y - p3.y;
+	int dy31 = p3.y - p1.y;
+
+	// 24.8 fixed-point deltas
+	int fdx12 = dx12 << 4;
+	int fdx23 = dx23 << 4;
+	int fdx31 = dx31 << 4;
+
+	int fdy12 = dy12 << 4;
+	int fdy23 = dy23 << 4;
+	int fdy31 = dy31 << 4;
+
+	// Bounding rectangle
+	int minx = (std::min({ p1.x, p2.x, p3.x }) + 0xF) >> 4;
+	int maxx = (std::max({ p1.x, p2.x, p3.x }) + 0xF) >> 4;
+	int miny = (std::min({ p1.y, p2.y, p3.y }) + 0xF) >> 4;
+	int maxy = (std::max({ p1.y, p2.y, p3.y }) + 0xF) >> 4;
+	minx = std::max<int>(minx, 0);
+	maxx = std::min<int>(maxx, pitch);
+	miny = std::max<int>(miny, 0);
+	maxy = std::min<int>(maxy, scanLines);
+
+	uint8_t *dst = lightmap + miny * pitch;
+
+	// Half-edge constants
+	int c1 = dy12 * p1.x - dx12 * p1.y;
+	int c2 = dy23 * p2.x - dx23 * p2.y;
+	int c3 = dy31 * p3.x - dx31 * p3.y;
+
+	// Correct for fill convention
+	if (dy12 < 0 || (dy12 == 0 && dx12 > 0)) c1++;
+	if (dy23 < 0 || (dy23 == 0 && dx23 > 0)) c2++;
+	if (dy31 < 0 || (dy31 == 0 && dx31 > 0)) c3++;
+
+	int cy1 = c1 + dx12 * (miny << 4) - dy12 * (minx << 4);
+	int cy2 = c2 + dx23 * (miny << 4) - dy23 * (minx << 4);
+	int cy3 = c3 + dx31 * (miny << 4) - dy31 * (minx << 4);
+
+	for (int y = miny; y < maxy; y++) {
+		int cx1 = cy1;
+		int cx2 = cy2;
+		int cx3 = cy3;
+
+		for (int x = minx; x < maxx; x++) {
+			if (cx1 > 0 && cx2 > 0 && cx3 > 0)
+				dst[x] = lightLevel;
+
+			cx1 -= fdy12;
+			cx2 -= fdy23;
+			cx3 -= fdy31;
+		}
+
+		cy1 += fdx12;
+		cy2 += fdx23;
+		cy3 += fdx31;
+
+		dst += pitch;
+	}
+}
+
+uint8_t Interpolate(uint8_t q1, uint8_t q2, uint8_t lightLevel)
+{
+	// Result will be 28.4 fixed-point
+	uint8_t numerator = (lightLevel - q1) << 4;
+	return (numerator + 0x8) / (q2 - q1);
+}
+
+void RenderCell(uint8_t quad[4], Point position, uint8_t lightLevel, uint8_t *lightmap, uint16_t pitch, uint16_t scanLines)
+{
+	Point center0 = position;
+	Point center1 = position + Displacement { TILE_WIDTH / 2, TILE_HEIGHT / 2 };
+	Point center2 = position + Displacement { 0, TILE_HEIGHT };
+	Point center3 = position + Displacement { -TILE_WIDTH / 2, TILE_HEIGHT / 2 };
+
+	// 28.4 fixed-point coordinates
+	Point fpCenter0 = center0 * (1 << 4);
+	Point fpCenter1 = center1 * (1 << 4);
+	Point fpCenter2 = center2 * (1 << 4);
+	Point fpCenter3 = center3 * (1 << 4);
+
+	// Marching squares
+	// https://en.wikipedia.org/wiki/Marching_squares
+	uint8_t shape = 0;
+	shape |= quad[0] <= lightLevel ? 8 : 0;
+	shape |= quad[1] <= lightLevel ? 4 : 0;
+	shape |= quad[2] <= lightLevel ? 2 : 0;
+	shape |= quad[3] <= lightLevel ? 1 : 0;
+
+	switch (shape) {
+	// The whole cell is darker than lightLevel
+	case 0: break;
+
+	// Fill in the bottom-left corner of the cell
+	// In isometric view, only the west tile of the quad is lit
+	case 1: {
+		uint8_t bottomFactor = Interpolate(quad[3], quad[2], lightLevel);
+		uint8_t leftFactor = Interpolate(quad[3], quad[0], lightLevel);
+		Point p1 = fpCenter3 + (center2 - center3) * bottomFactor;
+		Point p2 = fpCenter3;
+		Point p3 = fpCenter3 + (center0 - center3) * leftFactor;
+		RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the bottom-right corner of the cell
+	// In isometric view, only the south tile of the quad is lit
+	case 2: {
+		uint8_t rightFactor = Interpolate(quad[2], quad[1], lightLevel);
+		uint8_t bottomFactor = Interpolate(quad[2], quad[3], lightLevel);
+		Point p1 = fpCenter2 + (center1 - center2) * rightFactor;
+		Point p2 = fpCenter2;
+		Point p3 = fpCenter2 + (center3 - center2) * bottomFactor;
+		RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the bottom half of the cell
+	// In isometric view, the south and west tiles of the quad are lit
+	case 3: {
+		uint8_t rightFactor = Interpolate(quad[2], quad[1], lightLevel);
+		uint8_t leftFactor = Interpolate(quad[3], quad[0], lightLevel);
+		Point p1 = fpCenter2 + (center1 - center2) * rightFactor;
+		Point p2 = fpCenter2;
+		Point p3 = fpCenter3;
+		Point p4 = fpCenter3 + (center1 - center2) * leftFactor;
+		RenderTriangle(p1, p4, p2, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p2, p4, p3, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the top-right corner of the cell
+	// In isometric view, only the east tile of the quad is lit
+	case 4: {
+		uint8_t topFactor = Interpolate(quad[1], quad[0], lightLevel);
+		uint8_t rightFactor = Interpolate(quad[1], quad[2], lightLevel);
+		Point p1 = fpCenter1 + (center0 - center1) * topFactor;
+		Point p2 = fpCenter1;
+		Point p3 = fpCenter1 + (center2 - center1) * rightFactor;
+		RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the top-right and bottom-left corners of the cell
+	// Use the average of all values in the quad to determine whether to fill in the center
+	// In isometric view, the east and west tiles of the quad are lit
+	case 5: {
+		uint8_t cell = (quad[0] + quad[1] + quad[2] + quad[3] + 2) / 4;
+		uint8_t topFactor = Interpolate(quad[1], quad[0], lightLevel);
+		uint8_t rightFactor = Interpolate(quad[1], quad[2], lightLevel);
+		uint8_t bottomFactor = Interpolate(quad[3], quad[2], lightLevel);
+		uint8_t leftFactor = Interpolate(quad[3], quad[0], lightLevel);
+		Point p1 = fpCenter1 + (center0 - center1) * topFactor;
+		Point p2 = fpCenter1;
+		Point p3 = fpCenter1 + (center2 - center1) * rightFactor;
+		Point p4 = fpCenter3 + (center2 - center3) * bottomFactor;
+		Point p5 = fpCenter3;
+		Point p6 = fpCenter3 + (center0 - center3) * leftFactor;
+
+		if (cell <= lightLevel) {
+			uint8_t midFactor0 = Interpolate(quad[0], cell, lightLevel);
+			uint8_t midFactor2 = Interpolate(quad[2], cell, lightLevel);
+			Point p7 = fpCenter0 + (center2 - center0) / 2 * midFactor0;
+			Point p8 = fpCenter2 + (center0 - center2) / 2 * midFactor2;
+			RenderTriangle(p1, p7, p2, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p2, p7, p8, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p2, p8, p3, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p4, p8, p5, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p5, p8, p7, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p5, p7, p6, lightLevel, lightmap, pitch, scanLines);
+		} else {
+			uint8_t midFactor1 = Interpolate(quad[1], cell, lightLevel);
+			uint8_t midFactor3 = Interpolate(quad[3], cell, lightLevel);
+			Point p7 = fpCenter1 + (center3 - center1) / 2 * midFactor1;
+			Point p8 = fpCenter3 + (center1 - center3) / 2 * midFactor3;
+			RenderTriangle(p1, p7, p2, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p2, p7, p3, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p4, p8, p5, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p5, p8, p6, lightLevel, lightmap, pitch, scanLines);
+		}
+	} break;
+
+	// Fill in the right half of the cell
+	// In isometric view, the south and east tiles of the quad are lit
+	case 6: {
+		uint8_t topFactor = Interpolate(quad[1], quad[0], lightLevel);
+		uint8_t bottomFactor = Interpolate(quad[2], quad[3], lightLevel);
+		Point p1 = fpCenter1 + (center0 - center1) * topFactor;
+		Point p2 = fpCenter1;
+		Point p3 = fpCenter2;
+		Point p4 = fpCenter2 + (center3 - center2) * bottomFactor;
+		RenderTriangle(p1, p4, p2, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p2, p4, p3, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in everything except the top-left corner of the cell
+	// In isometric view, the south, east, and west tiles of the quad are lit
+	case 7: {
+		uint8_t topFactor = Interpolate(quad[1], quad[0], lightLevel);
+		uint8_t leftFactor = Interpolate(quad[3], quad[0], lightLevel);
+		Point p1 = fpCenter1 + (center0 - center1) * topFactor;
+		Point p2 = fpCenter1;
+		Point p3 = fpCenter2;
+		Point p4 = fpCenter3;
+		Point p5 = fpCenter3 + (center0 - center3) * leftFactor;
+		RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p1, p5, p3, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p3, p5, p4, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the top-left corner of the cell
+	// In isometric view, only the north tile of the quad is lit
+	case 8: {
+		uint8_t topFactor = Interpolate(quad[0], quad[1], lightLevel);
+		uint8_t leftFactor = Interpolate(quad[0], quad[3], lightLevel);
+		Point p1 = fpCenter0;
+		Point p2 = fpCenter0 + (center1 - center0) * topFactor;
+		Point p3 = fpCenter0 + (center3 - center0) * leftFactor;
+		RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the left half of the cell
+	// In isometric view, the north and west tiles of the quad are lit
+	case 9: {
+		uint8_t topFactor = Interpolate(quad[0], quad[1], lightLevel);
+		uint8_t bottomFactor = Interpolate(quad[3], quad[2], lightLevel);
+		Point p1 = fpCenter0;
+		Point p2 = fpCenter0 + (center1 - center0) * topFactor;
+		Point p3 = fpCenter3 + (center2 - center3) * bottomFactor;
+		Point p4 = fpCenter3;
+		RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p1, p4, p3, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the top-left and bottom-right corners of the cell
+	// Use the average of all values in the quad to determine whether to fill in the center
+	// In isometric view, the north and south tiles of the quad are lit
+	case 10: {
+		uint8_t cell = (quad[0] + quad[1] + quad[2] + quad[3] + 2) / 4;
+		uint8_t topFactor = Interpolate(quad[0], quad[1], lightLevel);
+		uint8_t rightFactor = Interpolate(quad[2], quad[1], lightLevel);
+		uint8_t bottomFactor = Interpolate(quad[2], quad[3], lightLevel);
+		uint8_t leftFactor = Interpolate(quad[0], quad[3], lightLevel);
+		Point p1 = fpCenter0;
+		Point p2 = fpCenter0 + (center1 - center0) * topFactor;
+		Point p3 = fpCenter2 + (center1 - center2) * rightFactor;
+		Point p4 = fpCenter2;
+		Point p5 = fpCenter2 + (center3 - center2) * bottomFactor;
+		Point p6 = fpCenter0 + (center3 - center0) * leftFactor;
+
+		if (cell <= lightLevel) {
+			uint8_t midFactor1 = Interpolate(quad[1], cell, lightLevel);
+			uint8_t midFactor3 = Interpolate(quad[3], cell, lightLevel);
+			Point p7 = fpCenter1 + (center3 - center1) / 2 * midFactor1;
+			Point p8 = fpCenter3 + (center1 - center3) / 2 * midFactor3;
+			RenderTriangle(p1, p7, p2, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p1, p6, p8, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p1, p8, p7, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p3, p7, p4, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p4, p8, p5, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p4, p7, p8, lightLevel, lightmap, pitch, scanLines);
+		} else {
+			uint8_t midFactor0 = Interpolate(quad[0], cell, lightLevel);
+			uint8_t midFactor2 = Interpolate(quad[2], cell, lightLevel);
+			Point p7 = fpCenter0 + (center2 - center0) / 2 * midFactor0;
+			Point p8 = fpCenter2 + (center0 - center2) / 2 * midFactor2;
+			RenderTriangle(p1, p7, p2, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p1, p6, p7, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p3, p8, p4, lightLevel, lightmap, pitch, scanLines);
+			RenderTriangle(p4, p8, p5, lightLevel, lightmap, pitch, scanLines);
+		}
+	} break;
+
+	// Fill in everything except the top-right corner of the cell
+	// In isometric view, the north, south, and west tiles of the quad are lit
+	case 11: {
+		uint8_t topFactor = Interpolate(quad[0], quad[1], lightLevel);
+		uint8_t rightFactor = Interpolate(quad[2], quad[1], lightLevel);
+		Point p1 = fpCenter0;
+		Point p2 = fpCenter0 + (center1 - center0) * topFactor;
+		Point p3 = fpCenter2 + (center1 - center2) * rightFactor;
+		Point p4 = fpCenter2;
+		Point p5 = fpCenter3;
+		RenderTriangle(p1, p5, p2, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p2, p5, p3, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p3, p5, p4, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the top half of the cell
+	// In isometric view, the north and east tiles of the quad are lit
+	case 12: {
+		uint8_t rightFactor = Interpolate(quad[1], quad[2], lightLevel);
+		uint8_t leftFactor = Interpolate(quad[0], quad[3], lightLevel);
+		Point p1 = fpCenter0;
+		Point p2 = fpCenter1;
+		Point p3 = fpCenter1 + (center2 - center1) * rightFactor;
+		Point p4 = fpCenter0 + (center3 - center0) * leftFactor;
+		RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p1, p4, p3, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in everything except the bottom-right corner of the cell
+	// In isometric view, the north, east, and west tiles of the quad are lit
+	case 13: {
+		uint8_t rightFactor = Interpolate(quad[1], quad[2], lightLevel);
+		uint8_t bottomFactor = Interpolate(quad[3], quad[2], lightLevel);
+		Point p1 = fpCenter0;
+		Point p2 = fpCenter1;
+		Point p3 = fpCenter1 + (center2 - center1) * rightFactor;
+		Point p4 = fpCenter3 + (center2 - center3) * bottomFactor;
+		Point p5 = fpCenter3;
+		RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p1, p4, p3, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p2, p5, p4, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in everything except the bottom-left corner of the cell
+	// In isometric view, the north, south, and east tiles of the quad are lit
+	case 14: {
+		uint8_t bottomFactor = Interpolate(quad[2], quad[3], lightLevel);
+		uint8_t leftFactor = Interpolate(quad[0], quad[3], lightLevel);
+		Point p1 = fpCenter0;
+		Point p2 = fpCenter1;
+		Point p3 = fpCenter2;
+		Point p4 = fpCenter2 + (center3 - center2) * bottomFactor;
+		Point p5 = fpCenter0 + (center3 - center0) * leftFactor;
+		RenderTriangle(p1, p5, p2, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p2, p5, p4, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(p2, p4, p3, lightLevel, lightmap, pitch, scanLines);
+	} break;
+
+	// Fill in the whole cell
+	// All four tiles in the quad are lit
+	case 15: {
+		RenderTriangle(fpCenter0, fpCenter2, fpCenter1, lightLevel, lightmap, pitch, scanLines);
+		RenderTriangle(fpCenter0, fpCenter3, fpCenter2, lightLevel, lightmap, pitch, scanLines);
+	} break;
+	}
+}
+
+void BuildLightmap(Point tilePosition, Point targetBufferPosition, uint16_t viewportWidth, uint16_t viewportHeight, int rows, int columns)
+{
+	if (!*GetOptions().Graphics.perPixelLighting)
+		return;
+
+	const size_t totalPixels = static_cast<size_t>(viewportWidth) * viewportHeight;
+	LightmapBuffer.resize(totalPixels);
+
+	// Since rendering occurs in cells between quads,
+	// expand the rendering space to include tiles outside the viewport
+	tilePosition += Displacement(Direction::NorthWest) * 2;
+	targetBufferPosition -= Displacement { TILE_WIDTH, TILE_HEIGHT };
+	rows += 3;
+	columns++;
+
+	uint8_t *lightmap = LightmapBuffer.data();
+	memset(lightmap, LightsMax, totalPixels);
+	for (int i = 0; i < rows; i++) {
+		for (int j = 0; j < columns; j++, tilePosition += Direction::East, targetBufferPosition.x += TILE_WIDTH) {
+			Point center0 = targetBufferPosition + Displacement { TILE_WIDTH / 2, -TILE_HEIGHT / 2 };
+
+			Point tile0 = tilePosition;
+			Point tile1 = tilePosition + Displacement { 1, 0 };
+			Point tile2 = tilePosition + Displacement { 1, 1 };
+			Point tile3 = tilePosition + Displacement { 0, 1 };
+
+			uint8_t quad[] = {
+				InDungeonBounds(tile0) ? dLight[tile0.x][tile0.y] : static_cast<uint8_t>(LightsMax),
+				InDungeonBounds(tile1) ? dLight[tile1.x][tile1.y] : static_cast<uint8_t>(LightsMax),
+				InDungeonBounds(tile2) ? dLight[tile2.x][tile2.y] : static_cast<uint8_t>(LightsMax),
+				InDungeonBounds(tile3) ? dLight[tile3.x][tile3.y] : static_cast<uint8_t>(LightsMax)
+			};
+
+			uint8_t maxLight = std::max({ quad[0], quad[1], quad[2], quad[3] });
+			uint8_t minLight = std::min({ quad[0], quad[1], quad[2], quad[3] });
+
+			for (uint8_t i = 0; i < LightsMax; i++) {
+				uint8_t lightLevel = LightsMax - i - 1;
+				if (lightLevel > maxLight)
+					continue;
+				if (lightLevel < minLight)
+					break;
+				RenderCell(quad, center0, lightLevel, lightmap, viewportWidth, viewportHeight);
+			}
+		}
+
+		// Return to start of row
+		tilePosition += Displacement(Direction::West) * columns;
+		targetBufferPosition.x -= columns * TILE_WIDTH;
+
+		// Jump to next row
+		targetBufferPosition.y += TILE_HEIGHT / 2;
+		if ((i & 1) != 0) {
+			tilePosition.x++;
+			columns--;
+			targetBufferPosition.x += TILE_WIDTH / 2;
+		} else {
+			tilePosition.y++;
+			columns++;
+			targetBufferPosition.x -= TILE_WIDTH / 2;
+		}
+	}
+}
+
+} // namespace
+
+Lightmap::Lightmap(const uint8_t *outBuffer, const uint8_t *lightmapBuffer, const uint8_t *lightTables, size_t lightTableSize)
+    : outBuffer(outBuffer)
+    , lightmapBuffer(lightmapBuffer)
+    , lightTables(lightTables)
+    , lightTableSize(lightTableSize)
+{
+}
+
+Lightmap Lightmap::build(Point tilePosition, Point targetBufferPosition,
+    int viewportWidth, int viewportHeight, int rows, int columns,
+    const uint8_t *outBuffer, const uint8_t *lightTables, size_t lightTableSize)
+{
+	BuildLightmap(tilePosition, targetBufferPosition, viewportWidth, viewportHeight, rows, columns);
+	return Lightmap(outBuffer, LightmapBuffer.data(), lightTables, lightTableSize);
+}
+
+} // namespace devilution
diff --git a/Source/engine/render/light_render.hpp b/Source/engine/render/light_render.hpp
new file mode 100644
index 00000000000..cfd6e5ea52d
--- /dev/null
+++ b/Source/engine/render/light_render.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "engine/point.hpp"
+
+namespace devilution {
+
+class Lightmap {
+public:
+	explicit Lightmap(const uint8_t *outBuffer, const uint8_t *lightmapBuffer, const uint8_t *lightTables, size_t lightTableSize);
+
+	uint8_t adjustColor(uint8_t color, uint8_t lightLevel) const
+	{
+		size_t offset = lightLevel * lightTableSize + color;
+		return lightTables[offset];
+	}
+
+	const uint8_t *getLightingAt(const uint8_t *outLoc) const
+	{
+		return lightmapBuffer + (outLoc - outBuffer);
+	}
+
+	static Lightmap build(Point tilePosition, Point targetBufferPosition,
+	    int viewportWidth, int viewportHeight, int rows, int columns,
+	    const uint8_t *outBuffer, const uint8_t *lightTables, size_t lightTableSize);
+
+private:
+	const uint8_t *outBuffer;
+	const uint8_t *lightmapBuffer;
+	const uint8_t *lightTables;
+	const size_t lightTableSize;
+};
+
+} // namespace devilution
diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp
index e9f96bfd225..5a3d41a688f 100644
--- a/Source/engine/render/scrollrt.cpp
+++ b/Source/engine/render/scrollrt.cpp
@@ -25,6 +25,7 @@
 #include "engine/point.hpp"
 #include "engine/render/clx_render.hpp"
 #include "engine/render/dun_render.hpp"
+#include "engine/render/light_render.hpp"
 #include "engine/render/text_render.hpp"
 #include "engine/trn.hpp"
 #include "engine/world_tile.hpp"
@@ -467,15 +468,16 @@ void DrawObject(const Surface &out, const Object &objectToDraw, Point tilePositi
 	}
 }
 
-static void DrawDungeon(const Surface & /*out*/, Point /*tilePosition*/, Point /*targetBufferPosition*/);
+static void DrawDungeon(const Surface & /*out*/, const Lightmap & /*lightmap*/, Point /*tilePosition*/, Point /*targetBufferPosition*/);
 
 /**
  * @brief Render a cell
  * @param out Target buffer
+ * @param lightmap Per-pixel light buffer
  * @param tilePosition dPiece coordinates
  * @param targetBufferPosition Target buffer coordinates
  */
-void DrawCell(const Surface &out, Point tilePosition, Point targetBufferPosition, int lightTableIndex)
+void DrawCell(const Surface &out, const Lightmap lightmap, Point tilePosition, Point targetBufferPosition, int lightTableIndex)
 {
 	const uint16_t levelPieceId = dPiece[tilePosition.x][tilePosition.y];
 	const MICROS *pMap = &DPieceMicros[levelPieceId];
@@ -541,9 +543,9 @@ void DrawCell(const Surface &out, Point tilePosition, Point targetBufferPosition
 		const TileType tileType = levelCelBlock.type();
 		if (!isFloor || tileType == TileType::TransparentSquare) {
 			if (isFloor && tileType == TileType::TransparentSquare) {
-				RenderTileFoliage(out, targetBufferPosition, levelCelBlock, foliageTbl);
+				RenderTileFoliage(out, lightmap, targetBufferPosition, levelCelBlock, foliageTbl);
 			} else {
-				RenderTile(out, targetBufferPosition, levelCelBlock, getFirstTileMaskLeft(tileType), tbl);
+				RenderTile(out, lightmap, targetBufferPosition, levelCelBlock, getFirstTileMaskLeft(tileType), tbl);
 			}
 		}
 	}
@@ -551,9 +553,9 @@ void DrawCell(const Surface &out, Point tilePosition, Point targetBufferPosition
 		const TileType tileType = levelCelBlock.type();
 		if (!isFloor || tileType == TileType::TransparentSquare) {
 			if (isFloor && tileType == TileType::TransparentSquare) {
-				RenderTileFoliage(out, targetBufferPosition + RightFrameDisplacement, levelCelBlock, foliageTbl);
+				RenderTileFoliage(out, lightmap, targetBufferPosition + RightFrameDisplacement, levelCelBlock, foliageTbl);
 			} else {
-				RenderTile(out, targetBufferPosition + RightFrameDisplacement,
+				RenderTile(out, lightmap, targetBufferPosition + RightFrameDisplacement,
 				    levelCelBlock, getFirstTileMaskRight(tileType), tbl);
 			}
 		}
@@ -564,7 +566,7 @@ void DrawCell(const Surface &out, Point tilePosition, Point targetBufferPosition
 		{
 			const LevelCelBlock levelCelBlock { pMap->mt[i] };
 			if (levelCelBlock.hasValue()) {
-				RenderTile(out, targetBufferPosition,
+				RenderTile(out, lightmap, targetBufferPosition,
 				    levelCelBlock,
 				    transparency ? MaskType::Transparent : MaskType::Solid, foliageTbl);
 			}
@@ -572,7 +574,7 @@ void DrawCell(const Surface &out, Point tilePosition, Point targetBufferPosition
 		{
 			const LevelCelBlock levelCelBlock { pMap->mt[i + 1] };
 			if (levelCelBlock.hasValue()) {
-				RenderTile(out, targetBufferPosition + RightFrameDisplacement,
+				RenderTile(out, lightmap, targetBufferPosition + RightFrameDisplacement,
 				    levelCelBlock,
 				    transparency ? MaskType::Transparent : MaskType::Solid, foliageTbl);
 			}
@@ -594,10 +596,11 @@ void DrawCell(const Surface &out, Point tilePosition, Point targetBufferPosition
 /**
  * @brief Render a floor tile.
  * @param out Target buffer
+ * @param lightmap Per-pixel light buffer
  * @param tilePosition dPiece coordinates
  * @param targetBufferPosition Target buffer coordinate
  */
-void DrawFloorTile(const Surface &out, Point tilePosition, Point targetBufferPosition)
+void DrawFloorTile(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition)
 {
 	const int lightTableIndex = dLight[tilePosition.x][tilePosition.y];
 
@@ -611,14 +614,14 @@ void DrawFloorTile(const Surface &out, Point tilePosition, Point targetBufferPos
 	{
 		const LevelCelBlock levelCelBlock { DPieceMicros[levelPieceId].mt[0] };
 		if (levelCelBlock.hasValue()) {
-			RenderTileFrame(out, targetBufferPosition, TileType::LeftTriangle,
+			RenderTileFrame(out, lightmap, targetBufferPosition, TileType::LeftTriangle,
 			    GetDunFrame(levelCelBlock.frame()), DunFrameTriangleHeight, MaskType::Solid, tbl);
 		}
 	}
 	{
 		const LevelCelBlock levelCelBlock { DPieceMicros[levelPieceId].mt[1] };
 		if (levelCelBlock.hasValue()) {
-			RenderTileFrame(out, targetBufferPosition + RightFrameDisplacement, TileType::RightTriangle,
+			RenderTileFrame(out, lightmap, targetBufferPosition + RightFrameDisplacement, TileType::RightTriangle,
 			    GetDunFrame(levelCelBlock.frame()), DunFrameTriangleHeight, MaskType::Solid, tbl);
 		}
 	}
@@ -694,15 +697,16 @@ void DrawMonsterHelper(const Surface &out, Point tilePosition, Point targetBuffe
 /**
  * @brief Render object sprites
  * @param out Target buffer
+ * @param lightmap Per-pixel light buffer
  * @param tilePosition dPiece coordinates
  * @param targetBufferPosition Target buffer coordinates
  */
-void DrawDungeon(const Surface &out, Point tilePosition, Point targetBufferPosition)
+void DrawDungeon(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition)
 {
 	assert(InDungeonBounds(tilePosition));
 	const int lightTableIndex = dLight[tilePosition.x][tilePosition.y];
 
-	DrawCell(out, tilePosition, targetBufferPosition, lightTableIndex);
+	DrawCell(out, lightmap, tilePosition, targetBufferPosition, lightTableIndex);
 
 	const int8_t bDead = dCorpse[tilePosition.x][tilePosition.y];
 	const int8_t bMap = dTransVal[tilePosition.x][tilePosition.y];
@@ -827,6 +831,7 @@ void DrawDungeon(const Surface &out, Point tilePosition, Point targetBufferPosit
 	}
 
 	if (leveltype != DTYPE_TOWN) {
+		bool perPixelLighting = *GetOptions().Graphics.perPixelLighting;
 		int8_t bArch = dSpecial[tilePosition.x][tilePosition.y] - 1;
 		if (bArch >= 0) {
 			bool transparency = TransList[bMap];
@@ -834,7 +839,11 @@ void DrawDungeon(const Surface &out, Point tilePosition, Point targetBufferPosit
 			// Turn transparency off here for debugging
 			transparency = transparency && (SDL_GetModState() & KMOD_ALT) == 0;
 #endif
-			if (transparency) {
+			if (perPixelLighting && transparency) {
+				ClxDrawBlendedWithLightmap(out, targetBufferPosition, (*pSpecialCels)[bArch], lightmap);
+			} else if (perPixelLighting) {
+				ClxDrawWithLightmap(out, targetBufferPosition, (*pSpecialCels)[bArch], lightmap);
+			} else if (transparency) {
 				ClxDrawLightBlended(out, targetBufferPosition, (*pSpecialCels)[bArch], lightTableIndex);
 			} else {
 				ClxDrawLight(out, targetBufferPosition, (*pSpecialCels)[bArch], lightTableIndex);
@@ -855,12 +864,13 @@ void DrawDungeon(const Surface &out, Point tilePosition, Point targetBufferPosit
 /**
  * @brief Render a row of tiles
  * @param out Buffer to render to
+ * @param lightmap Per-pixel light buffer
  * @param tilePosition dPiece coordinates
  * @param targetBufferPosition Target buffer coordinates
  * @param rows Number of rows
  * @param columns Tile in a row
  */
-void DrawFloor(const Surface &out, Point tilePosition, Point targetBufferPosition, int rows, int columns)
+void DrawFloor(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition, int rows, int columns)
 {
 	for (int i = 0; i < rows; i++) {
 		for (int j = 0; j < columns; j++, tilePosition += Direction::East, targetBufferPosition.x += TILE_WIDTH) {
@@ -869,7 +879,7 @@ void DrawFloor(const Surface &out, Point tilePosition, Point targetBufferPositio
 				continue;
 			}
 			if (IsFloor(tilePosition)) {
-				DrawFloorTile(out, tilePosition, targetBufferPosition);
+				DrawFloorTile(out, lightmap, tilePosition, targetBufferPosition);
 			}
 		}
 		// Return to start of row
@@ -893,12 +903,13 @@ void DrawFloor(const Surface &out, Point tilePosition, Point targetBufferPositio
 /**
  * @brief Renders the floor tiles
  * @param out Output buffer
+ * @param lightmap Per-pixel light buffer
  * @param tilePosition dPiece coordinates
  * @param targetBufferPosition Buffer coordinates
  * @param rows Number of rows
  * @param columns Tile in a row
  */
-void DrawTileContent(const Surface &out, Point tilePosition, Point targetBufferPosition, int rows, int columns)
+void DrawTileContent(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition, int rows, int columns)
 {
 	// Keep evaluating until MicroTiles can't affect screen
 	rows += MicroTileLen;
@@ -922,13 +933,13 @@ void DrawTileContent(const Surface &out, Point tilePosition, Point targetBufferP
 					// sprite screen position rather than tile position.
 					if (IsWall(tilePosition) && (IsWall(tilePosition + Displacement { 1, 0 }) || (tilePosition.x > 0 && IsWall(tilePosition + Displacement { -1, 0 })))) { // Part of a wall aligned on the x-axis
 						if (IsTileNotSolid(tilePosition + Displacement { 1, -1 }) && IsTileNotSolid(tilePosition + Displacement { 0, -1 })) {                              // Has walkable area behind it
-							DrawDungeon(out, tilePosition + Direction::East, { targetBufferPosition.x + TILE_WIDTH, targetBufferPosition.y });
+							DrawDungeon(out, lightmap, tilePosition + Direction::East, { targetBufferPosition.x + TILE_WIDTH, targetBufferPosition.y });
 							skipNext = true;
 						}
 					}
 				}
 				if (!skip) {
-					DrawDungeon(out, tilePosition, targetBufferPosition);
+					DrawDungeon(out, lightmap, tilePosition, targetBufferPosition);
 				}
 				skip = skipNext;
 			}
@@ -1111,8 +1122,12 @@ void DrawGame(const Surface &fullOut, Point position, Displacement offset)
 	DunRenderStats.clear();
 #endif
 
-	DrawFloor(out, position, Point {} + offset, rows, columns);
-	DrawTileContent(out, position, Point {} + offset, rows, columns);
+	Lightmap lightmap = Lightmap::build(position, Point {} + offset,
+	    gnScreenWidth, gnViewportHeight, rows, columns,
+	    out.at(0, 0), LightTables[0].data(), LightTables[0].size());
+
+	DrawFloor(out, lightmap, position, Point {} + offset, rows, columns);
+	DrawTileContent(out, lightmap, position, Point {} + offset, rows, columns);
 
 	if (*GetOptions().Graphics.zoom) {
 		Zoom(fullOut.subregionY(0, gnViewportHeight));
diff --git a/Source/levels/gendung.h b/Source/levels/gendung.h
index 7c318d7c85f..d8ad32b3f28 100644
--- a/Source/levels/gendung.h
+++ b/Source/levels/gendung.h
@@ -166,7 +166,7 @@ extern DVL_API_FOR_TEST uint8_t currlevel;
 extern bool setlevel;
 /** Specifies the active quest level of the current game. */
 extern _setlevels setlvlnum;
-/** Specifies the player viewpoint X-coordinate of the map. */
+/** Specifies the dungeon type of the active quest level of the current game. */
 extern dungeon_type setlvltype;
 /** Specifies the player viewpoint X,Y-coordinates of the map. */
 extern DVL_API_FOR_TEST Point ViewPosition;
diff --git a/Source/options.cpp b/Source/options.cpp
index a9acef01b32..220a19e1a73 100644
--- a/Source/options.cpp
+++ b/Source/options.cpp
@@ -674,6 +674,7 @@ GraphicsOptions::GraphicsOptions()
           })
     , brightness("Brightness Correction", OptionEntryFlags::Invisible, "Brightness Correction", "Brightness correction level.", 0)
     , zoom("Zoom", OptionEntryFlags::None, N_("Zoom"), N_("Zoom on when enabled."), false)
+    , perPixelLighting("Per-pixel Lighting", OptionEntryFlags::None, N_("Per-pixel Lighting"), N_("Subtile lighting for smoother light gradients."), true)
     , colorCycling("Color Cycling", OptionEntryFlags::None, N_("Color Cycling"), N_("Color cycling effect used for water, lava, and acid animation."), true)
     , alternateNestArt("Alternate nest art", OptionEntryFlags::OnlyHellfire | OptionEntryFlags::CantChangeInGame, N_("Alternate nest art"), N_("The game will use an alternative palette for Hellfire’s nest tileset."), false)
 #if SDL_VERSION_ATLEAST(2, 0, 0)
@@ -704,6 +705,7 @@ std::vector<OptionEntryBase *> GraphicsOptions::GetEntries()
 		&brightness,
 		&zoom,
 		&showFPS,
+		&perPixelLighting,
 		&colorCycling,
 		&alternateNestArt,
 #if SDL_VERSION_ATLEAST(2, 0, 0)
diff --git a/Source/options.h b/Source/options.h
index 1c83e77e05b..0e70af79d7c 100644
--- a/Source/options.h
+++ b/Source/options.h
@@ -530,6 +530,8 @@ struct GraphicsOptions : OptionCategoryBase {
 	OptionEntryInt<int> brightness;
 	/** @brief Zoom on start. */
 	OptionEntryBoolean zoom;
+	/** @brief Subtile lighting for smoother light gradients. */
+	OptionEntryBoolean perPixelLighting;
 	/** @brief Enable color cycling animations. */
 	OptionEntryBoolean colorCycling;
 	/** @brief Use alternate nest palette. */
diff --git a/test/dun_render_benchmark.cpp b/test/dun_render_benchmark.cpp
index ece59309344..cce1dee70aa 100644
--- a/test/dun_render_benchmark.cpp
+++ b/test/dun_render_benchmark.cpp
@@ -65,11 +65,12 @@ void InitOnce()
 void RunForTileMaskLight(benchmark::State &state, TileType tileType, MaskType maskType, const uint8_t *lightTable)
 {
 	Surface out = Surface(SdlSurface.get());
+	Lightmap lightmap(nullptr, nullptr, nullptr, 0);
 	size_t numItemsProcessed = 0;
 	const std::span<const LevelCelBlock> tiles = Tiles[tileType];
 	for (auto _ : state) {
 		for (const LevelCelBlock &levelCelBlock : tiles) {
-			RenderTile(out, Point { 320, 240 }, levelCelBlock, maskType, lightTable);
+			RenderTile(out, lightmap, Point { 320, 240 }, levelCelBlock, maskType, lightTable);
 			uint8_t color = out[Point { 310, 200 }];
 			benchmark::DoNotOptimize(color);
 		}


Reviews:
user: login: glebm
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:16:59Z
user: login: glebm
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:17:26Z
user: login: glebm
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:25:56Z
user: login: glebm
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:30:20Z
user: login: glebm
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:39:24Z
user: login: glebm
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:39:35Z
user: login: StephenCWills
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T00:40:42Z
user: login: glebm
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T00:45:38Z
user: login: StephenCWills
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-04T01:10:50Z
user: login: glebm
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-04T13:32:29Z

Review comments:
diff_hunk: @@ -0,0 +1,31 @@
+#pragma once
+
+#include "engine/point.hpp"
+
+namespace devilution {
+
+class Lightmap {
+public:
+	Lightmap(uint8_t *outBuffer);
path: Source/engine/render/light_render.hpp
user: login: glebm
body: Single-argument constructors should be marked `explicit` https://abseil.io/tips/142
```suggestion
	explicit Lightmap(uint8_t *outBuffer);
```
created_at: 2025-03-04T00:16:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,31 @@
+#pragma once
+
+#include "engine/point.hpp"
+
+namespace devilution {
+
+class Lightmap {
+public:
+	Lightmap(uint8_t *outBuffer);
+
+	uint8_t adjustColor(uint8_t color, uint8_t lightLevel) const
+	{
+		size_t offset = lightLevel * lightTableSize + color;
+		return lightTables[offset];
+	}
+
+	const uint8_t *getLightingAt(const uint8_t *outLoc) const
+	{
+		return lightmapBuffer + (outLoc - outBuffer);
+	}
+
+private:
+	uint8_t *outBuffer;
+	uint8_t *lightmapBuffer;
+	uint8_t *lightTables;
+	size_t lightTableSize;
+};
+
+void BuildLightmap(Point tilePosition, Point targetBufferPosition, int rows, int columns);
+
+} // namespace devilution
path: Source/engine/render/light_render.hpp
user: login: glebm
body: The new files are missing a trailing newline
created_at: 2025-03-04T00:17:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,31 @@
+#pragma once
+
+#include "engine/point.hpp"
+
+namespace devilution {
+
+class Lightmap {
+public:
+	Lightmap(uint8_t *outBuffer);
path: Source/engine/render/light_render.hpp
user: login: glebm
body: I think there is a way to further simplify / isolate this:
1. Pass `LightTables` here as an argument.
2. Pass everything needed for `BuildLightmap` as arguments here and calling `BuildLightmap` in the constructor (also things like `gnScreenWidth` should be arguments).
created_at: 2025-03-04T00:25:56Z
author_association: COLLABORATOR
diff_hunk: @@ -64,6 +65,37 @@ struct BlitWithMap {
 	}
 };
 
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillWithLightmap(uint8_t *dst, unsigned length, uint8_t color, const Lightmap *lightmap)
path: Source/engine/render/blit_impl.hpp
user: login: glebm
body: `lightmap` cannot be null so it should be passed by reference throughout
created_at: 2025-03-04T00:30:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,31 @@
+#pragma once
+
+#include "engine/point.hpp"
+
+namespace devilution {
+
+class Lightmap {
+public:
+	Lightmap(uint8_t *outBuffer);
+
+	uint8_t adjustColor(uint8_t color, uint8_t lightLevel) const
+	{
+		size_t offset = lightLevel * lightTableSize + color;
+		return lightTables[offset];
+	}
+
+	const uint8_t *getLightingAt(const uint8_t *outLoc) const
+	{
+		return lightmapBuffer + (outLoc - outBuffer);
+	}
+
+private:
+	uint8_t *outBuffer;
path: Source/engine/render/light_render.hpp
user: login: glebm
body: ```suggestion
	const uint8_t *outBuffer;
```
created_at: 2025-03-04T00:39:24Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,31 @@
+#pragma once
+
+#include "engine/point.hpp"
+
+namespace devilution {
+
+class Lightmap {
+public:
+	Lightmap(uint8_t *outBuffer);
+
+	uint8_t adjustColor(uint8_t color, uint8_t lightLevel) const
+	{
+		size_t offset = lightLevel * lightTableSize + color;
+		return lightTables[offset];
+	}
+
+	const uint8_t *getLightingAt(const uint8_t *outLoc) const
+	{
+		return lightmapBuffer + (outLoc - outBuffer);
+	}
+
+private:
+	uint8_t *outBuffer;
+	uint8_t *lightmapBuffer;
+	uint8_t *lightTables;
path: Source/engine/render/light_render.hpp
user: login: glebm
body: ```suggestion
	const uint8_t *lightTables;
```
created_at: 2025-03-04T00:39:35Z
author_association: COLLABORATOR
diff_hunk: @@ -64,6 +65,37 @@ struct BlitWithMap {
 	}
 };
 
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillWithLightmap(uint8_t *dst, unsigned length, uint8_t color, const Lightmap *lightmap)
path: Source/engine/render/blit_impl.hpp
user: login: StephenCWills
body: `world_draw_black_tile()` passes `nullptr` for the `lightmap` argument into `RenderLeftTriangle<LightType::FullyDark, ...>()`.
created_at: 2025-03-04T00:40:42Z
author_association: MEMBER
diff_hunk: @@ -64,6 +65,37 @@ struct BlitWithMap {
 	}
 };
 
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillWithLightmap(uint8_t *dst, unsigned length, uint8_t color, const Lightmap *lightmap)
path: Source/engine/render/blit_impl.hpp
user: login: glebm
body: But that won't result in a call to `BlitFillWithLightmap`, otherwise it'd crash right?
created_at: 2025-03-04T00:45:37Z
author_association: COLLABORATOR
diff_hunk: @@ -64,6 +65,37 @@ struct BlitWithMap {
 	}
 };
 
+DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillWithLightmap(uint8_t *dst, unsigned length, uint8_t color, const Lightmap *lightmap)
path: Source/engine/render/blit_impl.hpp
user: login: StephenCWills
body: That's true. These blit functions would never see the `nullptr` value.
created_at: 2025-03-04T01:10:50Z
author_association: MEMBER

PR comments:
user: login: glebm
created_at: 2025-03-03T08:05:31Z
body: Wow, this is the craziest software rasterization wizardry that I've seen in a while! Amazing! :mage_man: 

My main request is to avoid the use of globals in `light_render.cpp` and just pass everything as arguments, because:
1. This simplifies testing.
2. Compiler can often optimize locals better, e.g. because it knows the screen width remains constant for the duration of the call etc.

It'd also be nice to have a short comment before each of the 0..15 cases.
author_association: COLLABORATOR
user: login: StephenCWills
created_at: 2025-03-03T13:19:34Z
body: This update pushes global variable access up to the top of the `BuildLightmap()` function by passing a pointer to `lightmap` data and the `screenWidth` variable in as parameters to both `RenderCell()` and `RenderTriangle()`. It also adds the comments above each of the case statements in the marching squares algorithm.

I don't know of a good way to avoid access to `LightTables` and `GlobalBackBuffer()` inside the main rendering loop so I left the `AdjustColor()` and `GetLightmapAt()` functions alone. Maybe if I went through and added `gbb`, `lightmap`, and `LightTables` pointers as parameters to a whole bunch of rendering functions, I could make it work.

EDIT: I forgot to mention, this also fixes the issue where arches were still being rendered using tile-based lighting.
author_association: MEMBER
user: login: julealgon
created_at: 2025-03-03T13:25:19Z
body: Super cool to finally see this built-in!
author_association: CONTRIBUTOR
user: login: galaxyhaxz
created_at: 2025-03-03T16:17:36Z
body: How do the caves look? :P
author_association: MEMBER
user: login: StephenCWills
created_at: 2025-03-03T16:37:29Z
body: 
https://github.com/user-attachments/assets/593039d0-806d-41fb-bd97-33d820d11753


author_association: MEMBER
user: login: galaxyhaxz
created_at: 2025-03-03T16:52:38Z
body: That's pretty good. One of the problems I see in all implementations, the playstation port, Tchernobog and even my own, is that caves are difficult to pull off due to the walls and floors being connected, making it difficult to shade between them as it's irregularly shaped. Pic below demonstrating it:
![screen00](https://github.com/user-attachments/assets/a06a84a1-6ad7-4657-82d8-d7acc6db8560)

author_association: MEMBER
user: login: kphoenix137
created_at: 2025-03-03T18:45:17Z
body: > DevilutionX-2025-03-03_11.30.15.mp4

This is fantastic
author_association: COLLABORATOR
user: login: StephenCWills
created_at: 2025-03-03T20:32:20Z
body: My latest update introduces a `Lightmap` class which is used to pass per-pixel lighting information into all the rendering functions, avoiding the direct references to global variables via `GlobalBackBuffer()` and `LightTables`. I put it in a separate commit so I can revert it, just in case this is actually a huge mess and not the preferred way of doing things.
author_association: MEMBER
user: login: AJenbo
created_at: 2025-03-04T14:43:51Z
body: A follow up: https://github.com/diasurgical/DevilutionX/issues/7809
author_association: MEMBER