packages feed

fragr-0.1.0.0: src/Fragr/Execute.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE TypeFamilies #-}

{-|
The execute phase.

The single-queue fast path and the schedule-driven multi-queue path with
its 'QueueBackend' seam.
-}
module Fragr.Execute
  ( execute
  , executeQueued
  , QueueBackend (..)
  , executingQueues
  ) where

import Control.Exception (throwIO)
import Control.Monad (void, when)
import Control.Monad.IO.Class (MonadIO (..))
import Data.Coerce (coerce)
import Data.Foldable (for_)
import Data.IORef (readIORef, writeIORef)
import Data.IntMap.Strict qualified as IntMap
import Data.IntSet qualified as IntSet
import Data.Word (Word64)
import Type.Reflection

import Fragr.Compile (canExecute)
import Fragr.Error (FragrError (..))
import Fragr.Graph (FrameGraph (..), PassNode (..), ResourceEntry (..), Resources (..), SomeEntry (..), entryAt, entryOf, requireCompiled)
import Fragr.Recycle (RecycleQueue, collect, mkRetireItem, retireItem)
import Fragr.Resource (Access (..), Resource (..))
import Fragr.Sync (Compiled (..), PassSync (..), Transfer (..))
import Fragr.Types (Handle, QueueId (..), SomeHandle (..))

{- |
Walk the passes in registration order, skipping culled ones. Per
surviving pass:

1. materialize the transients it creates;
2. run the 'preRead' / 'preWrite' hooks for accesses with flags, then
   the 'Fragr.Graph.addPreExec' flushes;
3. invoke its execution callback;
4. fire the 'Fragr.Graph.addPostExec' flushes;
5. destroy every transient whose last executing user it is.

@ctx@ is forwarded to execution callbacks and hooks, @alloc@ to
'createResource' / 'destroyResource'.
-}
{-# INLINE execute #-}
execute :: (MonadIO m) => FrameGraph ctx alloc -> ctx -> alloc -> m ()
execute g ctx alloc = liftIO do
  compiled <- requireCompiled g "execute"
  passes <- readIORef g.passesRef
  for_ passes \p ->
    when (canExecute compiled.passRefs p) do
      runPass g ctx alloc Nothing p
      for_ (IntMap.findWithDefault [] p.passId compiled.retireAfter) \e ->
        entryAt g e >>= release alloc

{- |
The seam through which 'executeQueued' hands the schedule to the
application. The library computes 'PassSync' values and calls these back;
it never interprets a queue, timeline value or event itself.
-}
data QueueBackend = QueueBackend
  { beforePass :: PassSync -> IO ()
  {- ^ before a pass runs: wait the listed timeline values and events, and
  acquire ownership of the listed resources
  -}
  , afterPass :: PassSync -> IO ()
  {- ^ after a pass runs: release the listed resources, signal the listed
  events, then signal this pass's timeline value
  -}
  , invoke :: PassSync -> IO () -> IO ()
  {- ^ invocation control over one pass's whole step ('beforePass', the
  hook/run sequence, 'afterPass'): run it now (@\_ body -> body@, a device
  queue recording commands) or stash it to run later (a host queue executed
  after the submits, against real timeline values).

  Deferring moves the pass's 'createResource' calls with it, so a deferred
  queue must produce nothing an inline-invoked pass consumes — the consumer
  runs first and finds the resource unmaterialized. Reclamation is safe by
  construction: it goes through the 'RecycleQueue', whose 'completed' values
  gate the release behind the deferred pass's own timeline signal.
  -}
  , completed :: IO [(QueueId, Word64)]
  {- ^ the currently-reached timeline value per queue, consulted to decide
  what the recycle queue may reclaim
  -}
  }

