diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,16 @@
 # Changelog for keid-render-basic
 
+## 0.1.5.0
+
+- Extracted deprecated `allocateRenderpass_` into `Render.Basic.allocate`.
+- Extracted hardcoded shadowmap params into `Render.Basic.Settings`.
+- Added `Render.Unlit.TileMap` pipeline.
+- Added `Render.DepthOnly` pipeline.
+- Updated lit pipelines to match vertex position calculation.
+- Fixed shadowmap index guard.
+- Pipelines updated for new configs with specialization.
+  * `pDebug` is split into multiple pipelines, specialized by mode.
+
 ## 0.1.4.0
 
 Update to match `keid-core-0.1.4.0`.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -5,12 +5,17 @@
   * Unlit pipelines
     * Colored
     * Textured
+    * Tile map
+    * Wires
   * Lit pipelines
     * Colored
     * Textured
     * Material
   * Skybox pipeline
-  * Debug pipeline
+  * Debug pipelines
+    * UV
+    * Shadowmap
 - Shadowmap pass
   * Shadow-casting pipeline
+- Depth-only
 - Asset loader stage
diff --git a/keid-render-basic.cabal b/keid-render-basic.cabal
--- a/keid-render-basic.cabal
+++ b/keid-render-basic.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           keid-render-basic
-version:        0.1.4.0
+version:        0.1.5.0
 synopsis:       Basic rendering programs for Keid engine.
 category:       Game Engine
 author:         IC Rainbow
@@ -38,6 +38,7 @@
       Render.Code.Lit
       Render.Debug.Model
       Render.Debug.Pipeline
+      Render.DepthOnly.Pipeline
       Render.DescSets.Set0
       Render.DescSets.Set0.Code
       Render.DescSets.Sun
@@ -59,6 +60,8 @@
       Render.Unlit.Colored.Pipeline
       Render.Unlit.Textured.Model
       Render.Unlit.Textured.Pipeline
+      Render.Unlit.TileMap.Model
+      Render.Unlit.TileMap.Pipeline
       Resource.Font
       Resource.Font.EvanW
       Resource.Mesh.Lit
@@ -139,7 +142,7 @@
     , distributive
     , file-embed >=0.0.10
     , geomancy
-    , keid-core >=0.1.4.0
+    , keid-core >=0.1.5.0
     , keid-geometry
     , neat-interpolation
     , resourcet
diff --git a/src/Render/Basic.hs b/src/Render/Basic.hs
--- a/src/Render/Basic.hs
+++ b/src/Render/Basic.hs
@@ -10,6 +10,7 @@
 import Data.Tagged (Tagged(..))
 import RIO.Vector.Partial as Vector (headM)
 import Vulkan.Core10 qualified as Vk
+import Vulkan.Zero (Zero(..))
 
 -- keid-core
 
@@ -17,11 +18,12 @@
 import Engine.Types qualified as Engine
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Swapchain qualified as Swapchain
-import Engine.Vulkan.Types (DsBindings, HasSwapchain, RenderPass(..))
+import Engine.Vulkan.Types (DsBindings, HasSwapchain, HasVulkan, RenderPass(..))
 
 -- keid-render-basic
 
 import Render.Debug.Pipeline qualified as Debug
+import Render.DepthOnly.Pipeline qualified as DepthOnly
 import Render.DescSets.Set0 (Scene)
 import Render.DescSets.Set0 qualified as Scene
 import Render.DescSets.Sun (Sun)
@@ -39,6 +41,7 @@
 import Render.Skybox.Pipeline qualified as Skybox
 import Render.Unlit.Colored.Pipeline qualified as UnlitColored
 import Render.Unlit.Textured.Pipeline qualified as UnlitTextured
+import Render.Unlit.TileMap.Pipeline qualified as UnlitTileMap
 
 type Stage = Engine.Stage RenderPasses Pipelines
 type Frame = Engine.Frame RenderPasses Pipelines
@@ -50,33 +53,67 @@
   }
 
 instance RenderPass RenderPasses where
-  allocateRenderpass_ context = do
-    rpForwardMsaa <- ForwardMsaa.allocateMsaa context
-
-    -- TODO: get from config
-    let
-      size      = 4096 -- XXX: 2048 is kinda minimum, even with PCF enabled
-      numLayers = 2    -- XXX: no-cascade sun + moon
-    rpShadowPass <- ShadowPass.allocate context size numLayers
-
-    pure RenderPasses{..}
-
-  updateRenderpass context RenderPasses{..} = RenderPasses
-    <$> ForwardMsaa.updateMsaa context rpForwardMsaa
+  updateRenderpass swapchain RenderPasses{..} = RenderPasses
+    <$> ForwardMsaa.updateMsaa swapchain rpForwardMsaa
     <*> pure rpShadowPass -- XXX: not a screen pass
 
   refcountRenderpass RenderPasses{..} = do
     refcountRenderpass rpForwardMsaa
     refcountRenderpass rpShadowPass
 
