diff --git a/aztecs-sdl.cabal b/aztecs-sdl.cabal
--- a/aztecs-sdl.cabal
+++ b/aztecs-sdl.cabal
@@ -1,6 +1,6 @@
 cabal-version: 2.4
 name:          aztecs-sdl
-version:       0.5.0
+version:       0.6.0
 license:       BSD-3-Clause
 license-file:  LICENSE
 maintainer:    matt@hunzinger.me
@@ -27,7 +27,7 @@
     ghc-options:      -Wall
     build-depends:
         base >=4.6 && <5,
-        aztecs >= 0.8 && <0.9,
+        aztecs >= 0.9 && <0.10,
         containers >=0.6,
         deepseq >=1,
         mtl >=2,
diff --git a/src/Aztecs/SDL.hs b/src/Aztecs/SDL.hs
--- a/src/Aztecs/SDL.hs
+++ b/src/Aztecs/SDL.hs
@@ -1,10 +1,17 @@
 {-# LANGUAGE Arrows #-}
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE DeriveGeneric #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE TypeApplications #-}
 
+-- |
+-- Module      : Aztecs.SDL
+-- Copyright   : (c) Matt Hunzinger, 2025
+-- License     : BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  : matt@hunzinger.me
+-- Stability   : provisional
+-- Portability : non-portable (GHC extensions)
 module Aztecs.SDL
   ( -- * Window components
     WindowRenderer (..),
@@ -45,140 +52,149 @@
 where
 
 import Aztecs
-import Aztecs.Camera (addCameraTargets)
+import Aztecs.Camera
 import qualified Aztecs.ECS.Access as A
 import qualified Aztecs.ECS.Query as Q
 import qualified Aztecs.ECS.System as S
 import Aztecs.Input
-  ( InputMotion (..),
-    MouseButton (..),
-    handleKeyboardEvent,
-    keyboardInput,
-    mouseInput,
-  )
-import Control.Arrow (Arrow (..), returnA, (>>>))
+import Control.Arrow
 import Control.DeepSeq
+import Control.Monad
 import Control.Monad.IO.Class
+import Data.Foldable
 import qualified Data.Map.Strict as Map
-import Data.Maybe (fromMaybe, mapMaybe)
+import Data.Maybe
 import qualified Data.Text as T
-import GHC.Generics (Generic)
+import GHC.Generics
 import SDL hiding (InputMotion (..), MouseButton (..), Surface, Texture, Window, windowTitle)
 import qualified SDL
 
-#if !MIN_VERSION_base(4,20,0)
-import Data.Foldable (foldl')
-#endif
-
 -- | Window renderer component.
+--
+-- @since 0.5
 data WindowRenderer = WindowRenderer
   { -- | SDL window.
+    --
+    -- @since 0.5
     windowRendererRaw :: !SDL.Window,
     -- | SDL renderer.
+    --
+    -- @since 0.5
     windowRenderer :: !Renderer
   }
   deriving (Show, Generic)
 
+-- | @since 0.5
 instance Component WindowRenderer
 
+-- | @since 0.5
 instance NFData WindowRenderer where
   rnf = rwhnf
 
 -- | Setup SDL
-setup :: (MonadIO m) => (ArrowAccessSchedule b m arr) => arr () ()
-setup =
-  access (const $ liftIO initializeAll)
-    >>> access
-      ( const $ do
-          A.spawn_ . bundle $ Time 0
-          A.spawn_ $ bundle keyboardInput
-          A.spawn_ $ bundle mouseInput
-      )
+--
+-- @since 0.5
+setup :: (MonadAccess b m, MonadIO m) => m ()
+setup = do
+  liftIO initializeAll
+  A.spawn_ . bundle $ Time 0
+  A.spawn_ $ bundle keyboardInput
+  A.spawn_ $ bundle mouseInput
 
 -- | Update SDL windows
+--
+-- @since 0.5
 update ::
   ( ArrowQueryReader qr,
     ArrowDynamicQueryReader qr,
-    ArrowReaderSystem qr rs,
-    ArrowQueueSystem b qm rs,
-    ArrowReaderSchedule rs arr,
-    ArrowQuery q,
-    ArrowSystem q s,
-    ArrowReaderSystem qr s,
-    ArrowQueueSystem b qm s,
-    ArrowSchedule s arr,
-    MonadIO m,
-    ArrowAccessSchedule b m arr
+    ArrowQuery m q,
+    MonadSystem q s,
+    MonadReaderSystem qr s,
+    MonadIO s,
+    MonadAccess b s,
+    MonadIO m
   ) =>
-  arr () ()
-update =
+  s ()
+update = do
   updateTime
-    >>> addWindows
-    >>> reader (addCameraTargets >>> addSurfaceTargets)
-    >>> buildTextures
-    >>> handleInput
+  join addWindows
+  join addCameraTargets
+  join addSurfaceTargets
+  join buildTextures
+  handleInput
 
 -- | Setup new windows.
+--
+-- @since 0.5
 addWindows ::
   ( ArrowQueryReader q,
     ArrowDynamicQueryReader q,
-    ArrowReaderSystem q s,
-    ArrowQueueSystem b qm s,
-    ArrowReaderSchedule s arr,
-    MonadIO m,
-    ArrowAccessSchedule b m arr
+    MonadReaderSystem q s,
+    MonadIO s,
+    MonadAccess b ma
   ) =>
-  arr () ()
-addWindows = proc () -> do
-  newWindows <- reader $ S.filter (Q.entity &&& Q.fetch @_ @Window) (without @WindowRenderer) -< ()
-  newWindows' <- access $ liftIO . mapM createWindowRenderer -< newWindows
-  reader $ S.queue $ mapM_ insertWindowRenderer -< newWindows'
+  s (ma ())
+addWindows = do
+  newWindows <- S.filter () (Q.entity &&& Q.fetch @_ @Window) (without @WindowRenderer)
+  newWindows' <- liftIO $ mapM createWindowRenderer newWindows
+  return $ mapM_ insertWindowRenderer newWindows'
   where
     createWindowRenderer (eId, window) = do
       sdlWindow <- createWindow (T.pack $ windowTitle window) defaultWindow
       renderer <- createRenderer sdlWindow (-1) defaultRenderer
       return (eId, sdlWindow, renderer)
-    insertWindowRenderer (eId, window, renderer) = A.insert eId (WindowRenderer window renderer)
+    insertWindowRenderer (eId, window, renderer) = A.insert eId . bundle $ WindowRenderer window renderer
 
 -- | Surface texture component.
+--
+-- @since 0.5
 newtype SurfaceTexture = SurfaceTexture
   { -- | SDL texture.
+    --
+    -- @since 0.5
     unSurfaceTexture :: SDL.Texture
   }
   deriving (Generic)
 
+-- | @since 0.5
 instance Component SurfaceTexture
 
+-- | @since 0.5
 instance NFData SurfaceTexture where
   rnf = rwhnf
 
+-- | Query all window textures.
+--
+-- @since 0.5
 allWindowTextures ::
-  (ArrowQueryReader q, ArrowDynamicQueryReader q, ArrowReaderSystem q arr) =>
-  arr () [(WindowRenderer, [(EntityID, Surface, Transform2D, Maybe SurfaceTexture)])]
+  (ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s) =>
+  s [(WindowRenderer, [(EntityID, Surface, Transform2D, Maybe SurfaceTexture)])]
 allWindowTextures =
-  allWindowDraws
-    (arr (const ()))
-    ( proc () -> do
-        e <- Q.entity -< ()
-        surface <- Q.fetch -< ()
-        transform <- Q.fetch -< ()
-        texture <- Q.fetchMaybe -< ()
-        returnA -< (e, surface, transform, texture)
-    )
-    >>> arr (\cs -> map (\(w, cs') -> (w, concatMap snd cs')) cs)
+  map (second (concatMap snd))
+    <$> allWindowDraws
+      (arr (const ()))
+      ( proc () -> do
+          e <- Q.entity -< ()
+          surface <- Q.fetch -< ()
+          transform <- Q.fetch -< ()
+          texture <- Q.fetchMaybe -< ()
+          returnA -< (e, surface, transform, texture)
+      )
 
 -- | Build textures from surfaces in preparation for `drawTextures`.
+--
+-- @since 0.5
 buildTextures ::
   ( ArrowQueryReader q,
     ArrowDynamicQueryReader q,
-    ArrowReaderSystem q s,
-    ArrowReaderSchedule s arr,
-    MonadIO m,
-    ArrowAccessSchedule b m arr
+    MonadReaderSystem q s,
+    MonadAccess b m,
+    MonadIO m
   ) =>
-  arr () ()
-buildTextures =
-  let go windowDraws =
+  s (m ())
+buildTextures = do
+  windowTextures <- allWindowTextures
+  let go =
         mapM_
           ( \(window, cameraDraws) -> do
               let renderer = windowRenderer window
@@ -189,77 +205,70 @@
                     case maybeTexture of
                       Just (SurfaceTexture lastTexture) -> destroyTexture lastTexture
                       Nothing -> return ()
-                    A.insert eId (SurfaceTexture sdlTexture)
+                    A.insert eId . bundle $ SurfaceTexture sdlTexture
                     A.insert
                       eId
-                      ( Size $
+                      ( bundle . Size $
                           transformScale transform
-                            * fromMaybe
-                              (fmap fromIntegral $ V2 (textureWidth textureDesc) (textureHeight textureDesc))
-                              ((fmap (\(Rectangle _ s) -> fmap fromIntegral s) $ surfaceBounds surface))
+                            * maybe (fromIntegral <$> V2 (textureWidth textureDesc) (textureHeight textureDesc)) (\(Rectangle _ s) -> fmap fromIntegral s) (surfaceBounds surface)
                       )
                 )
                 cameraDraws
           )
-          windowDraws
-   in reader allWindowTextures >>> access go
+          windowTextures
+  return go
 
-draw ::
-  ( ArrowQueryReader q,
-    ArrowDynamicQueryReader q,
-    ArrowReaderSystem q s,
-    ArrowReaderSchedule s arr,
-    MonadIO m,
-    ArrowAccessSchedule b m arr
-  ) =>
-  arr () ()
-draw =
-  let go windowDraws =
+-- | Draw all textures to their targetted windows.
+--
+-- @since 0.5
+draw :: (ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s, MonadIO s) => s ()
+draw = do
+  cameraSurfaces <- allCameraSurfaces
+  mapM_
+    ( \(window, cameraDraws) -> do
+        let renderer = windowRenderer window
+        clear renderer
         mapM_
-          ( \(window, cameraDraws) -> do
-              let renderer = windowRenderer window
-              rendererDrawColor renderer $= V4 0 0 0 255
-              clear renderer
+          ( \((camera, cameraTransform), cameraDraws') -> do
+              rendererScale renderer $= fmap realToFrac (cameraScale camera)
+              rendererViewport renderer
+                $= Just
+                  ( Rectangle
+                      (P (fromIntegral <$> transformTranslation cameraTransform))
+                      (fromIntegral <$> cameraViewport camera)
+                  )
               mapM_
-                ( \((camera, cameraTransform), cameraDraws') -> do
-                    rendererScale renderer $= fmap realToFrac (cameraScale camera)
-                    rendererViewport renderer
-                      $= Just
-                        ( Rectangle
-                            (P (fmap fromIntegral $ transformTranslation cameraTransform))
-                            (fmap fromIntegral $ cameraViewport camera)
-                        )
-                    mapM_
-                      ( \(surface, transform, texture) -> do
-                          textureDesc <- queryTexture $ unSurfaceTexture texture
-                          copyEx
-                            renderer
-                            (unSurfaceTexture texture)
-                            (fmap fromIntegral <$> surfaceBounds surface)
-                            ( Just
-                                ( Rectangle
-                                    (fmap fromIntegral . P $ transformTranslation transform)
-                                    ( fromMaybe
-                                        (fmap fromIntegral $ V2 (textureWidth textureDesc) (textureHeight textureDesc))
-                                        ((fmap (\(Rectangle _ s) -> fmap fromIntegral s) $ surfaceBounds surface))
-                                    )
-                                )
-                            )
-                            (realToFrac $ transformRotation transform)
-                            Nothing
-                            (V2 False False)
+                ( \(surface, transform, texture) -> do
+                    textureDesc <- queryTexture $ unSurfaceTexture texture
+                    copyEx
+                      renderer
+                      (unSurfaceTexture texture)
+                      (fmap fromIntegral <$> surfaceBounds surface)
+                      ( Just
+                          ( Rectangle
+                              (fmap fromIntegral . P $ transformTranslation transform)
+                              ( maybe (fromIntegral <$> V2 (textureWidth textureDesc) (textureHeight textureDesc)) (\(Rectangle _ s) -> fmap fromIntegral s) (surfaceBounds surface)
+                              )
+                          )
                       )
-                      cameraDraws'
+                      (realToFrac $ transformRotation transform)
+                      Nothing
+                      (V2 False False)
                 )
-                cameraDraws
-              present renderer
+                cameraDraws'
           )
-          windowDraws
-   in reader allCameraSurfaces >>> access go
+          cameraDraws
+        rendererDrawColor renderer $= V4 0 0 0 255
+        present renderer
+    )
+    cameraSurfaces
 
+-- | Query all cameras and their target surfaces.
+--
+-- @since 0.5
 allCameraSurfaces ::
-  (ArrowQueryReader q, ArrowDynamicQueryReader q, ArrowReaderSystem q arr) =>
-  arr () [(WindowRenderer, [((Camera, Transform2D), [(Surface, Transform2D, SurfaceTexture)])])]
+  (ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s) =>
+  s [(WindowRenderer, [((Camera, Transform2D), [(Surface, Transform2D, SurfaceTexture)])])]
 allCameraSurfaces =
   allWindowDraws
     (Q.fetch &&& Q.fetch)
@@ -270,32 +279,34 @@
         returnA -< (surface, transform, texture)
     )
 
+-- | Query all windows and drawable surfaces.
+--
+-- @since 0.5
 allWindowDraws ::
-  (ArrowQueryReader q, ArrowDynamicQueryReader q, ArrowReaderSystem q arr) =>
-  (q () a) ->
-  (q () b) ->
-  arr () [(WindowRenderer, [(a, [b])])]
-allWindowDraws qA qB = proc () -> do
+  (ArrowQueryReader q, ArrowDynamicQueryReader q, MonadReaderSystem q s) =>
+  q () a ->
+  q () b ->
+  s [(WindowRenderer, [(a, [b])])]
+allWindowDraws qA qB = do
   cameras <-
     S.all
+      ()
       ( proc () -> do
           eId <- Q.entity -< ()
           cameraTarget <- Q.fetch @_ @CameraTarget -< ()
           a <- qA -< ()
           returnA -< (eId, cameraTarget, a)
       )
-      -<
-        ()
-  windows <- S.all (Q.entity &&& Q.fetch @_ @WindowRenderer) -< ()
+
+  windows <- S.all () (Q.entity &&& Q.fetch @_ @WindowRenderer)
   draws <-
     S.all
+      ()
       ( proc () -> do
           t <- Q.fetch @_ @SurfaceTarget -< ()
           a <- qB -< ()
           returnA -< (t, a)
       )
-      -<
-        ()
   let cameraDraws =
         map
           ( \(eId, cameraTarget, b) ->
@@ -320,10 +331,12 @@
               )
           )
           windows
-  returnA -< windowDraws
+  return windowDraws
 
 -- | Surface target component.
 -- This component can be used to specify which `Camera` to draw a `Surface` to.
+--
+-- @since 0.5
 newtype SurfaceTarget = SurfaceTarget {drawTargetCamera :: EntityID}
   deriving (Eq, Show, Generic, NFData)
 
@@ -331,99 +344,107 @@
 
 -- | Surface component.
 -- This component can be used to draw to a window.
+--
+-- @since 0.5
 data Surface = Surface
   { sdlSurface :: !SDL.Surface,
     surfaceBounds :: !(Maybe (Rectangle Int))
   }
 
+-- | @since 0.5
 instance Component Surface
 
+-- | @since 0.5
 instance NFData Surface where
   rnf = rwhnf
 
 -- | Add `SurfaceTarget` components to entities with a new `Surface` component.
+--
+-- @since 0.5
 addSurfaceTargets ::
   ( ArrowQueryReader q,
     ArrowDynamicQueryReader q,
-    ArrowReaderSystem q arr,
-    ArrowQueueSystem b m arr
+    MonadReaderSystem q s,
+    MonadAccess b m
   ) =>
-  arr () ()
-addSurfaceTargets = proc () -> do
-  cameras <- S.all (Q.entity &&& Q.fetch @_ @Camera) -< ()
-  newDraws <- S.filter (Q.entity &&& Q.fetch @_ @Surface) (without @SurfaceTarget) -< ()
-  S.queue
-    ( \(newDraws, cameras) -> case cameras of
-        (cameraEId, _) : _ -> mapM_ (\(eId, _) -> A.insert eId $ SurfaceTarget cameraEId) newDraws
+  s (m ())
+addSurfaceTargets = do
+  cameras <- S.all () (Q.entity &&& Q.fetch @_ @Camera)
+  newDraws <- S.filter () (Q.entity &&& Q.fetch @_ @Surface) (without @SurfaceTarget)
+  let go = case cameras of
+        (cameraEId, _) : _ -> mapM_ (\(eId, _) -> A.insert eId . bundle $ SurfaceTarget cameraEId) newDraws
         _ -> return ()
-    )
-    -<
-      (newDraws, cameras)
+  return go
 
+-- | Update the current `Time`.
+--
+-- @since 0.5
 updateTime ::
-  ( ArrowQuery q,
-    ArrowSystem q s,
-    ArrowSchedule s arr,
-    MonadIO m,
-    ArrowAccessSchedule b m arr
+  ( ArrowQuery m q,
+    MonadSystem q s,
+    MonadIO m
   ) =>
-  arr () ()
-updateTime = proc () -> do
-  t <- access . const $ liftIO SDL.ticks -< ()
-  system $ S.mapSingle Q.set -< Time t
-  returnA -< ()
+  s ()
+updateTime = void . S.mapSingle () $ Q.adjustM (\_ _ -> Time <$> SDL.ticks)
 
+-- | Keyboard input system.
+--
+-- @since 0.5
 handleInput ::
   ( ArrowQueryReader qr,
-    ArrowReaderSystem qr s,
-    ArrowQuery q,
-    ArrowSystem q s,
-    ArrowSchedule s arr,
-    MonadIO m,
-    ArrowAccessSchedule b m arr
+    MonadReaderSystem qr s,
+    ArrowQuery m q,
+    MonadSystem q s,
+    MonadIO s
   ) =>
-  arr () ()
-handleInput = access (const pollEvents) >>> system handleInput'
+  s ()
+handleInput = liftIO pollEvents >>= handleInput'
 
 -- | Keyboard input system.
+--
+-- @since 0.5
 handleInput' ::
-  (ArrowQueryReader qr, ArrowReaderSystem qr arr, ArrowQuery q, ArrowSystem q arr) =>
-  arr [Event] ()
-handleInput' = proc events -> do
-  kb <- S.single Q.fetch -< ()
-  mouse <- S.single Q.fetch -< ()
+  (ArrowQueryReader qr, MonadReaderSystem qr s, ArrowQuery m q, MonadSystem q s) =>
+  [Event] ->
+  s ()
+handleInput' events = do
   let go (kbAcc, mouseAcc) event = case eventPayload event of
         KeyboardEvent keyboardEvent ->
           ( case fromSDLKeycode $ keysymKeycode $ keyboardEventKeysym keyboardEvent of
-              Just key -> handleKeyboardEvent key (motionFromSDL $ keyboardEventKeyMotion keyboardEvent) kbAcc
+              Just key -> handleKeyboardEvent key (motionFromSDL $ keyboardEventKeyMotion keyboardEvent) . kbAcc
               Nothing -> kbAcc,
             mouseAcc
           )
         MouseMotionEvent mouseMotionEvent ->
           ( kbAcc,
-            mouseAcc
-              { mousePosition = (fmap fromIntegral $ mouseMotionEventPos mouseMotionEvent),
-                mouseOffset = (fmap fromIntegral $ mouseMotionEventRelMotion mouseMotionEvent),
-                mouseButtons = (mouseButtons mouseAcc)
-              }
+            \m ->
+              (mouseAcc m)
+                { mousePosition = fromIntegral <$> mouseMotionEventPos mouseMotionEvent,
+                  mouseOffset = fromIntegral <$> mouseMotionEventRelMotion mouseMotionEvent,
+                  mouseButtons = mouseButtons m
+                }
           )
         MouseButtonEvent mouseButtonEvent ->
           ( kbAcc,
-            mouseAcc
-              { mousePosition = (fmap fromIntegral $ mouseButtonEventPos mouseButtonEvent),
-                mouseButtons =
-                  Map.insert
-                    (mouseButtonFromSDL $ mouseButtonEventButton mouseButtonEvent)
-                    (motionFromSDL $ mouseButtonEventMotion mouseButtonEvent)
-                    (mouseButtons mouseAcc)
-              }
+            \m ->
+              (mouseAcc m)
+                { mousePosition = fromIntegral <$> mouseButtonEventPos mouseButtonEvent,
+                  mouseButtons =
+                    Map.insert
+                      (mouseButtonFromSDL $ mouseButtonEventButton mouseButtonEvent)
+                      (motionFromSDL $ mouseButtonEventMotion mouseButtonEvent)
+                      (mouseButtons m)
+                }
           )
         _ -> (kbAcc, mouseAcc)
-      (kb', mouseInput') = foldl' go (kb {keyboardEvents = mempty}, mouse {mouseOffset = V2 0 0}) events
-  S.mapSingle Q.set -< kb'
-  S.mapSingle Q.set -< mouseInput'
-  returnA -< ()
+      (updateKb, updateMouse) = foldl' go (id, id) events
+  _ <- S.map () . Q.adjust $ const updateKb
+  _ <- S.map () . Q.adjust $ const updateMouse
+  return ()
 
+-- | Convert an SDL mouse button to `MouseButton`.
+--
+-- @since 0.5
 mouseButtonFromSDL :: SDL.MouseButton -> MouseButton
 mouseButtonFromSDL b = case b of
   SDL.ButtonLeft -> ButtonLeft
@@ -433,6 +454,9 @@
   SDL.ButtonX2 -> ButtonX2
   SDL.ButtonExtra i -> ButtonExtra i
 
+-- | Convert a `MouseButton` to an SDL mouse button.
+--
+-- @since 0.5
 mouseButtonToSDL :: MouseButton -> SDL.MouseButton
 mouseButtonToSDL b = case b of
   ButtonLeft -> SDL.ButtonLeft
@@ -442,16 +466,25 @@
   ButtonX2 -> SDL.ButtonX2
   ButtonExtra i -> SDL.ButtonExtra i
 
+-- | Convert an SDL input motion to `InputMotion`.
+--
+-- @since 0.5
 motionFromSDL :: SDL.InputMotion -> InputMotion
 motionFromSDL m = case m of
   SDL.Pressed -> Pressed
   SDL.Released -> Released
 
+-- | Convert an `InputMotion` to an SDL input motion.
+--
+-- @since 0.5
 motionToSDL :: InputMotion -> SDL.InputMotion
 motionToSDL m = case m of
   Pressed -> SDL.Pressed
   Released -> SDL.Released
 
+-- | Convert an SDL key code to `Key`.
+--
+-- @since 0.5
 toSDLKeycode :: Key -> SDL.Keycode
 toSDLKeycode key = case key of
   KeyA -> SDL.KeycodeA
@@ -547,9 +580,12 @@
   KeyNumpadPlus -> SDL.KeycodeKPPlus
   KeyNumpadEnter -> SDL.KeycodeKPEnter
   KeyNumpadPeriod -> SDL.KeycodeKPPeriod
-  KeySuper -> SDL.KeycodeLGUI -- assuming left Super (Windows/Command key); SDL also has RGUI
+  KeySuper -> SDL.KeycodeLGUI
   KeyMenu -> SDL.KeycodeMenu
 
+-- | Convert a `Key` to an SDL key code.
+--
+-- @since 0.5
 fromSDLKeycode :: SDL.Keycode -> Maybe Key
 fromSDLKeycode keycode = case keycode of
   SDL.KeycodeA -> Just KeyA