{- |
Multi-queue execution: 'execute' driving the compiled schedule through
the 'QueueBackend' instead of destroying transients inline. Per surviving
pass:

1. hand its step to 'invoke' — 'beforePass', then materialize plus the
   full hook sequence of 'runPass' (acquire / release sides included),
   then 'afterPass';
2. retire any transient whose last executing user this pass is;
3. 'collect' the recycle queue.

An import-only graph (every resource owned outside the graph) never retires
anything and may pass 'Nothing' for the recycle queue; a graph that does
need to reclaim a transient then fails upfront with 'RecycleQueueRequired'.
-}
{-# INLINE executeQueued #-}
executeQueued
  :: (MonadIO m)
  => FrameGraph ctx alloc
  -> QueueBackend
  -> Maybe RecycleQueue
  -> ctx
  -> alloc
  -> m ()
executeQueued g backend mrq ctx alloc = liftIO do
  compiled <- requireCompiled g "executeQueued"
  -- The retire schedule holds transients only, so any entry at all
  -- demands a recycle queue.
  case (mrq, concat (IntMap.elems compiled.retireAfter)) of
    (Nothing, e : _) -> do
      entry <- entryAt g e
      throwIO $ RecycleQueueRequired entry.name
    _ -> pure ()
  passes <- readIORef g.passesRef
  for_ passes \p ->
    when (canExecute compiled.passRefs p) do
      psync <- case IntMap.lookup p.passId compiled.passSync of
        Nothing -> throwIO $ InternalInvariant "no schedule for an executing pass"
        Just s -> pure s
      backend.invoke psync do
        backend.beforePass psync
        runPass g ctx alloc (Just psync) p
        backend.afterPass psync
      for_ mrq \rq -> do
        for_ (IntMap.findWithDefault [] p.passId compiled.retireAfter) \e -> do
          entry <- entryAt g e
          let reqs = IntMap.findWithDefault [] e compiled.entryRetire
          item <- mkRetireItem e reqs (release alloc entry)
          retireItem rq item
        backend.completed >>= void . collect rq
  for_ mrq \rq -> backend.completed >>= void . collect rq

{- |
The queues with at least one executing pass on the compiled schedule,
ascending. Backends size per-queue state (command buffers, timelines) from
it. Fatal before 'Fragr.Compile.compile'.
-}
{-# INLINEABLE executingQueues #-}
executingQueues :: (MonadIO m) => FrameGraph ctx alloc -> m [QueueId]
executingQueues g = liftIO do
  c <- requireCompiled g "executingQueues"
  pure $ coerce . IntSet.toAscList . IntSet.fromList . coerce $ (map (.queue) (IntMap.elems c.passSync))

{- | The per-pass body shared by 'execute' and 'executeQueued': materialize
the pass's creates, fire the acquire / read / write hooks for accesses with
flags, fire the 'Fragr.Graph.addPreExec' flushes, run the callback, fire the
release hooks, then the 'Fragr.Graph.addPostExec' flushes. The acquire and
release sides exist only under a schedule ('executeQueued').
-}
runPass :: forall ctx alloc. FrameGraph ctx alloc -> ctx -> alloc -> Maybe PassSync -> PassNode ctx alloc -> IO ()
runPass g ctx alloc msync p = do
  for_ p.creates \(SomeHandle h) -> do
    entry <- entryOf g h
    when entry.imported $
      throwIO $
        InternalInvariant "create on an imported entry"
    materialize alloc entry
  for_ (maybe [] (.acquires) msync) \(Transfer h peer flags) -> fire (\hh d fl -> preAcquire hh d fl peer) h flags
  for_ p.reads \(Access h flags) -> fire preRead h flags
  for_ p.writes \(Access h flags) -> fire preWrite h flags
  preExecs <- readIORef g.preExecRef
  for_ preExecs ($ ctx)
  p.run Resources{graph = g, pass = p, ctx}
  for_ (maybe [] (.releases) msync) \(Transfer h peer flags) -> fire (\hh d fl -> preRelease hh d fl peer) h flags
  postExecs <- readIORef g.postExecRef
  for_ postExecs ($ ctx)
  where
    fire
      :: forall r
       . (Resource r)
      => (forall s. (Resource s, Alloc s ~ alloc, Ctx s ~ ctx) => Handle s -> Desc s -> Flags s -> Ctx s -> s -> IO ())
      -> Handle r
      -> Maybe (Flags r)
      -> IO ()
    fire hook h mflags = for_ mflags run
      where
        run :: Flags r -> IO ()
        run flags = do
          entry <- entryOf g h
          case entry.payload of
            SomeEntry rep desc objRef -> case eqTypeRep rep (typeRep @r) of
              Nothing -> throwIO $ TypeMismatch "hook" (SomeTypeRep rep) (SomeTypeRep (typeRep @r))
              Just HRefl ->
                readIORef objRef >>= \case
                  Nothing -> throwIO $ InternalInvariant "hook before create"
                  Just obj -> hook h desc flags ctx obj

materialize :: alloc -> ResourceEntry ctx alloc -> IO ()
materialize alloc entry = case entry.payload of
  SomeEntry _ desc objRef -> do
    obj <- createResource desc alloc
    writeIORef objRef (Just obj)

release :: alloc -> ResourceEntry ctx alloc -> IO ()
release alloc entry = case entry.payload of
  SomeEntry _ desc objRef ->
    readIORef objRef >>= \case
      Nothing -> throwIO $ InternalInvariant "destroy before create"
      Just obj -> do
        destroyResource desc alloc obj
        writeIORef objRef Nothing