+data Settings = Settings
+  { sShadowSize   :: Word32
+  , sShadowLayers :: Word32
+  }
+  deriving (Eq, Show)
+
+instance Zero Settings where
+  zero = Settings
+    -- XXX: 1x1 placeholder image
+    { sShadowSize   = 1
+    , sShadowLayers = 1
+    }
+
+allocate
+  :: ( HasSwapchain swapchain
+     , HasLogFunc env
+     , HasVulkan env
+     )
+  => Settings
+  -> swapchain
+  -> ResourceT (RIO env) RenderPasses
+allocate Settings{..} swapchain = do
+  rpForwardMsaa <- ForwardMsaa.allocateMsaa swapchain
+
+  rpShadowPass <- ShadowPass.allocate
+    swapchain
+    sShadowSize
+    sShadowLayers
+
+  pure RenderPasses{..}
+
+allocate_
+  :: ( HasSwapchain swapchain
+     , HasLogFunc env
+     , HasVulkan env
+     )
+  => swapchain
+  -> ResourceT (RIO env) RenderPasses
+allocate_ = allocate zero
+
 data Pipelines = Pipelines
   { pMSAA       :: Vk.SampleCountFlagBits
   , pSceneBinds :: Tagged Scene DsBindings
 
   , pEvanwSdf :: EvanwSdf.Pipeline
   , pSkybox   :: Skybox.Pipeline
-  , pDebug    :: Debug.Pipeline
 
+  , pDebugUV      :: Debug.Pipeline
+  , pDebugTexture :: Debug.Pipeline
+  , pDebugShadow  :: Debug.Pipeline
+
+  , pDepthOnly :: DepthOnly.Pipeline
+
   , pLitColored       :: LitColored.Pipeline
   , pLitColoredBlend  :: LitColored.Pipeline
   , pLitMaterial      :: LitMaterial.Pipeline
@@ -88,6 +125,8 @@
   , pUnlitColoredNoDepth :: UnlitColored.Pipeline
   , pUnlitTextured       :: UnlitTextured.Pipeline
   , pUnlitTexturedBlend  :: UnlitTextured.Pipeline
+  , pTileMap             :: UnlitTileMap.Pipeline
+  , pTileMapBlend        :: UnlitTileMap.Pipeline
   , pWireframe           :: UnlitColored.Pipeline
   , pWireframeNoDepth    :: UnlitColored.Pipeline
 
@@ -114,10 +153,15 @@
   -> RenderPasses
   -> ResourceT (StageRIO st) Pipelines
 allocatePipelines pSceneBinds pMSAA RenderPasses{..} = do
-  pDebug    <- Debug.allocate pMSAA pSceneBinds rpForwardMsaa
+  pDebugUV      <- Debug.allocate Debug.UV         pMSAA pSceneBinds rpForwardMsaa
+  pDebugTexture <- Debug.allocate Debug.Texture    pMSAA pSceneBinds rpForwardMsaa
+  pDebugShadow  <- Debug.allocate (Debug.Shadow 1) pMSAA pSceneBinds rpForwardMsaa
+
   pEvanwSdf <- EvanwSdf.allocate pMSAA pSceneBinds rpForwardMsaa
   pSkybox   <- Skybox.allocate pMSAA pSceneBinds rpForwardMsaa
 
+  pDepthOnly <- DepthOnly.allocate pMSAA pSceneBinds rpForwardMsaa
+
   pLitColored       <- LitColored.allocate pMSAA pSceneBinds rpForwardMsaa
   pLitColoredBlend  <- LitColored.allocateBlend pMSAA pSceneBinds rpForwardMsaa
   pLitMaterial      <- LitMaterial.allocate pMSAA pSceneBinds rpForwardMsaa
@@ -129,6 +173,8 @@
   pUnlitColoredNoDepth <- UnlitColored.allocate False pMSAA pSceneBinds rpForwardMsaa
   pUnlitTextured       <- UnlitTextured.allocate pMSAA pSceneBinds rpForwardMsaa
   pUnlitTexturedBlend  <- UnlitTextured.allocateBlend pMSAA pSceneBinds rpForwardMsaa
+  pTileMap             <- UnlitTileMap.allocate pMSAA pSceneBinds rpForwardMsaa
+  pTileMapBlend        <- UnlitTileMap.allocateBlend pMSAA pSceneBinds rpForwardMsaa
   pWireframe           <- UnlitColored.allocateWireframe True pMSAA pSceneBinds rpForwardMsaa
   pWireframeNoDepth    <- UnlitColored.allocateWireframe False pMSAA pSceneBinds rpForwardMsaa
 
diff --git a/src/Render/Code/Lit.hs b/src/Render/Code/Lit.hs
--- a/src/Render/Code/Lit.hs
+++ b/src/Render/Code/Lit.hs
@@ -141,7 +141,7 @@
       vec3 lightDir = normalize(lights[l].direction.xyz); // L
 
       float shade = 1.0; // XXX: 0 - occluded, 1 - lit
-      if (lights[l].shadow.z > 0) {
+      if (lights[l].shadow.z > -1) {
         vec4 light_space_pos = lights[l].viewProjection * fPosition;
         vec4 shadowCoord = light_space_pos /= light_space_pos.w;
 
diff --git a/src/Render/Debug/Pipeline.hs b/src/Render/Debug/Pipeline.hs
--- a/src/Render/Debug/Pipeline.hs
+++ b/src/Render/Debug/Pipeline.hs
@@ -1,6 +1,7 @@
 module Render.Debug.Pipeline
   ( Pipeline
   , allocate
+  , Mode(..)
   ) where
 
 import RIO
@@ -8,9 +9,9 @@
 import Control.Monad.Trans.Resource (ResourceT)
 import Data.Tagged (Tagged(..))
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (zero)
 
 import Engine.Vulkan.Pipeline qualified as Pipeline
+import Engine.Vulkan.Shader qualified as Shader
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
 import Render.Code (compileVert, compileFrag, glsl)
 import Render.Debug.Model qualified as Model
@@ -19,27 +20,41 @@
 
 type Pipeline = Pipeline.Pipeline '[Scene] Model.VertexAttrs Model.InstanceAttrs
 
+data Mode
+  = UV
+  | Texture
+  | Shadow Word32
+  deriving (Eq, Ord, Show)
+
+instance Shader.Specialization Mode where
+  specializationData = \case
+    UV ->
+      [0]
+    Texture ->
+      [1]
+    Shadow bits ->
+      [2, bits]
+
 allocate
   :: ( HasVulkan env
      , HasRenderPass renderpass
      )
-  => Vk.SampleCountFlagBits
+  => Mode
+  -> Vk.SampleCountFlagBits
   -> Tagged Scene DsBindings
   -> renderpass
   -> ResourceT (RIO env) Pipeline
-allocate multisample (Tagged set0) rp = do
-  (_, p) <- Pipeline.allocate
-    Nothing
-    multisample
-    (zero
-      { Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
-      , Pipeline.cVertexCode   = Just vertCode
-      , Pipeline.cVertexInput  = vertexInput
-      , Pipeline.cFragmentCode = Just fragCode
-      })
-    rp
-  pure p
+allocate mode multisample (Tagged set0) = do
+  fmap snd . Pipeline.allocate Nothing multisample config
   where
+    config = Pipeline.baseConfig
+      { Pipeline.cDescLayouts    = Tagged @'[Scene] [set0]
+      , Pipeline.cVertexCode     = Just vertCode
+      , Pipeline.cVertexInput    = vertexInput
+      , Pipeline.cFragmentCode   = Just fragCode
+      , Pipeline.cSpecialization = mode
+      }
+
     vertexInput = Pipeline.vertexInput
       [ vertexPos -- vPosition
       , (Vk.VERTEX_INPUT_RATE_VERTEX,   Model.vkVertexAttrs)
@@ -72,11 +87,12 @@
     layout(location = 2) flat out ivec2 fTextureIds;
 
     void main() {
+      vec4 fPosition = iModel * vec4(vPosition, 1.0);
+
       gl_Position
         = scene.projection
         * scene.view
-        * iModel
-        * vec4(vPosition, 1.0);
+        * fPosition;
 
       fTexCoord     = vTexCoord * iTextureScaleOffset.st + iTextureScaleOffset.pq;
       fTextureGamma = iTextureGamma;
@@ -102,7 +118,8 @@
 
     layout(location = 0) out vec4 oColor;
 
-    const int mode = 1;
+    layout (constant_id=0) const uint mode = 0;
+    layout (constant_id=1) const uint shadow_mask = 0;
 
     void main() {
       vec4 texel = vec4(0);
@@ -125,17 +142,28 @@
           break;
 
         case 2:
-          float d0 = texture(
-            shadowmaps,
-            vec3(fTexCoord, 0.0)
-          ).x;
+          float d0 = 1.0;
+          if ((shadow_mask & 1) == 1)
+            d0 = texture(
+              shadowmaps,
+              vec3(fTexCoord, 0.0)
+            ).x;
 
-          float d1 = texture(
-            shadowmaps,
-            vec3(fTexCoord, 1.0)
-          ).x;
+          float d1 = 1.0;
+          if ((shadow_mask & 2) == 2)
+            d1 = texture(
+              shadowmaps,
+              vec3(fTexCoord, 1.0)
+            ).x;
 
-          texel = vec4(1-d0, 0, 1-d1, 1.0);
+          float d2 = 1.0;
+          if ((shadow_mask & 4) == 4)
+            d2 = texture(
+              shadowmaps,
+              vec3(fTexCoord, 2.0)
+            ).x;
+
+          texel = vec4(1-d0, 1-d1, 1-d2, 1.0);
           break;
 
         default:
diff --git a/src/Render/DepthOnly/Pipeline.hs b/src/Render/DepthOnly/Pipeline.hs
new file mode 100644
--- /dev/null
+++ b/src/Render/DepthOnly/Pipeline.hs
@@ -0,0 +1,81 @@
+module Render.DepthOnly.Pipeline
+  ( Pipeline
+  , allocate
+
+  , Config
+  , config
+  ) where
+
+import RIO
+
+import Control.Monad.Trans.Resource (ResourceT)
+import Data.Tagged (Tagged(..))
+import Vulkan.Core10 qualified as Vk
+
+import Geomancy (Transform)
+import Engine.Vulkan.Pipeline qualified as Pipeline
+import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
+import Render.Code (compileVert, {- compileFrag, -} glsl)
+import Render.DescSets.Set0 (Scene, vertexPos, instanceTransform)
+import Render.DescSets.Set0.Code (set0binding0)
+
+type Config = Pipeline.Configure Pipeline ()
+type Pipeline = Pipeline.Pipeline '[Scene] () Transform
+
+allocate
+  :: ( HasVulkan env
+     , HasRenderPass renderpass
+     )
+  => Vk.SampleCountFlagBits
+  -> Tagged Scene DsBindings
+  -> renderpass
+  -> ResourceT (RIO env) Pipeline
+allocate multisample tset0 rp = do
+  (_, p) <- Pipeline.allocate
+    Nothing
+    multisample
+    (config tset0)
+    rp
+  pure p
+
+config :: Tagged Scene DsBindings -> Config
+config (Tagged set0) = Pipeline.baseConfig
+  { Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
+  , Pipeline.cVertexCode   = Just vertCode
+  , Pipeline.cVertexInput  = vertexInput
+  , Pipeline.cFragmentCode = Nothing -- XXX: needed for alpha cut-outs
+  }
+  where
+    vertexInput = Pipeline.vertexInput
+      [ vertexPos
+      , instanceTransform
+      ]
+
+vertCode :: ByteString
+vertCode =
+  $(compileVert [glsl|
+    #version 450
+    #extension GL_ARB_separate_shader_objects : enable
+
+    invariant gl_Position;
+
+    ${set0binding0}
+
+    layout(location = 0) in vec3 vPosition;
+    layout(location = 1) in mat4 iModel;
+
+    // layout(location = 0) out vec4 fColor;
+
+    void main() {
+      vec4 fPosition = iModel * vec4(vPosition, 1.0);
+
+      gl_Position
+        = scene.projection
+        * scene.view
+        * fPosition;
+
+      // XXX: needed for alpha cut-outs
+      // fColor = vColor;
+      // fColor.rgb * fColor.a;
+    }
+  |])
diff --git a/src/Render/Font/EvanwSdf/Pipeline.hs b/src/Render/Font/EvanwSdf/Pipeline.hs
--- a/src/Render/Font/EvanwSdf/Pipeline.hs
+++ b/src/Render/Font/EvanwSdf/Pipeline.hs
@@ -10,9 +10,7 @@
 import Control.Monad.Trans.Resource (ResourceT)
 import Data.Tagged (Tagged(..))
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (Zero(..))
 
-import Engine.Vulkan.Pipeline (Config(..))
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
 import Render.Code (compileVert, compileFrag, glsl)
@@ -34,15 +32,15 @@
   fmap snd . Pipeline.allocate
     Nothing
     multisample
-    zero
-      { cVertexCode         = Just vertCode
-      , cFragmentCode       = Just fragCode
-      , cDescLayouts        = Tagged @'[Scene] [set0]
-      , cVertexInput        = vertexInput
-      , cDepthTest          = False
-      , cDepthWrite         = False
-      , cBlend              = True
-      , cCull               = Vk.CULL_MODE_NONE
+    Pipeline.baseConfig
+      { Pipeline.cVertexCode     = Just vertCode
+      , Pipeline.cFragmentCode   = Just fragCode
+      , Pipeline.cDescLayouts    = Tagged @'[Scene] [set0]
+      , Pipeline.cVertexInput    = vertexInput
+      , Pipeline.cDepthTest      = False
+      , Pipeline.cDepthWrite     = False
+      , Pipeline.cBlend          = True
+      , Pipeline.cCull           = Vk.CULL_MODE_NONE
       }
   where
     vertexInput = Pipeline.vertexInput
diff --git a/src/Render/ForwardMsaa.hs b/src/Render/ForwardMsaa.hs
--- a/src/Render/ForwardMsaa.hs
+++ b/src/Render/ForwardMsaa.hs
@@ -33,16 +33,13 @@
   }
 
 instance HasRenderPass ForwardMsaa where
-  getRenderPass          = fmRenderPass
-  getFramebuffers        = fmFrameBuffers
-  getClearValues         = fmClear
-  getRenderArea          = fmRenderArea
+  getRenderPass   = fmRenderPass
+  getFramebuffers = fmFrameBuffers
+  getClearValues  = fmClear
+  getRenderArea   = fmRenderArea
 
 instance RenderPass ForwardMsaa where
-  allocateRenderpass_ = allocateMsaa -- error "Thou shalt allocateMsaa instead"
-
   updateRenderpass = updateMsaa
-
   refcountRenderpass = resourceTRefCount . fmRelease
 
 allocateMsaa
diff --git a/src/Render/Lit/Colored/Pipeline.hs b/src/Render/Lit/Colored/Pipeline.hs
--- a/src/Render/Lit/Colored/Pipeline.hs
+++ b/src/Render/Lit/Colored/Pipeline.hs
@@ -13,7 +13,6 @@
 import Control.Monad.Trans.Resource (ResourceT)
 import Data.Tagged (Tagged(..))
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (zero)
 
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
@@ -23,7 +22,7 @@
 import Render.DescSets.Set0.Code (set0binding0, set0binding1, set0binding2, set0binding3, set0binding4, set0binding5)
 import Render.Lit.Colored.Model qualified as Model
 
-type Config = Pipeline.Config '[Scene] Model.VertexAttrs Model.InstanceAttrs
+type Config = Pipeline.Configure Pipeline ()
 type Pipeline = Pipeline.Pipeline '[Scene] Model.VertexAttrs Model.InstanceAttrs
 
 allocate
@@ -59,7 +58,7 @@
   pure p
 
 config :: Tagged Scene DsBindings -> Config
-config (Tagged set0) = zero
+config (Tagged set0) = Pipeline.baseConfig
   { Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
   , Pipeline.cVertexCode   = Just vertCode
   , Pipeline.cVertexInput  = vertexInput
@@ -83,6 +82,8 @@
     #version 450
     #extension GL_ARB_separate_shader_objects : enable
 
+    invariant gl_Position;
+
     ${set0binding0}
 
     layout(location = 0) in vec3 vPosition;
@@ -123,6 +124,8 @@
     #version 450
     #extension GL_ARB_separate_shader_objects : enable
     #extension GL_EXT_nonuniform_qualifier : enable
+
+    layout(early_fragment_tests) in;
 
     // TODO: move to spec constant
     const uint MAX_LIGHTS = 255;
diff --git a/src/Render/Lit/Material/Pipeline.hs b/src/Render/Lit/Material/Pipeline.hs
--- a/src/Render/Lit/Material/Pipeline.hs
+++ b/src/Render/Lit/Material/Pipeline.hs
@@ -13,7 +13,6 @@
 import Control.Monad.Trans.Resource (ResourceT)
 import Data.Tagged (Tagged(..))
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (zero)
 
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
@@ -23,7 +22,7 @@
 import Render.Code.Lit (litMain, shadowFuns, structLight, structMaterial, brdfSpecular)
 import Render.Lit.Material.Model qualified as Model
 
-type Config = Pipeline.Config '[Scene] Model.VertexAttrs Model.InstanceAttrs
+type Config = Pipeline.Configure Pipeline ()
 type Pipeline = Pipeline.Pipeline '[Scene] Model.VertexAttrs Model.InstanceAttrs
 
 allocate
@@ -59,11 +58,13 @@
   pure p
 
 config :: Tagged Scene DsBindings -> Config
-config (Tagged set0) = zero
+config (Tagged set0) = Pipeline.baseConfig
   { Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
   , Pipeline.cVertexCode   = Just vertCode
   , Pipeline.cVertexInput  = vertexInput
   , Pipeline.cFragmentCode = Just fragCode
+  , Pipeline.cDepthWrite   = False -- XXX: Lit pipelines require depth pre-pass
+  , Pipeline.cDepthCompare = Vk.COMPARE_OP_EQUAL
   }
   where
     vertexInput = Pipeline.vertexInput
@@ -74,8 +75,11 @@
 
 configBlend :: Tagged Scene DsBindings -> Config
 configBlend tset0 = (config tset0)
-  { Pipeline.cBlend      = True
-  , Pipeline.cDepthWrite = False
+  { Pipeline.cBlend =
+      True
+  , Pipeline.cDepthCompare =
+      -- XXX: blended objects aren't a part of depth prepass
+      Vk.COMPARE_OP_LESS_OR_EQUAL
   }
 
 vertCode :: ByteString
@@ -84,6 +88,8 @@
     #version 450
     #extension GL_ARB_separate_shader_objects : enable
 
+    invariant gl_Position;
+
     ${set0binding0}
 
     // vertexPos
@@ -130,6 +136,8 @@
     #version 450
     #extension GL_ARB_separate_shader_objects : enable
     #extension GL_EXT_nonuniform_qualifier : enable
+
+    layout(early_fragment_tests) in;
 
     // XXX: copypasta from Lit.Colored
     // TODO: move to spec constant
diff --git a/src/Render/Lit/Textured/Pipeline.hs b/src/Render/Lit/Textured/Pipeline.hs
--- a/src/Render/Lit/Textured/Pipeline.hs
+++ b/src/Render/Lit/Textured/Pipeline.hs
@@ -13,7 +13,6 @@
 import Control.Monad.Trans.Resource (ResourceT)
 import Data.Tagged (Tagged(..))
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (zero)
 
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
@@ -23,7 +22,7 @@
 import Render.Code.Lit (litMain, shadowFuns, structLight, brdfSpecular)
 import Render.Lit.Textured.Model qualified as Model
 
-type Config = Pipeline.Config '[Scene] Model.VertexAttrs Model.InstanceAttrs
+type Config = Pipeline.Configure Pipeline ()
 type Pipeline = Pipeline.Pipeline '[Scene] Model.VertexAttrs Model.InstanceAttrs
 
 allocate
@@ -59,11 +58,13 @@
   pure p
 
 config :: Tagged Scene DsBindings -> Config
-config (Tagged set0) = zero
+config (Tagged set0) = Pipeline.baseConfig
   { Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
   , Pipeline.cVertexCode   = Just vertCode
   , Pipeline.cVertexInput  = vertexInput
   , Pipeline.cFragmentCode = Just fragCode
+  , Pipeline.cDepthWrite   = False -- XXX: Lit pipelines require depth pre-pass
+  , Pipeline.cDepthCompare = Vk.COMPARE_OP_EQUAL
   }
   where
     vertexInput = Pipeline.vertexInput
@@ -76,7 +77,7 @@
 configBlend :: Tagged Scene DsBindings -> Config
 configBlend tset0 = (config tset0)
   { Pipeline.cBlend      = True
-  , Pipeline.cDepthWrite = False
+  , Pipeline.cDepthCompare = Vk.COMPARE_OP_LESS_OR_EQUAL
   }
 
 vertCode :: ByteString
@@ -85,6 +86,8 @@
     #version 450
     #extension GL_ARB_separate_shader_objects : enable
 
+    invariant gl_Position;
+
     ${set0binding0}
 
     // vertexPos
@@ -128,6 +131,8 @@
     #version 450
     #extension GL_ARB_separate_shader_objects : enable
     #extension GL_EXT_nonuniform_qualifier : enable
+
+    layout(early_fragment_tests) in;
 
     // XXX: copypasta from Lit.Colored
     // TODO: move to spec constant
diff --git a/src/Render/ShadowMap/Pipeline.hs b/src/Render/ShadowMap/Pipeline.hs
--- a/src/Render/ShadowMap/Pipeline.hs
+++ b/src/Render/ShadowMap/Pipeline.hs
@@ -15,7 +15,6 @@
 import Data.Tagged (Tagged(..))
 import Geomancy (Transform)
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (zero)
 
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
@@ -24,7 +23,7 @@
 import Render.DescSets.Set0 (vertexPos, instanceTransform)
 import Render.DescSets.Sun (Sun, pattern MAX_VIEWS)
 
-type Config = Pipeline.Config '[Sun] () Transform
+type Config = Pipeline.Configure Pipeline ()
 type Pipeline = Pipeline.Pipeline '[Sun] () Transform
 
 data Settings = Settings
@@ -55,7 +54,7 @@
   pure p
 
 config :: Tagged Sun DsBindings -> Settings -> Config
-config (Tagged set0) Settings{..} = zero
+config (Tagged set0) Settings{..} = Pipeline.baseConfig
   { Pipeline.cDescLayouts  = Tagged @'[Sun] [set0]
   , Pipeline.cVertexCode   = Just vertCode
   , Pipeline.cVertexInput  = vertexInput
diff --git a/src/Render/ShadowMap/RenderPass.hs b/src/Render/ShadowMap/RenderPass.hs
--- a/src/Render/ShadowMap/RenderPass.hs
+++ b/src/Render/ShadowMap/RenderPass.hs
@@ -44,8 +44,6 @@
   getRenderArea   = smRenderArea
 
 instance RenderPass ShadowMap where
-  allocateRenderpass_ = error "Don't allocate ShadowMap via class"
-  updateRenderpass    = error "Don't recreate ShadowMap on resize"
   refcountRenderpass  = resourceTRefCount . smRelease
 
 allocate
diff --git a/src/Render/Skybox/Pipeline.hs b/src/Render/Skybox/Pipeline.hs
--- a/src/Render/Skybox/Pipeline.hs
+++ b/src/Render/Skybox/Pipeline.hs
@@ -10,9 +10,7 @@
 import Control.Monad.Trans.Resource (ResourceT)
 import Data.Tagged (Tagged(..))
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (Zero(..))
 
-import Engine.Vulkan.Pipeline (Config(..))
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
 import Render.Code (compileVert, compileFrag, glsl)
@@ -33,11 +31,11 @@
   fmap snd . Pipeline.allocate
     Nothing
     multisample
-    zero
-      { cVertexCode         = Just vertCode
-      , cFragmentCode       = Just fragCode
-      , cDescLayouts        = Tagged @'[Scene] [set0]
-      , cCull               = Vk.CULL_MODE_NONE
+    Pipeline.baseConfig
+      { Pipeline.cVertexCode   = Just vertCode
+      , Pipeline.cFragmentCode = Just fragCode
+      , Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
+      , Pipeline.cCull         = Vk.CULL_MODE_NONE
       }
 
 vertCode :: ByteString
diff --git a/src/Render/Unlit/Colored/Pipeline.hs b/src/Render/Unlit/Colored/Pipeline.hs
--- a/src/Render/Unlit/Colored/Pipeline.hs
+++ b/src/Render/Unlit/Colored/Pipeline.hs
@@ -13,7 +13,6 @@
 import Control.Monad.Trans.Resource (ResourceT)
 import Data.Tagged (Tagged(..))
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (zero)
 
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
@@ -22,7 +21,7 @@
 import Render.DescSets.Set0.Code (set0binding0)
 import Render.Unlit.Colored.Model qualified as Model
 
-type Config = Pipeline.Config '[Scene] Model.VertexAttrs Model.InstanceAttrs
+type Config = Pipeline.Configure Pipeline ()
 type Pipeline = Pipeline.Pipeline '[Scene] Model.VertexAttrs Model.InstanceAttrs
 
 allocate
@@ -60,7 +59,7 @@
   pure p
 
 config :: Bool -> Tagged Scene DsBindings -> Config
-config useDepth (Tagged set0) = zero
+config useDepth (Tagged set0) = Pipeline.baseConfig
   { Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
   , Pipeline.cVertexCode   = Just vertCode
   , Pipeline.cVertexInput  = vertexInput
@@ -97,11 +96,12 @@
     layout(location = 0) out vec4 fColor;
 
     void main() {
+      vec4 fPosition = iModel * vec4(vPosition, 1.0);
+
       gl_Position
         = scene.projection
         * scene.view
-        * iModel
-        * vec4(vPosition, 1.0);
+        * fPosition;
 
       fColor = vColor;
       fColor.rgb * fColor.a;
diff --git a/src/Render/Unlit/Textured/Pipeline.hs b/src/Render/Unlit/Textured/Pipeline.hs
--- a/src/Render/Unlit/Textured/Pipeline.hs
+++ b/src/Render/Unlit/Textured/Pipeline.hs
@@ -14,7 +14,6 @@
 import Control.Monad.Trans.Resource (ResourceT)
 import Data.Tagged (Tagged(..))
 import Vulkan.Core10 qualified as Vk
-import Vulkan.Zero (zero)
 
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
@@ -22,7 +21,7 @@
 import Render.DescSets.Set0 (Scene, vertexPos, instanceTransform)
 import Render.Unlit.Textured.Model qualified as Model
 
-type Config = Pipeline.Config '[Scene] Model.VertexAttrs Model.InstanceAttrs
+type Config = Pipeline.Configure Pipeline ()
 type Pipeline = Pipeline.Pipeline '[Scene] Model.VertexAttrs Model.InstanceAttrs
 
 allocate
@@ -58,7 +57,7 @@
   pure p
 
 config :: Tagged Scene DsBindings -> Config
-config (Tagged set0) = zero
+config (Tagged set0) = Pipeline.baseConfig
   { Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
   , Pipeline.cVertexCode   = Just vertCode
   , Pipeline.cVertexInput  = vertexInput
@@ -103,11 +102,12 @@
     layout(location = 2) flat out ivec2 fTextureIds;
 
     void main() {
+      vec4 fPosition = iModel * vec4(vPosition, 1.0);
+
       gl_Position
         = scene.projection
         * scene.view
-        * iModel
-        * vec4(vPosition, 1.0);
+        * fPosition;
 
       fTexCoord     = vTexCoord * iTextureScaleOffset.st + iTextureScaleOffset.pq;
       fTextureGamma = iTextureGamma;
diff --git a/src/Render/Unlit/TileMap/Model.hs b/src/Render/Unlit/TileMap/Model.hs
new file mode 100644
--- /dev/null
+++ b/src/Render/Unlit/TileMap/Model.hs
@@ -0,0 +1,203 @@
+module Render.Unlit.TileMap.Model
+  ( Model
+  , VertexAttrs
+  , vkVertexAttrs
+
+  , InstanceAttrs(..)
+  -- , instanceAttrs
+
+  , StorableAttrs
+  -- , storableAttrs1
+
+  , InstanceBuffers(..)
+
+  , TileMapParams(..)
+  , vkInstanceTileMap
+
+  , allocateInstancesWith
+  , allocateInstancesCoherent
+  , allocateInstancesCoherent_
+  , updateCoherentResize_
+  , Transform
+  ) where
+
+import RIO
+
+import Foreign (Storable(..))
+import Geomancy (IVec4, Transform, Vec2)
+import Geomancy.Vec3 qualified as Vec3
+import RIO.Vector.Storable qualified as Storable
+import UnliftIO.Resource (MonadResource, ReleaseKey, ResourceT, allocate)
+import Vulkan.Core10 qualified as Vk
+import Vulkan.NamedType ((:::))
+import Vulkan.Zero (Zero(..))
+
+import Engine.Vulkan.Types (HasVulkan)
+import Resource.Buffer qualified as Buffer
+import Resource.Model qualified as Model
+
+type Model buf = Model.Indexed buf Vec3.Packed VertexAttrs
+
+type VertexAttrs = "uv" ::: Vec2
+
+vkVertexAttrs :: [Vk.Format]
+vkVertexAttrs =
+  [ Vk.FORMAT_R32G32_SFLOAT -- vTexCoord :: vec2
+  ]
+
+-- | Data for a single element.
+data InstanceAttrs = InstanceAttrs
+  { tilemapParams :: TileMapParams
+  , transformMat4 :: Transform
+  }
+
+instance Zero InstanceAttrs where
+  zero = InstanceAttrs
+    { tilemapParams = zero
+    , transformMat4 = mempty
+    }
+
+-- | Intermediate data to be shipped.
+type StorableAttrs =
+  ( Storable.Vector TileMapParams
+  , Storable.Vector Transform
+  )
+
+-- | GPU-bound data.
+data InstanceBuffers tilemapStage transformStage = InstanceBuffers
+  { ibTileMap   :: InstanceTileMap tilemapStage
+  , ibTransform :: InstanceTransform transformStage
+  }
+
+type InstanceTileMap stage = Buffer.Allocated stage TileMapParams
+
+type InstanceTransform stage = Buffer.Allocated stage Transform
+
+instance Model.HasVertexBuffers (InstanceBuffers tilemapStage transformStage) where
+  type VertexBuffersOf (InstanceBuffers tilemapStage transformStage) = InstanceAttrs
+
+  {-# INLINE getVertexBuffers #-}
+  getVertexBuffers InstanceBuffers{..} =
+    [ Buffer.aBuffer ibTileMap
+    , Buffer.aBuffer ibTransform
+    ]
+
+  {-# INLINE getInstanceCount #-}
+  getInstanceCount InstanceBuffers{..} =
+    min
+      (Buffer.aUsed ibTileMap)
+      (Buffer.aUsed ibTransform)
+
+data TileMapParams = TileMapParams
+  { tmpTextureIds         :: IVec4
+  , tmpViewOffset         :: Vec2
+  , tmpViewportSize       :: Vec2
+  , tmpMapTextureSize     :: Vec2
+  , tmpTilesetTextureSize :: Vec2
+  , tmpTileSize           :: Vec2
+  }
+  deriving (Show)
+
+instance Zero TileMapParams where
+  zero = TileMapParams
+    { tmpTextureIds         = 0
+    , tmpViewOffset         = 0
+    , tmpViewportSize       = 1
+    , tmpMapTextureSize     = 1
+    , tmpTilesetTextureSize = 1
+    , tmpTileSize           = 1
+    }
+
+instance Storable TileMapParams where
+  alignment ~_ = 4
+
+  sizeOf ~_ = 16 + 8 + 8 + 8 + 8 + 8
+
+  poke ptr TileMapParams{..} = do
+    pokeByteOff ptr  0 tmpTextureIds
+    pokeByteOff ptr 16 tmpViewOffset
+    pokeByteOff ptr 24 tmpViewportSize
+    pokeByteOff ptr 32 tmpMapTextureSize
+    pokeByteOff ptr 40 tmpTilesetTextureSize
+    pokeByteOff ptr 48 tmpTileSize
+
+  peek ptr = do
+    tmpTextureIds         <- peekByteOff ptr  0
+    tmpViewOffset         <- peekByteOff ptr 16
+    tmpViewportSize       <- peekByteOff ptr 24
+    tmpMapTextureSize     <- peekByteOff ptr 32
+    tmpTilesetTextureSize <- peekByteOff ptr 48
+    tmpTileSize           <- peekByteOff ptr 56
+    pure TileMapParams{..}
+
+vkInstanceTileMap :: [Vk.Format]
+vkInstanceTileMap =
+  [ Vk.FORMAT_R32G32B32A32_SINT -- tmpTextureIds         :: IVec4
+  , Vk.FORMAT_R32G32_SFLOAT     -- tmpViewOffset         :: Vec2
+  , Vk.FORMAT_R32G32_SFLOAT     -- tmpViewportSize       :: Vec2
+  , Vk.FORMAT_R32G32_SFLOAT     -- tmpMapTextureSize     :: Vec2
+  , Vk.FORMAT_R32G32_SFLOAT     -- tmpTilesetTextureSize :: Vec2
+  , Vk.FORMAT_R32G32_SFLOAT     -- tmpTileSize           :: Vec2
+  ]
+
+allocateInstancesWith
+  :: ( MonadResource m
+     , MonadUnliftIO m
+     )
+  => (Vk.BufferUsageFlagBits -> Int -> Storable.Vector TileMapParams -> m (InstanceTileMap texture))
+  -> (Vk.BufferUsageFlagBits -> Int -> Storable.Vector Transform -> m (InstanceTransform transform))
+  -> (forall stage a . Buffer.Allocated stage a -> m ())
+  -> [InstanceAttrs]
+  -> m (ReleaseKey, InstanceBuffers texture transform)
+allocateInstancesWith createTextures createTransforms bufferDestroy instances = do
+  ul <- askUnliftIO
+  allocate (create ul) (destroy ul)
+  where
+    textures   = Storable.fromList $ map tilemapParams instances
+    transforms = Storable.fromList $ map transformMat4 instances
+    numInstances = Storable.length textures
+
+    create (UnliftIO ul) = ul do
+      ibTileMap   <- createTextures   Vk.BUFFER_USAGE_VERTEX_BUFFER_BIT numInstances textures
+      ibTransform <- createTransforms Vk.BUFFER_USAGE_VERTEX_BUFFER_BIT numInstances transforms
+      pure InstanceBuffers{..}
+
+    destroy (UnliftIO ul) InstanceBuffers{..} = ul do
+      bufferDestroy ibTileMap
+      bufferDestroy ibTransform
+
+allocateInstancesCoherent
+  :: ( MonadReader env m
+     , HasVulkan env
+     , MonadResource m
+     , MonadUnliftIO m
+     )
+  => [InstanceAttrs]
+  -> m (ReleaseKey, InstanceBuffers 'Buffer.Coherent 'Buffer.Coherent)
+allocateInstancesCoherent instances = do
+  context <- ask
+  allocateInstancesWith
+    (Buffer.createCoherent context)
+    (Buffer.createCoherent context)
+    (Buffer.destroy context)
+    instances
+
+allocateInstancesCoherent_
+  :: (HasVulkan env)
+  => Int
+  -> ResourceT (RIO env) (InstanceBuffers 'Buffer.Coherent 'Buffer.Coherent)
+allocateInstancesCoherent_ n =
+  fmap snd $ allocateInstancesCoherent (replicate n zero)
+
+updateCoherentResize_
+  :: ( HasVulkan context
+     , MonadUnliftIO m
+     )
+  => context
+  -> InstanceBuffers 'Buffer.Coherent 'Buffer.Coherent
+  -> (Storable.Vector TileMapParams, Storable.Vector Transform)
+  -> m (InstanceBuffers 'Buffer.Coherent 'Buffer.Coherent)
+updateCoherentResize_ context InstanceBuffers{..} (textures, transforms) =
+  InstanceBuffers
+    <$> Buffer.updateCoherentResize_ context ibTileMap textures
+    <*> Buffer.updateCoherentResize_ context ibTransform transforms
diff --git a/src/Render/Unlit/TileMap/Pipeline.hs b/src/Render/Unlit/TileMap/Pipeline.hs
new file mode 100644
--- /dev/null
+++ b/src/Render/Unlit/TileMap/Pipeline.hs
@@ -0,0 +1,179 @@
+module Render.Unlit.TileMap.Pipeline
+  ( Pipeline
+  , allocate
+  , allocateBlend
+
+  , Config
+  , config
+  , configBlend
+  ) where
+
+import RIO
+import Render.DescSets.Set0.Code
+
+import Control.Monad.Trans.Resource (ResourceT)
+import Data.Tagged (Tagged(..))
+import Vulkan.Core10 qualified as Vk
+
+import Engine.Vulkan.Pipeline qualified as Pipeline
+import Engine.Vulkan.Types (HasVulkan, HasRenderPass(..), DsBindings)
+import Render.Code (compileVert, compileFrag, glsl)
+import Render.DescSets.Set0 (Scene, vertexPos, instanceTransform)
+import Render.Unlit.TileMap.Model qualified as Model
+
+type Config = Pipeline.Configure Pipeline ()
+type Pipeline = Pipeline.Pipeline '[Scene] Model.VertexAttrs Model.InstanceAttrs
+
+allocate
+  :: ( HasVulkan env
+     , HasRenderPass renderpass
+     )
+  => Vk.SampleCountFlagBits
+  -> Tagged Scene DsBindings
+  -> renderpass
+  -> ResourceT (RIO env) Pipeline
+allocate multisample tset0 rp = do
+  (_, p) <- Pipeline.allocate
+    Nothing
+    multisample
+    (config tset0)
+    rp
+  pure p
+
+allocateBlend
+  :: ( HasVulkan env
+     , HasRenderPass renderpass
+     )
+  => Vk.SampleCountFlagBits
+  -> Tagged Scene DsBindings
+  -> renderpass
+  -> ResourceT (RIO env) Pipeline
+allocateBlend multisample tset0 rp = do
+  (_, p) <- Pipeline.allocate
+    Nothing
+    multisample
+    (configBlend tset0)
+    rp
+  pure p
+
+config :: Tagged Scene DsBindings -> Config
+config (Tagged set0) = Pipeline.baseConfig
+  { Pipeline.cDescLayouts  = Tagged @'[Scene] [set0]
+  , Pipeline.cVertexCode   = Just vertCode
+  , Pipeline.cVertexInput  = vertexInput
+  , Pipeline.cFragmentCode = Just fragCode
+  }
+  where
+    vertexInput = Pipeline.vertexInput
+      [ vertexPos
+      , (Vk.VERTEX_INPUT_RATE_VERTEX,   Model.vkVertexAttrs)
+      , (Vk.VERTEX_INPUT_RATE_INSTANCE, Model.vkInstanceTileMap)
+      , instanceTransform
+      ]
+
+configBlend :: Tagged Scene DsBindings -> Config
+configBlend tset0 = (config tset0)
+  { Pipeline.cBlend      = True
+  , Pipeline.cDepthWrite = False
+  }
+
+vertCode :: ByteString
+vertCode =
+  $(compileVert [glsl|
+    #version 450
+    #extension GL_ARB_separate_shader_objects : enable
+
+    ${set0binding0}
+
+    // vertexPos
+    layout(location = 0) in vec3 vPosition;
+    // vertexAttrs
+    layout(location = 1) in vec2 vTexCoord;
+    // tilemapParams
+    layout(location = 2) in ivec4 iTextureIds; // combined: tileset, tileset sampler, map, repeat
+    layout(location = 3) in vec2  iViewOffset;
+    layout(location = 4) in vec2  iViewportSize;
+    layout(location = 5) in vec2  iMapTextureSize;
+    layout(location = 6) in vec2  iTilesetTextureSize;
+    layout(location = 7) in vec2  iTileSize;
+
+    // transformMat
+    layout(location = 8) in mat4 iModel;
+
+    layout(location = 0)      out  vec2 fTexCoord;
+    layout(location = 1)      out  vec2 fPixCoord;
+    layout(location = 2) flat out ivec4 fTextureIds;
+    layout(location = 3) flat out  vec2 fTilesetTextureSize;
+    layout(location = 4) flat out  vec2 fTileSize;
+
+    void main() {
+      vec4 fPosition = iModel * vec4(vPosition, 1.0);
+
+      gl_Position
+        = scene.projection
+        * scene.view
+        * fPosition;
+
+      fPixCoord = (vTexCoord * iViewportSize) + iViewOffset;
+      fTexCoord = fPixCoord / iMapTextureSize / iTileSize;
+
+      fTextureIds = iTextureIds;
+      fTilesetTextureSize = iTilesetTextureSize;
+      fTileSize = iTileSize;
+    }
+  |])
+
+fragCode :: ByteString
+fragCode =
+  $(compileFrag [glsl|
+    #version 450
+    #extension GL_ARB_separate_shader_objects : enable
+    #extension GL_EXT_nonuniform_qualifier : enable
+
+    ${set0binding1}
+    ${set0binding2}
+
+    layout(location = 0) in vec2 fTexCoord;
+    layout(location = 1) in vec2 fPixCoord;
+
+    // combined: tileset, tileset sampler, map, repeat
+    layout(location = 2) flat in ivec4 fTextureIds;
+    layout(location = 3) flat in vec2 fTilesetTextureSize;
+    layout(location = 4) flat in vec2 fTileSize;
+
+    layout(location = 0) out vec4 oColor;
+
+    int tilesetTextureIx = fTextureIds[0];
+    int tilesetSamplerIx = fTextureIds[1];
+    int mapTextureIx     = fTextureIds[2];
+    int repeatTiles      = fTextureIds[3];
+
+    // TODO
+    // const vec4 fTextureGamma = vec4(1.0);
+
+    void main() {
+      if (repeatTiles == 0 && (fTexCoord.x < 0.0 || fTexCoord.y < 0.0 || fTexCoord.x > 1.0 || fTexCoord.y > 1.0)) {
+        discard;
+      }
+
+      vec4 map = texture(
+        sampler2D(
+          textures[nonuniformEXT(mapTextureIx)],
+          samplers[7] // nearest
+        ),
+        fTexCoord
+      );
+
+      vec2 spriteOffset = floor(map.xy * 256.0) * fTileSize;
+      vec2 spriteCoord = mod(fPixCoord, fTileSize);
+      vec2 spriteUV = (spriteOffset + spriteCoord) / fTilesetTextureSize;
+
+      oColor = texture(
+        sampler2D(
+          textures[nonuniformEXT(tilesetTextureIx)],
+          samplers[nonuniformEXT(tilesetSamplerIx)]
+        ),
+        spriteUV
+      );
+    }
+  |])
diff --git a/src/Stage/Loader/Setup.hs b/src/Stage/Loader/Setup.hs
--- a/src/Stage/Loader/Setup.hs
+++ b/src/Stage/Loader/Setup.hs
@@ -122,6 +122,7 @@
 loaderStage loadAction nextStage uiSettings = Stage
   { sTitle = "Loader"
 
+  , sAllocateRP = allocateRenderPass
   , sAllocateP  = allocatePipelines
   , sInitialRS  = initialRunState loadAction nextStage uiSettings
   , sInitialRR  = initialFrameResources uiSettings
@@ -133,6 +134,14 @@
   , sAfterLoop    = pure
   }
   where
+    allocateRenderPass swapchain = do
+      Basic.allocate
+        Basic.Settings
+          { sShadowLayers = 1
+          , sShadowSize   = 1
+          }
+        swapchain
+
     allocatePipelines swapchain rps = do
       logDebug "Allocating loader pipelines"
       (_, samplers) <- Samplers.allocate
