diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
 # Changelog for keid-render-basic
 
+## 0.1.3.0
+
+- Replaced RIO in resource loaders with constraints.
+- `ForwardMSAA` and `ShadowMap` updated to `keid-core-0.1.3.0`.
+
 ## 0.1.2.1
 
 - Fixed missing embedded textures in source distribution.
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.2.1
+version:        0.1.3.0
 synopsis:       Basic rendering programs for Keid engine.
 category:       Game Engine
 author:         IC Rainbow
diff --git a/src/Render/ForwardMsaa.hs b/src/Render/ForwardMsaa.hs
--- a/src/Render/ForwardMsaa.hs
+++ b/src/Render/ForwardMsaa.hs
@@ -1,17 +1,22 @@
-module Render.ForwardMsaa where
+module Render.ForwardMsaa
+  ( ForwardMsaa(..)
+  , allocateMsaa
+  , updateMsaa
+  , usePass
+  ) where
 
 import RIO
 
 import Control.Monad.Trans.Resource qualified as Resource
 import Data.Bits ((.|.))
 import Data.Vector qualified as Vector
-import RIO.Vector.Partial ((!))
 import Vulkan.Core10 qualified as Vk
 import Vulkan.Utils.Debug qualified as Debug
 import Vulkan.Zero (zero)
 
 import Engine.Types.RefCounted (RefCounted, newRefCounted, releaseRefCounted, resourceTRefCount)
 import Engine.Vulkan.Types (HasVulkan(..), HasSwapchain(..), HasRenderPass(..), RenderPass(..), MonadVulkan)
+import Render.Pass (usePass)
 import Resource.Image (AllocatedImage)
 import Resource.Image qualified as Image
 
@@ -44,14 +49,14 @@
   :: ( Resource.MonadResource m
      , MonadVulkan env m
      , HasLogFunc env
-     , HasSwapchain context
+     , HasSwapchain swapchain
      )
-  => context
+  => swapchain
   -> m ForwardMsaa
-allocateMsaa context = do
+allocateMsaa swapchain = do
   logDebug "Allocating ForwardMsaa resources"
