nano-ui-sdl-0.1.0.0: lib/NanoUI/Sdl/Session.hs
{-# LANGUAGE DataKinds #-}
-- | SDL window session loop: event poll, resize sync, frame present.
module NanoUI.Sdl.Session
( runSdlSession
) where
import Control.Exception (bracket)
import Control.Monad (void, when)
import Data.IORef (newIORef, readIORef, writeIORef)
import NanoUI (Input (..), emptyInput)
import NanoUI.Sdl.Debug (SdlDebugSampler (..))
import NanoUI.Input (clearEphemeral)
import NanoUI.Runner
( SessionDriver (..)
, newDrawingLock
, runSessionLoop
, shouldRedrawFrame
, tryWithDrawingLock
)
import NanoUI.Testing
( Context
, clearDirty
)
import NanoUI.Sdl.Cursor (syncPointerCursor)
import NanoUI.Sdl.Input
( SdlEvent (..)
, applyEvent
, isButtonEdge
, isHardQuit
, pollEvents
, waitEvent
, waitEventTimeout
)
import NanoUI.Sdl.Display (installResizeWatch)
import NanoUI.Sdl.Window (SdlEnv (..), SdlOptions (..), syncDisplay, withSdl)
import SDL3.Sys.Bindgen.Blendmode (sDL_BLENDMODE_BLEND)
import SDL3.Sys.Render (setRenderDrawBlendModeSafe, setRenderVSync)
runSdlSession ::
SdlOptions ->
Context ->
(SdlEnv -> IO ()) ->
(Input -> Bool) ->
(Context -> SdlEnv -> Input -> Bool -> IO (Bool, Input)) ->
IO ()
runSdlSession options ctx setup shouldQuit drawFn =
withSdl options ctx $ \ctx0 env -> do
setup env
void $ setRenderDrawBlendModeSafe (sdlRenderer env) (fromIntegral sDL_BLENDMODE_BLEND)
ctxRef <- newIORef ctx0
prev <- newIORef emptyInput
drawing <- newDrawingLock
startupDone <- newIORef False
startupCatchup <- newIORef False
-- The resize watch presents with vsync off: Windows' modal size loop
-- cannot take the next drag step while a present waits for vblank. The
-- main loop turns vsync back on before its own frames.
vsyncPaused <- newIORef False
-- Window size the resize watch presented since the main loop last
-- decided whether to draw. That frame already covers the size change and
-- expose events the loop is about to see.
resizePresented <- newIORef Nothing
let onResize = do
void $
tryWithDrawingLock drawing $ do
liveCtx <- readIORef ctxRef
inp <- readIORef prev
scale0 <- readIORef (sdlScaleRef env)
(ctx', inpSynced) <- syncDisplay liveCtx env (clearEphemeral inp)
writeIORef ctxRef ctx'
done <- readIORef startupDone
if not done
then do
writeIORef prev inpSynced
writeIORef startupCatchup True
else do
scale1 <- readIORef (sdlScaleRef env)
if inputWindowSize inpSynced == inputWindowSize inp && scale1 == scale0
then writeIORef prev inpSynced
else do
paused <- readIORef vsyncPaused
when (sdlVsync env && not paused) $ do
void $ setRenderVSync (sdlRenderer env) 0
writeIORef vsyncPaused True
(_, s) <- drawFn ctx' env inpSynced True
writeIORef prev s
writeIORef resizePresented (Just (inputWindowSize s))
-- A refresh wake (a finished file dialog) may postdate the watch's frame,
-- so it voids that frame's cover.
let noteWake evs = do
when (EvRefresh `elem` evs) $ writeIORef resizePresented Nothing
pure evs
let drainUntilQuiet c inp = do
pending <- pollEvents
(c', inp') <- syncDisplay c env (foldl' applyEvent inp pending)
if null pending
then pure (c', inp')
else drainUntilQuiet c' inp'
let inpSeed = emptyInput {inputWindowSize = sdlWindowSize options}
(ctx1, inp0) <- drainUntilQuiet ctx0 inpSeed
writeIORef ctxRef ctx1
scale0 <- readIORef (sdlScaleRef env)
(_, synced0) <- drawFn ctx1 env inp0 True
-- First present can apply DPI. Prev rects are empty on that frame.
-- Draw once more before idle or the Controls page stays stretched
-- until the first mouse move.
(ctx1b, inp0b) <- drainUntilQuiet ctx1 synced0
writeIORef ctxRef ctx1b
scaleSettle <- readIORef (sdlScaleRef env)
let paintedSize = inputWindowSize inp0b
(_, synced0b) <- drawFn ctx1b env inp0b True
clearDirty ctx1b
(ctx2, inp1) <- drainUntilQuiet ctx1b synced0b
writeIORef ctxRef ctx2
scale1 <- readIORef (sdlScaleRef env)
catchup <- readIORef startupCatchup
synced1 <-
if catchup || inputWindowSize inp1 /= paintedSize || abs (scale1 - scaleSettle) > 0.001 || abs (scaleSettle - scale0) > 0.001
then do
(_, s) <- drawFn ctx2 env inp1 True
clearDirty ctx2
pure s
else pure inp1
writeIORef startupCatchup False
writeIORef startupDone True
writeIORef prev synced1
let drv =
SessionDriver
{ sdPollEvents = pollEvents >>= noteWake
, sdWaitEvents = \t -> do
-- Take the rest of the queue with the event that ended the
-- wait, so one pass sees a whole burst (a resize queues
-- several window events at once).
woke <- if t < 0 then waitEvent else waitEventTimeout t
case woke of
Nothing -> pure []
Just ev -> noteWake . (ev :) =<< pollEvents
, sdApplyEvent = applyEvent
, sdIsButtonEdge = isButtonEdge
, sdIsHardQuit = isHardQuit
, sdIsSessionQuit = (== EvQuit)
, sdSyncDisplay = \c inp -> do
paused <- readIORef vsyncPaused
when paused $ do
void $ setRenderVSync (sdlRenderer env) 1
writeIORef vsyncPaused False
(c', inp') <- syncDisplay c env inp
writeIORef ctxRef c'
writeIORef prev inp'
pure (c', inp')
, sdDebug = sdsSampler (sdlDebug env)
, sdContinuous = sdlContinuous env
-- With vsync on, presents throttle the loop. With vsync off a
-- live animation would spin at max speed, so wait ~2 ms short
-- of the frame period, leaving the slack for alignFrameStart:
-- SDL_WaitEventTimeout overruns by ~1 ms, and a wait that
-- returns past the boundary makes the frame late.
, sdPacingMs = if sdlVsync env then 16 else max 1 (floor (sdlRefreshPeriod env * 1000) - 2)
-- A frame that skipped (empty damage, e.g. an animation scrolled
-- out of view) did not wait for vblank, so it must not loop
-- without waiting.
, sdPresentPaces = if sdlVsync env then readIORef (sdlLastPresented env) else pure False
, sdShouldDraw = \c prevInp inpSynced wasAnim wantDebug -> do
presented <- readIORef resizePresented
writeIORef resizePresented Nothing
let (prevInp', inpSynced') = case presented of
Just size
| size == inputWindowSize inpSynced ->
(prevInp {inputWindowSize = size}, inpSynced {inputWindowRedraw = False})
_ -> (prevInp, inpSynced)
shouldRedrawFrame c prevInp' inpSynced' wasAnim (sdlContinuous env) wantDebug
, sdDraw = \c inpSynced forceFull -> do
writeIORef resizePresented Nothing
ms <- tryWithDrawingLock drawing (drawFn c env inpSynced (forceFull || sdlContinuous env))
case ms of
Just (dirtyOut, s) -> do
writeIORef prev s
pure (dirtyOut, s)
Nothing -> pure (False, inpSynced)
, sdOnCursor = syncPointerCursor (sdlCursors env)
, sdAlignSec = sdlRefreshPeriod env
, sdShouldQuit = shouldQuit
}
bracket (installResizeWatch onResize) id $ \_ ->
runSessionLoop drv ctx2 synced1