packages feed

vulkan-utils-0.5.11.0: src/Vulkan/Utils/WindowLoop.hs

{-| Per-frame window loop shared by windowed applications.

The skeleton — read swapchain, run a frame inside @runFrame@, recreate on
'Vulkan.Utils.Swapchain.threwSwapchainError', advance — is the same for any
windowed Vulkan app. Each consumer only varies in:

* the per-swapchain state it holds (framebuffers, descriptor sets, …),
* the per-frame render action, and
* the "what to do on exit / per-frame metric" hooks.

'runWindowLoop' takes those four points as fields of a 'WindowLoop' record.
-}
module Vulkan.Utils.WindowLoop
  ( WindowLoop (..)
  , runWindowLoop
  , noWindowState
  , noRecycledResources
  , noOnFrame
  , noOnExit
  ) where

import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Resource
  ( ReleaseKey
  , ResourceT
  , register
  , release
  )
import Data.IORef
import Data.Word (Word64)
import GHC.Clock (getMonotonicTimeNSec)
import qualified Vulkan.Core10 as Vk
import Vulkan.Utils.Frame (Frame (..), InitRecycledResources, advanceFrame, drainFrames, initialFrame, runFrame)
import Vulkan.Utils.Swapchain (Swapchain, recreateSwapchain, threwSwapchainError)
import Vulkan.Utils.VulkanContext (VulkanContext (..))

data WindowLoop s rr = WindowLoop
  { wlMkState :: Swapchain -> ResourceT IO (s, ReleaseKey)
  {- ^ Build per-swapchain state. The release key is fired when the
  swapchain is recreated and a fresh state replaces this one.
  -}
  , wlMkRecycled :: InitRecycledResources (ResourceT IO) rr
  , wlRender :: s -> Frame rr -> ResourceT IO ()
  -- ^ Per-frame render action; runs inside @runFrame@.
  , wlOnFrame :: Word64 -> Word64 -> ResourceT IO ()
  {- ^ Optional metric hook with start/end nanoseconds around 'runFrame'.
  Use 'noOnFrame' if you don't care.
  -}
  , wlOnExit :: Frame rr -> ResourceT IO ()
  -- ^ Fired once when the window closes. Use 'noOnExit' if you don't care.
  }

runWindowLoop
  :: VulkanContext rr
  -> Swapchain
  -> IO Vk.Extent2D
  -- ^ Get current drawable size (called on resize)
  -> IO Bool
  -- ^ Per-frame poller; 'True' means quit
  -> WindowLoop s rr
  -> ResourceT IO ()
runWindowLoop vc initialSC getSize shouldQuit WindowLoop{..} = do
  initialState <- wlMkState initialSC
  scRef <- liftIO $ newIORef initialSC
  stRef <- liftIO $ newIORef initialState
  initial <- initialFrame vc initialSC wlMkRecycled
  let
    perFrame f = do
      currentSC <- liftIO $ readIORef scRef
      (st, _) <- liftIO $ readIORef stRef
      let f' = f{fSwapchain = currentSC}
      startNs <- liftIO getMonotonicTimeNSec
      needsNew <-
        liftIO . threwSwapchainError $
          runFrame vc f' (wlRender st f')
      endNs <- liftIO getMonotonicTimeNSec
      wlOnFrame startNs endNs
      sc' <-
        if needsNew
          then do
            newSize <- liftIO getSize
            -- A swapchain recreation retires the old swapchain. Drain the
            -- graphics/present queue first so the old swapchain's pending
            -- presents — and this frame's GPU work behind the old
            -- per-swapchain state — all complete before we free the old
            -- per-image present-wait semaphores, the old swapchain, and the
            -- old state. A present-wait semaphore cannot otherwise be known
            -- idle (its present has no host-visible completion without
            -- VK_KHR_swapchain_maintenance1). Recreation is rare, so this
            -- one-shot wait is cheap.
            Vk.deviceWaitIdle (vcDevice vc)
            sc' <- recreateSwapchain (vcPhysicalDevice vc) (vcDevice vc) newSize currentSC
            -- Free the old state before building its replacement: recreation
            -- already retired the old swapchain's images, so the new state may
            -- be handed their recycled handles — any bookkeeping the old state
            -- keys on them must be gone before wlMkState re-wraps.
            (_, oldKey) <- liftIO $ readIORef stRef
            release oldKey
            (newSt, newKey) <- wlMkState sc'
            liftIO $ writeIORef scRef sc'
            liftIO $ writeIORef stRef (newSt, newKey)
            pure sc'
          else pure currentSC
      advanceFrame vc sc' f'

    loop f =
      liftIO shouldQuit >>= \case
        True -> do
          Vk.deviceWaitIdle (vcDevice vc)
          wlOnExit f
          liftIO $ drainFrames vc f
          pure Nothing
        False -> Just <$> perFrame f
  loopJust loop initial

-- | 'wlMkState' for callers that have no per-swapchain state.
noWindowState :: Swapchain -> ResourceT IO ((), ReleaseKey)
noWindowState _ = do
  key <- register (pure ())
  pure ((), key)

noRecycledResources :: (Applicative m) => InitRecycledResources m ()
noRecycledResources _vc _dbIx _pools = pure ()

noOnFrame :: Word64 -> Word64 -> ResourceT IO ()
noOnFrame _ _ = pure ()

noOnExit :: Frame rr -> ResourceT IO ()
noOnExit _ = pure ()

loopJust :: (Monad m) => (a -> m (Maybe a)) -> a -> m ()
loopJust f x =
  f x >>= \case
    Nothing -> pure ()
    Just x' -> loopJust f x'