-  (_rpKey, renderPass) <- allocateRenderPassMsaa context
-  (refcounted, color, depth, framebuffers) <- allocateFramebufferMsaa context renderPass
+  (_rpKey, renderPass) <- allocateRenderPassMsaa swapchain
+  (refcounted, color, depth, framebuffers) <- allocateFramebufferMsaa swapchain renderPass
 
   pure ForwardMsaa
     { fmRenderPass   = renderPass
@@ -65,7 +70,7 @@
   where
     fullSurface = Vk.Rect2D
       { Vk.offset = zero
-      , Vk.extent = getSurfaceExtent context
+      , Vk.extent = getSurfaceExtent swapchain
       }
     clear = Vector.fromList
       [ clearColor
@@ -78,14 +83,14 @@
   :: ( Resource.MonadResource m
      , MonadVulkan env m
      , HasLogFunc env
-     , HasSwapchain context
+     , HasSwapchain swapchain
      )
-  => context
+  => swapchain
   -> ForwardMsaa
   -> m ForwardMsaa
-updateMsaa context old@ForwardMsaa{fmRelease, fmRenderPass} = do
+updateMsaa swapchain old@ForwardMsaa{fmRelease, fmRenderPass} = do
   releaseRefCounted fmRelease
-  (refcounted, color, depth, framebuffers) <- allocateFramebufferMsaa context fmRenderPass
+  (refcounted, color, depth, framebuffers) <- allocateFramebufferMsaa swapchain fmRenderPass
   pure old
     { fmColor        = color
     , fmDepth        = depth
@@ -96,7 +101,7 @@
   where
     fullSurface = Vk.Rect2D
       { Vk.offset = zero
-      , Vk.extent = getSurfaceExtent context
+      , Vk.extent = getSurfaceExtent swapchain
       }
 
 -- ** Render pass
@@ -104,16 +109,16 @@
 allocateRenderPassMsaa
   :: ( MonadVulkan env m
      , Resource.MonadResource m
-     , HasSwapchain context
+     , HasSwapchain swapchain
      )
-  => context
+  => swapchain
   -> m (Resource.ReleaseKey, Vk.RenderPass)
-allocateRenderPassMsaa context = do
+allocateRenderPassMsaa swapchain = do
   device <- asks getDevice
   let
-    format = getSurfaceFormat context
-    depthFormat = getDepthFormat context
-    msaa = getMultisample context
+    format = getSurfaceFormat swapchain
+    depthFormat = getDepthFormat swapchain
+    msaa = getMultisample swapchain
 
     attachments =
       [ color format msaa
@@ -225,25 +230,44 @@
   :: ( Resource.MonadResource m
      , MonadVulkan env m
      , HasLogFunc env
-     , HasSwapchain context
+     , HasSwapchain swapchain
      )
-  => context
+  => swapchain
   -> Vk.RenderPass
   -> m FramebuffersMsaa
-allocateFramebufferMsaa context renderPass = do
-  device <- asks getDevice
-  context' <- asks (, context)
-  let extent@Vk.Extent2D{width, height} = getSurfaceExtent context
+allocateFramebufferMsaa swapchain renderPass = do
+  context <- ask
+  let extent@Vk.Extent2D{width, height} = getSurfaceExtent swapchain
 
   (colorKey, color) <- Resource.allocate
-    (Image.createColorResource context' extent)
-    (Image.destroyAllocatedImage context')
+    ( Image.create
+        context
+        (Just $ "ForwardMSAA.color")
+        Vk.IMAGE_ASPECT_COLOR_BIT
+        extent
+        1
+        1
+        (getMultisample swapchain)
+        (getSurfaceFormat swapchain)
+        Vk.IMAGE_USAGE_COLOR_ATTACHMENT_BIT
+    )
+    (Image.destroy context)
 
   (depthKey, depth) <- Resource.allocate
-    (Image.createDepthResource context' extent Nothing)
-    (Image.destroyAllocatedImage context')
+    ( Image.create
+        context
+        (Just $ "ForwardMSAA.depth")
+        Vk.IMAGE_ASPECT_DEPTH_BIT
+        extent
+        1
+        1
+        (getMultisample swapchain)
+        (getDepthFormat swapchain)
+        Vk.IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
+    )
+    (Image.destroy context)
 
-  perView <- for (getSwapchainViews context) \colorResolve -> do
+  perView <- Vector.iforM (getSwapchainViews swapchain) \ix colorResolve -> do
     let
       attachments = Vector.fromList
         [ Image.aiImageView color
@@ -259,8 +283,13 @@
         , Vk.layers      = 1
         }
 
-    Vk.withFramebuffer device fbCI Nothing Resource.allocate
+      device = getDevice context
+    res <- Vk.withFramebuffer device fbCI Nothing Resource.allocate
+    Debug.nameObject device (snd res) $
+      "ForwardMSAA.FB:" <> fromString (show @Int ix)
 
+    pure res
+
   let (framebufferKeys, framebuffers) = Vector.unzip perView
   releaseDebug <- toIO $ logDebug "Releasing ForwardMsaa resources"
   release <- newRefCounted do
@@ -270,18 +299,3 @@
     traverse_ Resource.release framebufferKeys
 
   pure (release, color, depth, framebuffers)
-
-usePass :: (MonadIO io, HasRenderPass a) => a -> Word32 -> Vk.CommandBuffer -> io r -> io r
-usePass render imageIndex cb =
-  Vk.cmdUseRenderPass
-    cb
-    (beginInfo render imageIndex)
-    Vk.SUBPASS_CONTENTS_INLINE
-
-beginInfo :: HasRenderPass a => a -> Word32 -> Vk.RenderPassBeginInfo '[]
-beginInfo rp imageIndex = zero
-  { Vk.renderPass  = getRenderPass rp
-  , Vk.framebuffer = getFramebuffers rp ! fromIntegral imageIndex
-  , Vk.renderArea  = getRenderArea rp
-  , Vk.clearValues = getClearValues rp
-  }
diff --git a/src/Render/Lit/Material/Collect.hs b/src/Render/Lit/Material/Collect.hs
--- a/src/Render/Lit/Material/Collect.hs
+++ b/src/Render/Lit/Material/Collect.hs
@@ -9,6 +9,7 @@
 import RIO
 
 import RIO.Map qualified as Map
+import RIO.Set qualified as Set
 import RIO.Vector.Storable qualified as Storable
 
 import Render.Lit.Material (shiftTextures)
@@ -37,7 +38,7 @@
   -> t (SceneModel models textures)
   -> Storable.Vector Material
 sceneMaterials loadedModels combinedTextures =
-  Storable.fromList . Map.elems . foldl' beep mempty
+  Storable.fromList . map snd . Map.elems . snd . foldl' beep (mempty, mempty)
   where
     beep acc SceneModel{..} =
       let
@@ -50,9 +51,9 @@
   :: Foldable t
   => Text
   -> Int32
-  -> Map Int Material
+  -> (Map Text (Set Int), Map Int (Text, Material))
   -> t Lit.MaterialNode
-  -> Map Int Material
+  -> (Map Text (Set Int), Map Int (Text, Material))
 modelMaterials label textureOffset =
   foldr (nodeMaterials label textureOffset)
 
@@ -60,22 +61,37 @@
   :: Text
   -> Int32
   -> Lit.MaterialNode
-  -> Map Int Material
-  -> Map Int Material
-nodeMaterials label textureStart Lit.MaterialNode{mnMaterialIx, mnMaterial} collection =
+  -> (Map Text (Set Int), Map Int (Text, Material))
+  -> (Map Text (Set Int), Map Int (Text, Material))
+nodeMaterials label textureStart Lit.MaterialNode{mnMaterialIx, mnMaterial} (ids, collection) =
   case Map.lookup mnMaterialIx collection of
     Nothing ->
-      Map.insert mnMaterialIx newMaterial collection
-    Just oldMaterial ->
+      ( Map.insertWith mappend label (Set.singleton mnMaterialIx) ids
+      , Map.insert mnMaterialIx (label, newMaterial) collection
+      )
+    Just (oldLabel, oldMaterial) ->
       if newMaterial /= oldMaterial then
         error $ unlines
           [ "Ouf... The material indices are clashing for " <> show label
-          , "At: " <> show mnMaterialIx
+          , "ID: " <> show mnMaterialIx <> " from " <> show oldLabel
+          , ""
           , "Old: " <> show oldMaterial
           , "Now: " <> show mnMaterial
+          , ""
+          , "Bad material offset? Known material IDs:"
+          , unlines do
+              (knownLabel, knownIds) <- Map.toList ids
+              pure $ show knownLabel <> ": " <> show (Set.toList knownIds)
+          , "Try setting " <> show label <> " material offset to " <> show suggestOffset
           ]
       else
-        collection
+        (ids, collection)
   where
     newMaterial =
       shiftTextures textureStart mnMaterial
+
+    suggestOffset :: Int
+    suggestOffset =
+      (+ 1) . foldr max 0 $
+        catMaybes . map Set.lookupMax $
+          Map.elems (Map.delete label ids)
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
@@ -1,12 +1,16 @@
 {-# LANGUAGE OverloadedLists #-}
 
-module Render.ShadowMap.RenderPass where
+module Render.ShadowMap.RenderPass
+  ( ShadowMap(..)
+  , allocate
+  , usePass
+  ) where
 
 import RIO
 
 import Control.Monad.Trans.Resource qualified as Resource
+import Data.Bits ((.|.))
 import Data.Vector qualified as Vector
-import RIO.Vector.Partial ((!))
 import Vulkan.Core10 qualified as Vk
 import Vulkan.Core11.Promoted_From_VK_KHR_multiview qualified as Khr
 import Vulkan.NamedType ((:::))
@@ -16,6 +20,7 @@
 
 import Engine.Types.RefCounted (RefCounted, newRefCounted, resourceTRefCount)
 import Engine.Vulkan.Types (HasVulkan(..), HasSwapchain(..), HasRenderPass(..), RenderPass(..), MonadVulkan)
+import Render.Pass (usePass)
 import Resource.Image (AllocatedImage)
 import Resource.Image qualified as Image
 
@@ -85,23 +90,24 @@
 allocateRenderPass
   :: ( MonadVulkan env m
      , Resource.MonadResource m
-     , HasSwapchain context
+     , HasSwapchain swapchain
      )
-  => context
+  => swapchain
   -> Word32
   -> Word32
   -> m (Resource.ReleaseKey, Vk.RenderPass)
-allocateRenderPass context viewMask correlationMask = do
+allocateRenderPass swapchain viewMask correlationMask = do
   device <- asks getDevice
-  let depthFormat = getDepthFormat context
 
-  res@(_key, object) <- Vk.withRenderPass device (createInfo depthFormat) Nothing Resource.allocate
+  res@(_key, object) <- Vk.withRenderPass device createInfo Nothing Resource.allocate
   Debug.nameObject device object "ShadowMap.RP"
   pure res
   where
-    createInfo depthFormat =
+    depthFormat = getDepthFormat swapchain
+
+    createInfo =
       zero
-        { Vk.attachments  = Vector.fromList [depth depthFormat]
+        { Vk.attachments  = Vector.fromList [depth]
         , Vk.subpasses    = Vector.fromList [subpass]
         , Vk.dependencies = Vector.fromList [pre, post]
         }
@@ -112,7 +118,7 @@
         }
       :& ()
 
-    depth depthFormat = zero
+    depth = zero
       { Vk.format         = depthFormat
       , Vk.samples        = Vk.SAMPLE_COUNT_1_BIT
       , Vk.loadOp         = Vk.ATTACHMENT_LOAD_OP_CLEAR
@@ -167,21 +173,29 @@
   :: ( Resource.MonadResource m
      , MonadVulkan env m
      , HasLogFunc env
-     , HasSwapchain context
+     , HasSwapchain swapchain
      )
-  => context
+  => swapchain
   -> Vk.Extent2D
   -> Word32
   -> Vk.RenderPass
   -> m Framebuffers
-allocateFramebuffer context extent layerCount renderPass = do
-  device <- asks getDevice
-  context' <- asks (, context)
-  let Vk.Extent2D{width, height} = extent
+allocateFramebuffer swapchain extent layerCount renderPass = do
+  context <- ask
 
   (depthKey, depth) <- Resource.allocate
-    (Image.createDepthResource context' extent $ Just layerCount)
-    (Image.destroyAllocatedImage context')
+    ( Image.create
+        context
+        (Just "ShadowMap.depth")
+        Vk.IMAGE_ASPECT_DEPTH_BIT
+        extent
+        1
+        layerCount
+        Vk.SAMPLE_COUNT_1_BIT
+        (getDepthFormat swapchain)
+        (Vk.IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT .|. Vk.IMAGE_USAGE_SAMPLED_BIT)
+    )
+    (Image.destroy context)
 
   let
     attachments = Vector.fromList
@@ -195,6 +209,7 @@
     -}
     fbNumLayers = 1
 
+    Vk.Extent2D{width, height} = extent
     fbCI = zero
       { Vk.renderPass  = renderPass
       , Vk.width       = width
@@ -202,6 +217,8 @@
       , Vk.attachments = attachments
       , Vk.layers      = fbNumLayers
       }
+
+    device = getDevice context
   (framebufferKey, framebuffer) <- Vk.withFramebuffer device fbCI Nothing Resource.allocate
   Debug.nameObject device framebuffer "ShadowMap.FB"
 
@@ -212,18 +229,3 @@
     Resource.release framebufferKey
 
   pure (release, depth, framebuffer)
-
-usePass :: (MonadIO io, HasRenderPass a) => a -> Word32 -> Vk.CommandBuffer -> io r -> io r
-usePass render imageIndex cb =
-  Vk.cmdUseRenderPass
-    cb
-    (beginInfo render imageIndex)
-    Vk.SUBPASS_CONTENTS_INLINE
-
-beginInfo :: HasRenderPass a => a -> Word32 -> Vk.RenderPassBeginInfo '[]
-beginInfo rp imageIndex = zero
-  { Vk.renderPass  = getRenderPass rp
-  , Vk.framebuffer = getFramebuffers rp ! fromIntegral imageIndex -- XXX: we only have one
-  , Vk.renderArea  = getRenderArea rp
-  , Vk.clearValues = getClearValues rp
-  }
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
@@ -89,7 +89,7 @@
         outColor = texture(
           samplerCube(
             cubes[nonuniformEXT(scene.envCubeId)],
-            samplers[2] // XXX: linear/mip0/repeat
+            samplers[3] // XXX: linear
           ),
           fragUVW
         );
diff --git a/src/Resource/Font.hs b/src/Resource/Font.hs
--- a/src/Resource/Font.hs
+++ b/src/Resource/Font.hs
@@ -15,8 +15,7 @@
 import UnliftIO.Resource qualified as Resource
 import Vulkan.Core10 qualified as Vk
 
-import Engine.Types (StageRIO)
-import Engine.Vulkan.Types (Queues)
+import Engine.Vulkan.Types (MonadVulkan, Queues)
 import Resource.Font.EvanW qualified as EvanW
 import Resource.Source (Source)
 import Resource.Texture (Texture, Flat)
@@ -33,11 +32,15 @@
 
 allocateCollection
   :: ( Traversable collection
+     , MonadVulkan env m
+     , HasLogFunc env
+     , MonadThrow m
+     , Resource.MonadResource m
      , HasCallStack
      )
   => Queues Vk.CommandPool
   -> collection Config
-  -> StageRIO st (Resource.ReleaseKey, collection Font)
+  -> m (Resource.ReleaseKey, collection Font)
 allocateCollection pools collection = do
   collected <- for collection $
     withFrozenCallStack $ allocateFont pools
@@ -55,10 +58,15 @@
   }
 
 allocateFont
-  :: HasCallStack
+  :: ( HasCallStack
+     , MonadVulkan env m
+     , HasLogFunc env
+     , MonadThrow m
+     , Resource.MonadResource m
+     )
   => Queues Vk.CommandPool
   -> Config
-  -> StageRIO st (Resource.ReleaseKey, Font)
+  -> m (Resource.ReleaseKey, Font)
 allocateFont pools Config{..} = do
   context <- ask
 
diff --git a/src/Stage/Loader/Render.hs b/src/Stage/Loader/Render.hs
--- a/src/Stage/Loader/Render.hs
+++ b/src/Stage/Loader/Render.hs
@@ -7,18 +7,17 @@
 
 import RIO
 
-import RIO.State (gets)
-import Vulkan.Core10 qualified as Vk
-
 import Engine.Types qualified as Engine
 import Engine.Vulkan.Pipeline qualified as Pipeline
 import Engine.Vulkan.Swapchain qualified as Swapchain
 import Engine.Worker qualified as Worker
-import Render.Basic qualified as Basic
-import Render.DescSets.Set0 qualified as Set0
 import Render.Draw qualified as Draw
-import Render.ForwardMsaa qualified as ForwardMsaa
+import Render.Pass (usePass)
+import RIO.State (gets)
+import Vulkan.Core10 qualified as Vk
 
+import Render.Basic qualified as Basic
+import Render.DescSets.Set0 qualified as Set0
 import Stage.Loader.Types (FrameResources(..), RunState(..))
 import Stage.Loader.UI qualified as UI
 
@@ -39,12 +38,12 @@
   (_context, Engine.Frame{fSwapchainResources, fRenderpass, fPipelines}) <- ask
   let Basic.Pipelines{..} = fPipelines
 
-  ui <- mapRIO fst $ gets rsUI
+  ui <- gets rsUI
   uiMessages <- traverse Worker.readObservedIO (UI.messages frUI)
   background <- Worker.readObservedIO (UI.background frUI)
   spinner <- Worker.readObservedIO (UI.spinner frUI)
 
-  ForwardMsaa.usePass (Basic.rpForwardMsaa fRenderpass) imageIndex cb do
+  usePass (Basic.rpForwardMsaa fRenderpass) imageIndex cb do
     Swapchain.setDynamicFullscreen cb fSwapchainResources
 
     Set0.withBoundSet0 frSceneUi pWireframe cb do
