packages feed

immutaball-core-0.1.0.4.1: Immutaball/Share/State/Context.hs

{-# OPTIONS_GHC -fno-warn-tabs #-}  -- Support tab indentation better, for a better default of no warning if tabs are used: https://dmitryfrank.com/articles/indent_with_tabs_align_with_spaces .
-- Enable warnings:
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}

-- State.hs.

{-# LANGUAGE Haskell2010 #-}
{-# LANGUAGE TemplateHaskell, Arrows, ScopedTypeVariables #-}

module Immutaball.Share.State.Context
	(
		IBStateContext(..), ibContext, ibNeverballrc, ibSDLWindow,
			ibSDLGLContext, ibSDLFont, ibShader, ibGLTextureNames,
			ibGLTextTextures, ibGLAllocatedTextures, ibSSBOs, ibElemVaoVboEbo,
			ibBallElemVaoVboEbo, ibCurrentlyLoadedSol,
		initialStateCxt,
		stateContextStorage,
		requireVideo,
		requireShader,
		requireFont,
		requireGLTextureNames,
		requireGLTextTextures,
		requireGLMtrlTextures,
		requireGLAllocatedTextures,
		requireLoadedSolStorage,
		requireMisc,
		requireBasics,
		finishFrame,
		finishFramePaint,
		checkGLErrors,
		glErrType,

		-- * Utils
		newTextureNameWithoutGenText,
		freeTextureNameWithoutGenText,
		newTextureName,
		freeTextureName,
		createTexture,
		freeTexture,

		cachingRenderText,
		uncacheText,
		clearTextCache,
		freeTextures,
		ibFreeTextures,

		uncacheMtrl,
		clearMtrlCache,
		freeAllTextures,
		ibFreeAllTextures,
		cachingRenderMtrl,

		juicyPixelsDynamicImageToJPImage,
		juicyPixelsImageToImage,

		checkPrecacheMtrls,
		precacheMtrls,
		precacheMtrlsIB,
		precacheMtrlsDirect,

		checkPrecacheMisc,
		precacheMisc,
		precacheMiscIB,
		precacheMiscDirect,

		freeSSBOs,
		freeSSBOsIB,
		freeElemVaoVboEbo,
		freeBallElemVaoVboEbo,
		freeElemVaoVboEboIB,
		freeBallElemVaoVboEboIB,
		setSSBO,
		setElemVaoVboEbo,
		setBallElemVaoVboEbo,
		getElemVaoVboEbo,
		getBallElemVaoVboEbo,

		setCurrentlyLoadedSOL,

		setTransformation
	) where

import Prelude ()
import Immutaball.Prelude

import Control.Arrow
import Control.Monad
import Data.Bits
import Data.Either
import Data.Int
import Data.List
import Data.Maybe
import Foreign.Storable
import Text.Printf

import qualified Codec.Picture as JP
import qualified Codec.Picture.Types as JP
import Control.Lens
import Control.Concurrent.STM.TMVar
import Control.Concurrent.STM.TVar
import Data.Array.IArray
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Lazy as BL
import qualified Data.Map.Lazy as M
import qualified Data.Set as S
import qualified Data.Text as T
import Graphics.GL.Core45
import Graphics.GL.Types
import qualified SDL.Font as SDL.Font  -- (sdl2-ttf)
import SDL.Vect as SDL
import qualified SDL.Video as SDL  -- (ambiguous ‘createTexture’)
--import SDL.Video.OpenGL as SDL
import System.FilePath

import Immutaball.Share.Config
import Immutaball.Share.Context
import Immutaball.Share.Context.Config
import Immutaball.Share.ImmutaballIO
import Immutaball.Share.ImmutaballIO.BasicIO
import Immutaball.Share.ImmutaballIO.GLIO
import Immutaball.Share.ImmutaballIO.SDLIO
import Immutaball.Share.Math
import Immutaball.Share.SDLManager
import Immutaball.Share.State
import Immutaball.Share.Utils
import Immutaball.Share.Wire
import Immutaball.Share.Video

-- | A running Immutaball context instance.
--
-- Normally the controller doesn't deal with this.
data IBStateContext = IBStateContext {
	_ibContext :: IBContext,

	_ibNeverballrc :: Neverballrc,

	_ibSDLWindow :: Maybe (SDL.Window),
	_ibSDLGLContext :: Maybe (SDL.GLContext),
	_ibSDLFont :: Maybe (SDL.Font.Font),
	_ibShader :: Maybe (TMVar ImmutaballShaderHandle),

	-- | Used, freed.
	_ibGLTextureNames      :: Maybe (TVar (S.Set GLuint, S.Set GLuint)),
	_ibGLTextTextures      :: Maybe (TVar (M.Map T.Text  (WidthHeightI, GLuint))),
	_ibGLMtrlTextures      :: Maybe (TVar (M.Map String ((WidthHeightI, GLuint), MtrlMeta))),
	-- | By tracking allocated textures, we can attach their lifetimes to the
	-- SDL Manager thread to automatically clean them up upon quit.
	-- 'createTexture' and 'freeTexture' use this interface.
	_ibGLAllocatedTextures :: Maybe (TVar (S.Set GLuint, S.Set GLuint)),

	-- | A map of locations to ssbo handles.  If an SSBO with data has been
	-- uploaded, it should be in the map; the shader can access the array data /
	-- SSBO through setting the location to the same int.  Once it's freed, it
	-- should no longer be there.
	_ibSSBOs :: Maybe (TVar (M.Map GLuint GLuint)),
	-- | Scene VAO, trivial identity elem indices.
	--
	-- The VAO is like a ‘full’ array: the header/meta info and the pointer
	-- to the second part of this, which is the raw data, just the array itself.
	-- This is VAO, VBO, EBO.
	_ibElemVaoVboEbo :: Maybe (TMVar (GLuint, GLuint, GLuint)),
	-- | Ball VAO, trivial identity elem indices (i.e. array of 0, 1, 2, …).
	_ibBallElemVaoVboEbo :: Maybe (TMVar (GLuint, GLuint, GLuint)),

	_ibCurrentlyLoadedSol :: Maybe (TMVar String)
}
makeLenses ''IBStateContext

initialStateCxt :: IBContext -> IBStateContext
initialStateCxt cxt = IBStateContext {
	_ibContext = cxt,

	_ibNeverballrc = cxt^.ibNeverballrc0,

	_ibSDLWindow           = Nothing,
	_ibSDLGLContext        = Nothing,
	_ibSDLFont             = Nothing,
	_ibShader              = Nothing,
	_ibGLTextureNames      = Nothing,
	_ibGLTextTextures      = Nothing,
	_ibGLMtrlTextures      = Nothing,
	_ibGLAllocatedTextures = Nothing,
	_ibSSBOs               = Nothing,
	_ibElemVaoVboEbo       = Nothing,
	_ibBallElemVaoVboEbo   = Nothing,
	_ibCurrentlyLoadedSol  = Nothing
}

stateContextStorage :: IBStateContext -> Wire ImmutaballM (Maybe IBStateContext) IBStateContext
stateContextStorage y0 = proc cxt -> do
	hold y0 -< cxt

requireVideo :: Wire ImmutaballM IBStateContext IBStateContext
requireVideo = proc cxt0 -> do
	case ((cxt0^.ibContext.ibHeadless), (cxt0^.ibSDLWindow)) of
		(True, _)        -> returnA -< cxt0
		(False, Just _)  -> returnA -< cxt0
		(False, Nothing) -> do
			let windowTitle = T.pack "Immutaball"
			let windowCfg = SDL.defaultWindow {
				SDL.windowMode = if' (cxt0^.ibNeverballrc.fullscreen) SDL.Fullscreen SDL.Windowed,
				SDL.windowGraphicsContext = SDL.OpenGLContext (SDL.defaultOpenGL {SDL.glProfile = SDL.Compatibility SDL.Normal 4 5}),
				SDL.windowInitialSize = V2 (fromIntegral $ (cxt0^.ibNeverballrc.width)) (fromIntegral $ cxt0^.ibNeverballrc.height)
			}
			if not sdlNeedsSpecialThread
				then do
					window <- monadic -< liftIBIO . BasicIBIOF . SDLIO $ SDLWithWindow windowTitle windowCfg id
					context <- monadic -< liftIBIO . BasicIBIOF . SDLIO $ SDLWithGLContext window id
					let cxt1 = cxt0 & (ibSDLWindow.~Just (window :: SDL.Window)) . (ibSDLGLContext.~Just (context :: SDL.GLContext))
					returnA -< cxt1
				else do
					let h = cxt0^.ibContext.ibSDLManagerHandle
					mwindow <- monadic -< liftIBIO $ Atomically (newEmptyTMVar) id
					() <- monadic -< liftIBIO $ issueSDLCommand h (WithWindow windowTitle windowCfg mwindow) ()
					window <- monadic -< liftIBIO $ Atomically (readTMVar mwindow) id
					mcontext <- monadic -< liftIBIO $ Atomically (newEmptyTMVar) id
					() <- monadic -< liftIBIO $ issueSDLCommand h (WithGLContext window mcontext) ()
					context <- monadic -< liftIBIO $ Atomically (readTMVar mcontext) id
					let cxt1 = cxt0 & (ibSDLWindow.~Just (window :: SDL.Window)) . (ibSDLGLContext.~Just (context :: SDL.GLContext))
					returnA -< cxt1
	where
		sdlNeedsSpecialThread :: Bool
		sdlNeedsSpecialThread = True

requireShader :: Wire ImmutaballM IBStateContext (ImmutaballShaderHandle, IBStateContext)
requireShader = proc cxt0 -> do
	case (cxt0^.ibShader) of
		Just mshader -> monadic *** id -< (liftIBIO $ Atomically (readTMVar mshader) id, cxt0)
		Nothing -> do
			mshader <- monadic -< liftIBIO $ sdlCreateImmutaballShader (cxt0^.ibContext.ibSDLManagerHandle)
			let cxt1 = cxt0 & (ibShader.~Just mshader)
			shader <- monadic -< liftIBIO $ Atomically (readTMVar mshader) id
			returnA -< (shader, cxt1)

requireFont :: Wire ImmutaballM IBStateContext (SDL.Font.Font, IBStateContext)
requireFont = proc cxt0 -> do
	case (cxt0^.ibSDLFont) of
		Just font -> returnA -< (font, cxt0)
		Nothing -> do
			let path = cxt0^.ibContext.ibDirs.ibStaticDataDir </> cxt0^.ibContext.ibStaticConfig.immutaballFont
			let size = cxt0^.ibContext.ibStaticConfig.immutaballFontSize
			-- Since this is likely the first time a data file is accessed,
			-- check the path exists and warn if it doesn't with a helpful
			-- message that ‘-d data-path’ needs to be correct.
			exists <- monadic -< liftIBIO . BasicIBIOF $ DoesPathExistSync path id
			() <- warnIf -< (not exists, ("Warning: ./immutaball -d PATH_TO_DATA_DIR must be passed with a path to a compiled neverball data directory; failed to find font file: " ++ path))
			font <- monadic -< liftIBIO . BasicIBIOF . SDLIO $ SDLTTFLoad path (fromIntegral size) id
			let cxt1 = cxt0 & (ibSDLFont.~Just font)
			returnA -< (font, cxt1)
	where
		warnIf :: Wire ImmutaballM (Bool, String) ()
		warnIf = proc (condition, msg) -> do
			() <- monadic -< if' (not condition) (pure ()) . liftIBIO . BasicIBIOF $ PutStrLn msg ()
			returnA -< ()

requireGLTextureNames :: Wire ImmutaballM IBStateContext (TVar (S.Set GLuint, S.Set GLuint), IBStateContext)
requireGLTextureNames = proc cxt0 -> do
	case (cxt0^.ibGLTextureNames) of
		Just glTextureNames -> returnA -< (glTextureNames, cxt0)
		Nothing -> do
			glTextureNames <- monadic -< liftIBIO $ Atomically (newTVar (S.empty, S.empty)) id
			let cxt1 = cxt0 & (ibGLTextureNames.~Just glTextureNames)
			returnA -< (glTextureNames, cxt1)

requireGLTextTextures :: Wire ImmutaballM IBStateContext (TVar (M.Map T.Text (WidthHeightI, GLuint)), IBStateContext)
requireGLTextTextures = proc cxt0 -> do
	case (cxt0^.ibGLTextTextures) of
		Just glTextTextures -> returnA -< (glTextTextures, cxt0)
		Nothing -> do
			glTextTextures <- monadic -< liftIBIO $ Atomically (newTVar M.empty) id
			let cxt1 = cxt0 & (ibGLTextTextures.~Just glTextTextures)
			returnA -< (glTextTextures, cxt1)

requireGLMtrlTextures :: Wire ImmutaballM IBStateContext (TVar (M.Map String ((WidthHeightI, GLuint), MtrlMeta)), IBStateContext)
requireGLMtrlTextures = proc cxt0 -> do
	case (cxt0^.ibGLMtrlTextures) of
		Just glMtrlTextures -> returnA -< (glMtrlTextures, cxt0)
		Nothing -> do
			glMtrlTextures <- monadic -< liftIBIO $ Atomically (newTVar M.empty) id
			let cxt1 = cxt0 & (ibGLMtrlTextures.~Just glMtrlTextures)
			returnA -< (glMtrlTextures, cxt1)

-- | If you require this after you require the mtrl and text cache, you can
-- also free the text and mtrl cache on lifetime end.
requireGLAllocatedTextures :: Wire ImmutaballM IBStateContext (TVar (S.Set GLuint, S.Set GLuint), IBStateContext)
requireGLAllocatedTextures = proc cxt0 -> do
	case (cxt0^.ibGLAllocatedTextures) of
		Just glAllocatedTextures -> returnA -< (glAllocatedTextures, cxt0)
		Nothing -> do
			unusedTo_ <- monadic -< liftIBIO $ Atomically (newTMVar ()) id
			glAllocatedTextures <- monadic -< liftIBIO $ Atomically (newTVar (S.empty, S.empty)) id
			let cxt1 = cxt0 & (ibGLAllocatedTextures.~Just glAllocatedTextures)
			() <- monadic -< liftIBIO $ attachLifetime (cxt0^.ibContext.ibSDLManagerHandle) (pure ()) (\() -> void $ ibFreeAllTextures cxt1) unusedTo_ ()
			--returnA -< (glAllocatedTextures, cxt1)
			cxt2 <- checkPrecacheMtrls -< cxt1
			cxt3 <- checkPrecacheMisc -< cxt2
			returnA -< (glAllocatedTextures, cxt3)

requireSSBOs :: Wire ImmutaballM IBStateContext (TVar (M.Map GLuint GLuint), IBStateContext)
requireSSBOs = proc cxt0 -> do
	let sdlh = (cxt0^.ibContext.ibSDLManagerHandle)
	case (cxt0^.ibSSBOs) of
		Just ssbos -> returnA -< (ssbos, cxt0)
		Nothing -> do
			ssbos <- monadic -< liftIBIO $ Atomically (newTVar M.empty) id

			trivialResourceMetaStorage <- monadic -< liftIBIO $ Atomically (newTMVar ()) id  -- The resource is nothing but a destructor that frees the ssbo1.
			() <- monadic -< liftIBIO $ attachLifetime sdlh (pure ()) (\() -> void $ freeSSBOsIB sdlh ssbos) trivialResourceMetaStorage ()

			let cxt1 = cxt0 & (ibSSBOs.~Just ssbos)
			returnA -< (ssbos, cxt1)

requireElemVaoVboEbo :: Wire ImmutaballM IBStateContext (TMVar (GLuint, GLuint, GLuint), IBStateContext)
requireElemVaoVboEbo = proc cxt0 -> do
	let sdlh = (cxt0^.ibContext.ibSDLManagerHandle)
	case (cxt0^.ibElemVaoVboEbo) of
		Just elemVaoVboEbo -> returnA -< (elemVaoVboEbo, cxt0)
		Nothing -> do
			elemVaoVboEbo <- monadic -< liftIBIO $ Atomically (newEmptyTMVar) id

			trivialResourceMetaStorage <- monadic -< liftIBIO $ Atomically (newTMVar ()) id  -- The resource is nothing but a destructor that frees the ssbo1.
			() <- monadic -< liftIBIO $ attachLifetime sdlh (pure ()) (\() -> void $ freeElemVaoVboEboIB sdlh elemVaoVboEbo) trivialResourceMetaStorage ()

			let cxt1 = cxt0 & (ibElemVaoVboEbo.~Just elemVaoVboEbo)
			returnA -< (elemVaoVboEbo, cxt1)

requireBallElemVaoVboEbo :: Wire ImmutaballM IBStateContext (TMVar (GLuint, GLuint, GLuint), IBStateContext)
requireBallElemVaoVboEbo = proc cxt0 -> do
	let sdlh = (cxt0^.ibContext.ibSDLManagerHandle)
	case (cxt0^.ibBallElemVaoVboEbo) of
		Just ballElemVaoVboEbo -> returnA -< (ballElemVaoVboEbo, cxt0)
		Nothing -> do
			ballElemVaoVboEbo <- monadic -< liftIBIO $ Atomically (newEmptyTMVar) id

			trivialResourceMetaStorage <- monadic -< liftIBIO $ Atomically (newTMVar ()) id  -- The resource is nothing but a destructor that frees the ssbo1.
			() <- monadic -< liftIBIO $ attachLifetime sdlh (pure ()) (\() -> void $ freeBallElemVaoVboEboIB sdlh ballElemVaoVboEbo) trivialResourceMetaStorage ()

			let cxt1 = cxt0 & (ibBallElemVaoVboEbo.~Just ballElemVaoVboEbo)
			returnA -< (ballElemVaoVboEbo, cxt1)

requireLoadedSolStorage :: Wire ImmutaballM IBStateContext (TMVar String, IBStateContext)
requireLoadedSolStorage = proc cxt0 -> do
	case (cxt0^.ibCurrentlyLoadedSol) of
		Just mcurrentlyLoadedSol -> returnA -< (mcurrentlyLoadedSol, cxt0)
		Nothing -> do
			mcurrentlyLoadedSol <- monadic -< liftIBIO $ Atomically (newEmptyTMVar) id
			let cxt1 = cxt0 & (ibCurrentlyLoadedSol.~Just mcurrentlyLoadedSol)
			returnA -< (mcurrentlyLoadedSol, cxt1)

requireMisc :: Wire ImmutaballM IBStateContext IBStateContext
requireMisc =
	snd <$> requireLoadedSolStorage <<<
	snd <$> requireBallElemVaoVboEbo <<<
	snd <$> requireElemVaoVboEbo <<<
	snd <$> requireSSBOs <<<
	snd <$> requireShader <<< snd <$> requireGLAllocatedTextures <<<
	snd <$> requireGLMtrlTextures <<<
	snd <$> requireGLTextTextures <<< snd <$> requireGLTextureNames <<<
	snd <$> requireFont <<< id

-- | Also handles common set-up tasks like clearing the color for rendering.
requireBasics :: Wire ImmutaballM (IBStateContext, Request) IBStateContext
requireBasics = proc (cxt0, _request) -> do
	cxtn <- requireMisc <<< requireVideo -< cxt0
	let sdlGL1' = liftIBIO . sdlGL1 (cxtn^.ibContext.ibSDLManagerHandle)
	() <- monadic -< sdlGL1' $ do
		GLClearColor 0.1 0.1 0.9 1.0 ()
		GLClear (GL_COLOR_BUFFER_BIT .|. GL_DEPTH_BUFFER_BIT .|. GL_STENCIL_BUFFER_BIT) ()
	returnA -< cxtn

-- | Handles common frame finishing like swapping the scene on paint.
--
-- Currently does not update the state context, so it outputs ().
finishFrame :: Wire ImmutaballM (Request, IBStateContext) ()
finishFrame = proc (request, cxt) -> do
	() <- nopA ||| finishFramePaint -< const () +++ const cxt $ matching _Paint request
	returnA -< ()

finishFramePaint :: Wire ImmutaballM IBStateContext ()
finishFramePaint = proc cxt -> do
	-- Swapping outside the SDL Manager thread on my platform didn't work.  So
	-- we'll have the SDL Manager thread do it.
	() <- monadic -<
		if' (not sdlNeedsSpecialThread)
			(maybe (pure ()) (liftIBIO . BasicIBIOF . SDLIO . flip SDLGLSwapWindow ()) $ (cxt^.ibSDLWindow))
			(maybe (pure ()) (liftIBIO . flip (sdlGLSwapWindow (cxt^.ibContext.ibSDLManagerHandle)) ()) $ (cxt^.ibSDLWindow))
	() <- checkGLErrors -< ()
	returnA -< ()
	where
		sdlNeedsSpecialThread :: Bool
		sdlNeedsSpecialThread = True

checkGLErrors :: Wire ImmutaballM () ()
checkGLErrors = proc () -> do
	error_ <- monadic -< liftIBIO . BasicIBIOF . GLIO $ GLGetError id
	case error_ of
		GL_NO_ERROR -> returnA -< ()
		err -> do
			() <- monadic -< liftIBIO . BasicIBIOF $ PutStrLn ("Error: an OpenGL error occurred (" ++ show err ++ "): " ++ glErrType err) ()
			() <- monadic -< liftIBIO . BasicIBIOF $ ExitFailureBasicIOF
			returnA -< ()

-- * Utils

-- | This variant does NOT use ibGLAllocatedTextures, so lifetimes would need
-- to be manually managed.  (This is because I'm currently not using this
-- variant.)
--
-- Edit: actually we need glGenTextures to create names.
-- So don't use the 'WithoutGenText' versions since they won't work.
newTextureNameWithoutGenText :: Wire ImmutaballM IBStateContext (GLuint, IBStateContext)
newTextureNameWithoutGenText = proc cxtn -> do
	(glTextureNames, cxtnp1) <- requireGLTextureNames -< cxtn
	(name, err) <- monadic -< liftIBIO . flip Atomically id $ do
		(used, freed) <- readTVar glTextureNames
		let defaultName = fromMaybe 0 $ (+1) <$> S.lookupMax used
		let name = fromMaybe defaultName $ S.lookupMin freed
		let (used', freed') = (S.insert name used, S.delete name freed)
		writeTVar glTextureNames (used', freed')
		let err = if' (not $ name `S.member` used) Nothing $ Just ("Error: newTextureName: created texture name already in use!: " ++ show name)
		return (name, err)
	() <- monadic -< flip (maybe $ pure ()) err $ \errMsg -> liftIBIO $ (BasicIBIOF $ PutStrLn errMsg ()) <>>- BasicIBIOF ExitFailureBasicIOF
	returnA -< (name, cxtnp1)

freeTextureNameWithoutGenText :: Wire ImmutaballM (GLuint, IBStateContext) IBStateContext
freeTextureNameWithoutGenText = proc (name, cxtn) -> do
	(glTextureNames, cxtnp1) <- requireGLTextureNames -< cxtn
	err <- monadic -< liftIBIO . flip Atomically id $ do
		(used, freed) <- readTVar glTextureNames
		let (used', freed') = (S.delete name used, S.insert name freed)
		writeTVar glTextureNames (used', freed')
		let err =
			if' (name `S.member` freed)       (Just ("Error: freeTextureName: double free of texture name!: " ++ show name)) .
			if' (not $ name `S.member` used)  (Just ("Error: freeTextureName: free of unallocated texture name!: " ++ show name)) $
			Nothing
		return err
	() <- monadic -< flip (maybe $ pure ()) err $ \errMsg -> liftIBIO $ (BasicIBIOF $ PutStrLn errMsg ()) <>>- BasicIBIOF ExitFailureBasicIOF
	returnA -< cxtnp1

-- | Generate a new texture name.
--
-- Also track its lifetime in ibGLAllocatedTextures, so that ibFreeTextures can
-- be attached to the SDL Manager thread to free all remaining on quit.
newTextureName :: Wire ImmutaballM IBStateContext (GLuint, IBStateContext)
newTextureName = proc cxtn -> do
	(glAllocatedTextures, cxtnp1) <- requireGLAllocatedTextures -< cxtn

	(sdlGL1' :: GLIOF me -> ImmutaballM me) <- returnA -< liftIBIO . sdlGL1 (cxtnp1^.ibContext.ibSDLManagerHandle)
	name <- monadic -< (unSingleton <$>) . sdlGL1' $ GLGenTextures 1 id
	-- Now track in ibGLAllocatedTextures.
	() <- monadic -< liftIBIO . flip Atomically id $ do
		let musedFree = glAllocatedTextures
		usedFree0 <- readTVar musedFree
		let (used0, free0) = usedFree0
		let (used1, free1) = (S.insert name used0, free0)
		let usedFree1 = (used1, free1)
		writeTVar musedFree usedFree1
	returnA -< (name, cxtnp1)
	where
		unSingleton [me] = me
		unSingleton _    = error "Internal error: newTextureName expected a single result from GLGenTextures."

freeTextureName :: Wire ImmutaballM (GLuint, IBStateContext) IBStateContext
freeTextureName = proc (name, cxtn) -> do
	(glAllocatedTextures, cxtnp1) <- requireGLAllocatedTextures -< cxtn

	(sdlGL1' :: GLIOF me -> ImmutaballM me) <- returnA -< liftIBIO . sdlGL1 (cxtnp1^.ibContext.ibSDLManagerHandle)
	() <- monadic -< sdlGL1' $ GLDeleteTextures [name] ()
	-- Now free in ibGLAllocatedTextures - just mark it as freed rather than
	-- used.
	() <- monadic -< liftIBIO . flip Atomically id $ do
		let musedFree = glAllocatedTextures
		usedFree0 <- readTVar musedFree
		let (used0, free0) = usedFree0
		let (used1, free1) = (S.delete name used0, if' keepTrackOfFreedTextureNames (S.insert name free0) free0)
		let usedFree1 = (used1, free1)
		writeTVar musedFree usedFree1
	returnA -< cxtnp1
	where
		keepTrackOfFreedTextureNames :: Bool
		keepTrackOfFreedTextureNames = False

-- | Tight RGBA.
createTexture :: Wire ImmutaballM ((WidthHeightI, BS.ByteString), IBStateContext) (GLuint, IBStateContext)
createTexture = proc (((w, h), image), cxtn) -> do
	(name, cxtnp1) <- newTextureName -< cxtn
	(sdlGL1' :: GLIOF me -> ImmutaballM me) <- returnA -< liftIBIO . sdlGL1 (cxtnp1^.ibContext.ibSDLManagerHandle)
	() <- monadic -< sdlGL1' $ do
		GLEnable GL_BLEND ()
		GLBlendEquationSeparate GL_FUNC_ADD GL_FUNC_ADD ()
		GLBlendFuncSeparate GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA GL_ONE GL_ZERO ()

		GLEnable GL_TEXTURE_2D ()
		GLActiveTexture GL_TEXTURE0 ()
		GLClientActiveTexture GL_TEXTURE0 ()
		GLBindTexture GL_TEXTURE_2D name ()
		let glImage = reverseRowsImage ((w, h), image)
		GLTexImage2D GL_TEXTURE_2D 0 GL_RGBA (fromIntegral w) (fromIntegral h) 0 GL_RGBA GL_UNSIGNED_BYTE glImage ()
		-- TODO: re-enable after mipmap is fixed.
		--when (cxtnp1^.ibNeverballrc.mipmap) $ do
		--	GLGenerateMipmap name ()

		GLTexParameteri GL_TEXTURE_2D GL_TEXTURE_WRAP_S GL_REPEAT ()
		GLTexParameteri GL_TEXTURE_2D GL_TEXTURE_WRAP_T GL_REPEAT ()
		-- TODO: re-enable after mipmap is fixed.
		{-
		when (      cxtnp1^.ibNeverballrc.mipmap) $ do
			GLTexParameteri GL_TEXTURE_2D GL_TEXTURE_MIN_FILTER GL_LINEAR_MIPMAP_LINEAR ()
			GLTexParameteri GL_TEXTURE_2D GL_TEXTURE_MAG_FILTER GL_LINEAR_MIPMAP_LINEAR ()
		when (not $ cxtnp1^.ibNeverballrc.mipmap) $ do
		-}
		when True $ do
			GLTexParameteri GL_TEXTURE_2D GL_TEXTURE_MIN_FILTER GL_LINEAR ()
			GLTexParameteri GL_TEXTURE_2D GL_TEXTURE_MAG_FILTER GL_LINEAR ()

		GLTexParameterfv GL_TEXTURE_2D GL_TEXTURE_BORDER_COLOR [0.0, 0.0, 0.0, 0.0] ()

	returnA -< (name, cxtnp1)

-- | This also frees the texture, not just the name.
freeTexture :: Wire ImmutaballM (GLuint, IBStateContext) IBStateContext
freeTexture = proc (name, cxtn) -> do
	cxtnp1 <- freeTextureName -< (name, cxtn)
	returnA -< cxtnp1

-- | Render a text.
--
-- Since rending a text may have a cost, hold onto the texture until its freed
-- with 'uncacheText'.
cachingRenderText :: Wire ImmutaballM (T.Text, IBStateContext) ((WidthHeightI, GLuint), IBStateContext)
cachingRenderText = proc (text, cxtn) -> do
	(mglTextTextures, cxtnp1) <- requireGLTextTextures -< cxtn
	(sdl' :: SDLIOF me -> ImmutaballM me) <- returnA -< liftIBIO . sdl (cxtnp1^.ibContext.ibSDLManagerHandle)
	glTextTextures <- monadic -< liftIBIO $ Atomically (readTVar mglTextTextures) id
	case M.lookup text glTextTextures of
		Just ((w, h), name) -> do
			returnA -< (((w, h), name), cxtnp1)
		Nothing -> do
			(font, cxtnp2) <- requireFont -< cxtnp1
			-- We were doing it ourselves, but it's unclear if it's designed to
			-- work outside the SDL thread.
			--((w, h), image) <- monadic -< liftIBIO . BasicIBIOF . SDLIO $ SDLTTFRenderSync font text id
			((w, h), image) <- monadic -< sdl' $ SDLTTFRenderSync font text id
			(name, cxtnp3) <- createTexture -< (((w, h), image), cxtnp2)
			-- Also see if somebody already cached our text while we were
			-- creating the texture.
			raceAlreadyCached <- monadic -< liftIBIO . flip Atomically id $ do
				glTextTextures2 <- readTVar mglTextTextures
				case M.lookup text glTextTextures2 of
					Nothing -> do
						let glTextTextures3 = M.insert text ((w, h), name) glTextTextures2
						writeTVar mglTextTextures glTextTextures3
						return Nothing
					Just ((w2, h2), name2) -> do
						return $ Just ((w2, h2), name2)
			case raceAlreadyCached of
				Nothing -> do returnA -< (((w, h), name), cxtnp3)
				Just ((w2, h2), name2) -> do
					cxtnp4 <- freeTexture -< (name, cxtnp3)
					returnA -< (((w2, h2), name2), cxtnp4)

-- | Can be repeated; does not if no cache exists for the text.
--
-- Delete the texture and free its texture name if the text has a texture
-- created for it.
uncacheText :: Wire ImmutaballM (T.Text, IBStateContext) IBStateContext
uncacheText = proc (text, cxtn) -> do
	(mglTextTextures, cxtnp1) <- requireGLTextTextures -< cxtn
	mname <- monadic -< liftIBIO . flip Atomically id $ do
		glTextTextures <- readTVar mglTextTextures
		let glTextTextures2 = M.delete text glTextTextures
		writeTVar mglTextTextures glTextTextures2
		return $ snd <$> M.lookup text glTextTextures
	case mname of
		Nothing -> returnA -< cxtnp1
		Just name -> do
			cxtnp2 <- freeTexture -< (name, cxtnp1)
			returnA -< cxtnp2

clearTextCache :: Wire ImmutaballM IBStateContext IBStateContext
clearTextCache = proc cxtn -> do
	(mglTextTextures, cxtnp1) <- requireGLTextTextures -< cxtn
	glTextTextures <- monadic -< liftIBIO $ Atomically (readTVar mglTextTextures) id
	let (texts :: [T.Text]) = M.keys glTextTextures
	foldrA uncacheText -< (cxtnp1, texts)

-- | This only frees allocated textures; it does not update the text texture ID
-- or mtrl texture ID cache.  To clear everything use 'freeAllTextures'.
freeTextures :: Wire ImmutaballM IBStateContext IBStateContext
freeTextures = proc cxtn -> do
	(musedFree, cxtnp1) <- requireGLAllocatedTextures -< cxtn
	needsFreeing <- monadic -< liftIBIO . flip Atomically id $ do
		usedFree0 <- readTVar musedFree
		let (used0, free0) = usedFree0
		let needsFreeing = used0
		let free1 = used0 `S.union` free0
		let used1 = S.empty
		let usedFree1 = (used1, free1)
		writeTVar musedFree (if' weManageAllTextures usedFree1 usedFree0)
		return needsFreeing
	cxtnp2 <- foldrA freeTexture -< (cxtnp1, needsFreeing)
	returnA -< cxtnp2
	where
		weManageAllTextures :: Bool
		weManageAllTextures = False

-- | You can use this to attach a resource using this to the SDLManager thread to
-- free all textures on exit.  The state context can be old so long as it still
-- has the same STM references.
--
-- This only frees allocated textures; it does not update the text texture ID
-- or mtrl texture ID cache.  To clear everything use 'ibFreeAllTextures'.
ibFreeTextures :: IBStateContext -> ImmutaballIOF IBStateContext
ibFreeTextures cxt0 = (\w -> fst <$> stepImmutaballWire w cxt0) $ freeTextures

uncacheMtrl :: Wire ImmutaballM (String, IBStateContext) IBStateContext
uncacheMtrl = proc (mtrl, cxtn) -> do
	(mglMtrlTextures, cxtnp1) <- requireGLMtrlTextures -< cxtn
	mname <- monadic -< liftIBIO . flip Atomically id $ do
		glMtrlTextures <- readTVar mglMtrlTextures
		let glMtrlTextures2 = M.delete mtrl glMtrlTextures
		writeTVar mglMtrlTextures glMtrlTextures2
		return $ snd . fst <$> M.lookup mtrl glMtrlTextures
	case mname of
		Nothing -> returnA -< cxtnp1
		Just name -> do
			cxtnp2 <- freeTexture -< (name, cxtnp1)
			returnA -< cxtnp2

clearMtrlCache :: Wire ImmutaballM IBStateContext IBStateContext
clearMtrlCache = proc cxtn -> do
	(mglMtrlTextures, cxtnp1) <- requireGLMtrlTextures -< cxtn
	glMtrlTextures <- monadic -< liftIBIO $ Atomically (readTVar mglMtrlTextures) id
	let (mtrls :: [String]) = M.keys glMtrlTextures
	foldrA uncacheMtrl -< (cxtnp1, mtrls)

freeAllTextures :: Wire ImmutaballM IBStateContext IBStateContext
freeAllTextures = proc cxtn -> do
	cxtnp1 <- clearMtrlCache -< cxtn
	cxtnp2 <- clearTextCache -< cxtnp1
	cxtnp3 <- freeTextures   -< cxtnp2
	returnA -< cxtnp3

ibFreeAllTextures :: IBStateContext -> ImmutaballIOF IBStateContext
ibFreeAllTextures cxt0 = (\w -> fst <$> stepImmutaballWire w cxt0) $ freeAllTextures

-- | Give it a material path, like ‘mtrl/invisible’.
--
-- TODO: implement more full support for mtrl textures.  For now just read the
-- base image file for the texture, inside ‘data/textures/mtrl/’.
cachingRenderMtrl :: Wire ImmutaballM (String, IBStateContext) (((WidthHeightI, GLuint), MtrlMeta), IBStateContext)
cachingRenderMtrl = proc (mtrl, cxtn) -> do
	(mglMtrlTextures, cxtnp1) <- requireGLMtrlTextures -< cxtn
	glMtrlTextures <- monadic -< liftIBIO $ Atomically (readTVar mglMtrlTextures) id
	case M.lookup mtrl glMtrlTextures of
		Just (((w, h), name), meta) -> do
			returnA -< ((((w, h), name), meta), cxtnp1)
		Nothing -> do
			-- Find (((w, h), image), meta).
			let texturesDir = cxtnp1^.ibContext.ibDirs.ibStaticDataDir </> "textures"
			let baseMtrlPath = texturesDir </> mtrl
			let mtrlMetaPath = baseMtrlPath
			let mtrlImageTryPaths = [baseMtrlPath ++ ".png", baseMtrlPath ++ ".jpg"]

			ammtrlMetaContents <- monadic -< liftIBIO . BasicIBIOF $ ReadText mtrlMetaPath id
			ammtrlEncodedImages <- monadic -< liftIBIO . BasicIBIOF $ forM mtrlImageTryPaths $ \mtrlImageTryPath -> ReadBytes mtrlImageTryPath id
			mmtrlMetaContents <- monadic -< liftIBIO $ Wait ammtrlMetaContents id
			mmtrlEncodedImages <- monadic -< liftIBIO . forM ammtrlEncodedImages $ \ammtrlEncodedImage -> Wait ammtrlEncodedImage id

			mtrlMetaContents <- monadic -< liftIBIO . (ThrowIO ||| pure) $ mmtrlMetaContents
			let (mtrlEncodedImagesFailures, mtrlEncodedImages) = partitionEithers mmtrlEncodedImages
			mtrlEncodedImage <- monadic -< liftIBIO $ case (mtrlEncodedImagesFailures, mtrlEncodedImages) of
				(errs@(err:_), []) -> do
					BasicIBIOF $ PutStrLn (printf "Error: cachingRenderMtrl %s: failed to read mtrl texture image: %s" mtrl (intercalate "\n" (map show errs))) ()
					ThrowIO err
				([], []) -> ThrowIO $ userError ("Error: cachingRenderMtrl: no errors or successes when reading the mtrl " ++ mtrl)
				(_, [encodedImage]) -> return encodedImage
				(_, (_encodedImages@(encodedImage:_more))) -> do
					BasicIBIOF $ PutStrLn (printf "Warning: cachingRenderMtrl %s: found multiple mtrl images; using the first" mtrl) ()
					return encodedImage
			let mmtrlJpEncodedImage = JP.decodeImage . BL.toStrict $ mtrlEncodedImage
			mtrlJpEncodedImage <- monadic -< liftIBIO . (ThrowIO . (\e -> userError (printf "Error: cachingRenderMtrl %s: failed to decode texture image!: %s" mtrl e)) ||| pure) $ mmtrlJpEncodedImage
			let mtrlJpImage = juicyPixelsDynamicImageToJPImage mtrlJpEncodedImage
			let (mtrlW, mtrlH) = join (***) fromIntegral (JP.imageWidth mtrlJpImage, JP.imageHeight mtrlJpImage)
			let mtrlImage = juicyPixelsImageToImage mtrlJpImage
			let mtrlImageGL = reverseRowsImage ((mtrlW, mtrlH), mtrlImage)
			let mtrlMeta = MtrlMeta
			let _unused = [mtrlMetaContents]

			let (((w, h), image), meta) = (((mtrlW, mtrlH), mtrlImageGL), mtrlMeta)

			-- Make the texture with (((w, h), image), meta).
			(name, cxtnp3) <- createTexture -< (((w, h), image), cxtnp1)
			-- Also see if somebody already cached our text while we were
			-- creating the texture.
			raceAlreadyCached <- monadic -< liftIBIO . flip Atomically id $ do
				glMtrlTextures2 <- readTVar mglMtrlTextures
				case M.lookup mtrl glMtrlTextures2 of
					Nothing -> do
						let glMtrlTextures3 = M.insert mtrl (((w, h), name), meta) glMtrlTextures2
						writeTVar mglMtrlTextures glMtrlTextures3
						return Nothing
					Just (((w2, h2), name2), meta2) -> do
						return $ Just (((w2, h2), name2), meta2)
			case raceAlreadyCached of
				Nothing -> do returnA -< ((((w, h), name), meta), cxtnp3)
				Just (((w2, h2), name2), meta2) -> do
					cxtnp4 <- freeTexture -< (name, cxtnp3)
					returnA -< ((((w2, h2), name2), meta2), cxtnp4)

-- | Convert a JuicyPixels image to our own RGBA pixel format.
juicyPixelsDynamicImageToJPImage :: JP.DynamicImage -> JP.Image JP.PixelRGBA8
juicyPixelsDynamicImageToJPImage (JP.ImageY8     img) = JP.promoteImage img
juicyPixelsDynamicImageToJPImage (JP.ImageY16    img) = juicyPixelsDynamicImageToJPImage . JP.ImageRGBA16 $ JP.promoteImage img
juicyPixelsDynamicImageToJPImage (JP.ImageY32    img) = flip JP.pixelMap img $ \pixel -> (\c -> JP.PixelRGBA8 c c c 255) $ (round $ (fromIntegral pixel / (4294967295  :: Double))*255)
juicyPixelsDynamicImageToJPImage (JP.ImageYF     img) = flip JP.pixelMap img $ \pixel -> (\c -> JP.PixelRGBA8 c c c 255) $ (round $ pixel*255)
juicyPixelsDynamicImageToJPImage (JP.ImageYA8    img) = JP.promoteImage img
juicyPixelsDynamicImageToJPImage (JP.ImageYA16   img) = juicyPixelsDynamicImageToJPImage . JP.ImageRGBA16 $ JP.promoteImage img
juicyPixelsDynamicImageToJPImage (JP.ImageRGB8   img) = JP.promoteImage img
juicyPixelsDynamicImageToJPImage (JP.ImageRGB16  img) = flip JP.pixelMap img $ \(JP.PixelRGB16 r g b) -> JP.PixelRGBA8 (round $ ((fromIntegral r  :: Double) / 65535.0)*255) (round $ ((fromIntegral g  :: Double) / 65535.0)*255) (round $ ((fromIntegral b  :: Double) / 65535.0)*255) 255
juicyPixelsDynamicImageToJPImage (JP.ImageRGBF   img) = flip JP.pixelMap img $ \(JP.PixelRGBF r g b) -> JP.PixelRGBA8 (round $ r*255) (round $ g*255) (round $ b*255) (255)
juicyPixelsDynamicImageToJPImage (JP.ImageRGBA8  img) = img
juicyPixelsDynamicImageToJPImage (JP.ImageRGBA16 img) = flip JP.pixelMap img $ \(JP.PixelRGBA16 r g b a) -> JP.PixelRGBA8 (round $ ((fromIntegral r  :: Double) / 65535.0)*255) (round $ ((fromIntegral g  :: Double) / 65535.0)*255) (round $ ((fromIntegral b  :: Double) / 65535.0)*255) (round $ ((fromIntegral a  :: Double) / 65535.0)*255)
juicyPixelsDynamicImageToJPImage (JP.ImageYCbCr8 img) = juicyPixelsDynamicImageToJPImage . JP.ImageRGB8  $ JP.convertImage img
juicyPixelsDynamicImageToJPImage (JP.ImageCMYK8  img) = juicyPixelsDynamicImageToJPImage . JP.ImageRGB8  $ JP.convertImage img
juicyPixelsDynamicImageToJPImage (JP.ImageCMYK16 img) = juicyPixelsDynamicImageToJPImage . JP.ImageRGB16 $ JP.convertImage img

juicyPixelsImageToImage :: JP.Image JP.PixelRGBA8 -> BS.ByteString
juicyPixelsImageToImage jpImage@(JP.Image w h _) = BL.toStrict . BB.toLazyByteString $ JP.pixelFold (\builder _ _ (JP.PixelRGBA8 r g b a) -> builder <> BB.word8 r <> BB.word8 g <> BB.word8 b <> BB.word8 a) mempty jpImage
	where
		_w', _h' :: Integer
		(_w', _h') = join (***) fromIntegral (w, h)

-- | If the context enables precaching mtrls, spawn a thread to precache them.
checkPrecacheMtrls :: Wire ImmutaballM IBStateContext IBStateContext
checkPrecacheMtrls = proc cxtn -> do
	--cxtnp1 <- if' (cxtn^.ibContext.ibStaticConfig.x'cfgPrecacheMtrls) precacheMtrls returnA -<< cxtn
	cxtnp1 <- replaceNow $ (proc (_cxt, doPrecacheMtrls) -> do
		returnA -< if' (not doPrecacheMtrls) (arr fst) $ proc (cxt2, _doPrecacheMtrls) -> do
			precacheMtrls -< cxt2
		) -< (cxtn, (cxtn^.ibContext.ibStaticConfig.x'cfgPrecacheMtrls))
	returnA -< cxtnp1

-- | Spawn a thread to scan for mtrls and render them to the cache.
precacheMtrls :: Wire ImmutaballM IBStateContext IBStateContext
precacheMtrls = proc cxtn -> do
	cxtnp1      <- requireVideo               -< cxtn
	(_, cxtnp2) <- requireGLTextureNames      -< cxtnp1
	(_, cxtnp3) <- requireGLTextTextures      -< cxtnp2
	(_, cxtnp4) <- requireGLMtrlTextures      -< cxtnp3
	(_, cxtnp5) <- requireGLAllocatedTextures -< cxtnp4

	-- TODO: the former uses async concurrency so exceptions are noticed and so
	-- on, which would be preferable, but FIXME it causes an exception to be
	-- thrown when the application quits.  Use the latter in the meantime.
	--() <- monadic -< liftIBIO $ forkIBIOF (void $ precacheMtrlsIB cxtnp5) (pure ())
	() <- monadic -< liftIBIO . JoinIBIOF . BasicIBIOF $ ForkIO (void $ precacheMtrlsIB cxtnp5) (pure ())

	returnA -< cxtnp5

-- | The mtrl precaching thread.
--
-- Ensure the STM resources have been allocated before calling.
precacheMtrlsIB :: IBStateContext -> ImmutaballIOF IBStateContext
precacheMtrlsIB cxt0 = ((\w -> fst <$> stepImmutaballWire w cxt0)) $ precacheMtrlsDirect

precacheMtrlsDirect :: Wire ImmutaballM IBStateContext IBStateContext
precacheMtrlsDirect = proc cxtn -> do
	let texturesDir = cxtn^.ibContext.ibDirs.ibStaticDataDir </> "textures"
	let mtrlsDir = texturesDir </> "mtrl"
	amtrlsDirContents <- monadic -< liftIBIO . BasicIBIOF $ GetDirectoryContents mtrlsDir id
	mtrlsDirContents <- monadic -< liftIBIO $ Wait amtrlsDirContents id
	let mtrlsBase_ = flip filter mtrlsDirContents $ \path -> not (path `elem` [".", ".."]) && not (".png" `isSuffixOf` path) && not (".jpg" `isSuffixOf` path) && path /= "default"
	let mtrlsBase = S.toList . S.fromList $ mtrlsBase_
	let mtrls = map ("mtrl" </>) mtrlsBase
	foldrA (proc (mtrlBase, cxt) -> snd <$> cachingRenderMtrl' -< (mtrlBase, cxt)) -< (cxtn, mtrls)
	-- Delay 100ms between materials to not congest the SDL manager thread.
	where cachingRenderMtrl' = proc (mtrl, cxt) -> do
		cxtnp1 <- cachingRenderMtrl -< (mtrl, cxt)
		() <- monadic -< liftIBIO . BasicIBIOF $ DelayUs (100*1000) ()
		returnA -< cxtnp1

-- | If the context enables precaching misc when setting up allocated textures, spawn a thread to perform misc precaching.
checkPrecacheMisc :: Wire ImmutaballM IBStateContext IBStateContext
checkPrecacheMisc = proc cxtn -> do
	--cxtnp1 <- if' (cxtn^.ibContext.ibStaticConfig.x'cfgPrecacheMisc) precacheMisc returnA -<< cxtn
	cxtnp1 <- replaceNow $ (proc (_cxt, doPrecacheMisc) -> do
		returnA -< if' (not doPrecacheMisc) (arr fst) $ proc (cxt2, _doPrecacheMisc) -> do
			precacheMisc -< cxt2
		) -< (cxtn, (cxtn^.ibContext.ibStaticConfig.x'cfgPrecacheMisc))
	returnA -< cxtnp1

-- | Spawn a thread to perform misc precaching.
precacheMisc :: Wire ImmutaballM IBStateContext IBStateContext
precacheMisc = proc cxtn -> do
	cxtnp1      <- requireVideo               -< cxtn
	(_, cxtnp2) <- requireGLTextureNames      -< cxtnp1
	(_, cxtnp3) <- requireGLTextTextures      -< cxtnp2
	(_, cxtnp4) <- requireGLAllocatedTextures -< cxtnp3
	(_, cxtnp5) <- requireFont                -< cxtnp4

	-- TODO: the former uses async concurrency so exceptions are noticed and so
	-- on, which would be preferable, but FIXME it causes an exception to be
	-- thrown when the application quits.  Use the latter in the meantime.
	--() <- monadic -< liftIBIO $ forkIBIOF (void $ precacheMiscIB cxtnp5) (pure ())
	() <- monadic -< liftIBIO . JoinIBIOF . BasicIBIOF $ ForkIO (void $ precacheMiscIB cxtnp5) (pure ())

	returnA -< cxtnp5

-- | The misc precaching thread.
--
-- Ensure the STM resources have been allocated before calling.
precacheMiscIB :: IBStateContext -> ImmutaballIOF IBStateContext
precacheMiscIB cxt0 = ((\w -> fst <$> stepImmutaballWire w cxt0)) $ precacheMiscDirect

precacheMiscDirect :: Wire ImmutaballM IBStateContext IBStateContext
precacheMiscDirect = proc cxtn -> do
	-- Just precache common timer texts: ":" and "00" through "99".
	let showI i = show (i :: Integer)
	let texts = map T.pack . concat $
		[
			[":"],
			[r | d1 <- [0..9], d2_ <- [0..9], r <- return $ showI d1 ++ showI d2_],
			[]
		]

	foldrA (proc (text, cxt) -> snd <$> cachingRenderText' -< (text, cxt)) -< (cxtn, texts)
	-- Delay 11ms between materials to not congest the SDL manager thread.
	where cachingRenderText' = proc (text, cxt) -> do
		cxtnp1 <- cachingRenderText -< (text, cxt)
		() <- monadic -< liftIBIO . BasicIBIOF $ DelayUs (11*1000) ()
		returnA -< cxtnp1

freeSSBOs :: Wire ImmutaballM IBStateContext IBStateContext
freeSSBOs = proc cxtn -> do
	(ssbos, cxtnp1) <- requireSSBOs -< cxtn
	() <- monadic -< liftIBIO $ freeSSBOsIB (cxtn^.ibContext.ibSDLManagerHandle) ssbos
	returnA -< cxtnp1

freeSSBOsIB :: SDLManagerHandle -> TVar (M.Map GLuint GLuint) -> ImmutaballIOF ()
freeSSBOsIB sdlh ssbos = do
	ssbosMap <- flip Atomically id $ do
		ssbosMap <- readTVar ssbos
		writeTVar ssbos M.empty
		return ssbosMap
	let ssbosVals = M.toList ssbosMap
	sdlGL1 sdlh $ do
		forM_ ssbosVals $ \(_location, ssbo) -> do
			GLDeleteBuffers [ssbo] ()

freeElemVaoVboEbo :: Wire ImmutaballM IBStateContext IBStateContext
freeElemVaoVboEbo = proc cxtn -> do
	(melemVaoVboEbo, cxtnp1) <- requireElemVaoVboEbo -< cxtn
	() <- monadic -< liftIBIO $ freeElemVaoVboEboIB (cxtn^.ibContext.ibSDLManagerHandle) melemVaoVboEbo
	returnA -< cxtnp1

freeBallElemVaoVboEbo :: Wire ImmutaballM IBStateContext IBStateContext
freeBallElemVaoVboEbo = proc cxtn -> do
	(mballElemVaoVboEbo, cxtnp1) <- requireBallElemVaoVboEbo -< cxtn
	() <- monadic -< liftIBIO $ freeBallElemVaoVboEboIB (cxtn^.ibContext.ibSDLManagerHandle) mballElemVaoVboEbo
	returnA -< cxtnp1

freeElemVaoVboEboIB :: SDLManagerHandle -> TMVar (GLuint, GLuint, GLuint) -> ImmutaballIOF ()
freeElemVaoVboEboIB sdlh elemVaoVboEbo = do
	mvaoVboEbo <- Atomically (tryTakeTMVar elemVaoVboEbo) id
	case mvaoVboEbo of
		Nothing -> return ()
		Just vaoVboEbo -> sdlGL1 sdlh $ do
			let (elemVao, elemVbo, elemEbo) = vaoVboEbo
			GLDeleteVertexArrays [elemVao] ()
			GLDeleteBuffers      [elemVbo] ()
			GLDeleteBuffers      [elemEbo] ()

freeBallElemVaoVboEboIB :: SDLManagerHandle -> TMVar (GLuint, GLuint, GLuint) -> ImmutaballIOF ()
freeBallElemVaoVboEboIB sdlh ballElemVaoVboEbo = do
	mvaoVboEbo <- Atomically (tryTakeTMVar ballElemVaoVboEbo) id
	case mvaoVboEbo of
		Nothing -> return ()
		Just vaoVboEbo -> sdlGL1 sdlh $ do
			let (elemVao, elemVbo, elemEbo) = vaoVboEbo
			GLDeleteVertexArrays [elemVao] ()
			GLDeleteBuffers      [elemVbo] ()
			GLDeleteBuffers      [elemEbo] ()

setSSBO :: Wire ImmutaballM ((GLuint, GLData), IBStateContext) IBStateContext
setSSBO = proc ((location, data_), cxtn) -> do
	(ssbos, cxtnp1) <- requireSSBOs -< cxtn

	-- First create and upload the SSBO before we expose its ID in the ssbo
	-- storage in the IBStateContext for ability to be freed.
	let sdlGL1' = liftIBIO . sdlGL1 (cxtnp1^.ibContext.ibSDLManagerHandle)
	newSSBO <- monadic -< sdlGL1' $ do
		-- Create the buffer.
		newSSBO <- unSingleton <$> GLGenBuffers 1 id

		-- Upload the data and assign the location (17).
		GLBindBuffer     GL_SHADER_STORAGE_BUFFER newSSBO ()
		GLBufferData     GL_SHADER_STORAGE_BUFFER data_ GL_STATIC_DRAW ()
		GLBindBufferBase GL_SHADER_STORAGE_BUFFER location newSSBO ()
		GLBindBuffer     GL_SHADER_STORAGE_BUFFER 0 ()

		-- Return the handle.
		return newSSBO

	-- Now insert the SSBO into the state context storage, swapping out the old
	-- one if there is an old one, noting it for freeing.
	moldSSBO <- monadic -< liftIBIO . flip Atomically id $ do
		ssbosVal <- readTVar ssbos
		let moldSSBO = M.lookup location ssbosVal
		let ssbosVal' = M.insert location newSSBO ssbosVal
		writeTVar ssbos ssbosVal'
		return moldSSBO

	-- If there was an old SSBO we replaced, free it.
	() <- monadic -< case moldSSBO of
		Nothing      -> pure ()
		Just oldSSBO -> liftIBIO . sdlGL1 (cxtnp1^.ibContext.ibSDLManagerHandle) $ do
			GLDeleteBuffers [oldSSBO] ()

	-- Return the new storage context.
	returnA -< cxtnp1

	where
		unSingleton [me] = me
		unSingleton _    = error "Internal error: setSSBO expected a single result from GLGenBuffers."

-- | Bool: set int32 vertex attrib?
setElemVaoVboEbo :: Wire ImmutaballM (GLData, Bool, IBStateContext) IBStateContext
setElemVaoVboEbo = proc (data_, setAttrib, cxtn) -> do
	(melemVaoVboEbo, cxtnp1) <- requireElemVaoVboEbo -< cxtn
	let sdlh = (cxtnp1^.ibContext.ibSDLManagerHandle)

	-- First create a new vao and buf, before exposing its ID to possible
	-- concurrency problems.
	newElemVaoVboEbo <- monadic -< liftIBIO . sdlGL1 sdlh $ do
		-- Create a new array and raw buffer of the array.
		elemVao <- unSingleton <$> GLGenVertexArrays 1 id
		elemVbo <- unSingleton <$> GLGenBuffers      1 id
		elemEbo <- unSingleton <$> GLGenBuffers      1 id

		-- Set the data, making it available for upload to the GPU.
		GLBindVertexArray elemVao ()

		GLBindBuffer GL_ELEMENT_ARRAY_BUFFER elemEbo                 ()
		GLBufferData GL_ELEMENT_ARRAY_BUFFER data_   GL_DYNAMIC_DRAW ()

		GLBindBuffer GL_ARRAY_BUFFER elemVbo                 ()
		GLBufferData GL_ARRAY_BUFFER data_   GL_DYNAMIC_DRAW ()

		let newElemVaoVboEbo = (elemVao, elemVbo, elemEbo)

		when setAttrib $ do
			let (si :: GLsizei) = fromIntegral $ sizeOf (error "Internal error: setElemVAOAndBuf: sizeOf accessed its argument!" :: Int32)
			GLVertexAttribIPointer 4 1 GL_INT si 0 ()  -- 4: location of ‘elem’ in the shader.
			GLEnableVertexAttribArray 4 ()

		-- Unbind the VAO.
		GLBindVertexArray 0 ()

		return newElemVaoVboEbo

	-- Now set the new elemvaoandbuf storage, noting if we removed an old
	-- reference so we can free the resource.
	moldElemVaoVboEbo <- monadic -< liftIBIO . flip Atomically id $ do
		moldElemVaoVboEbo <- tryTakeTMVar melemVaoVboEbo
		putTMVar melemVaoVboEbo newElemVaoVboEbo
		return $ moldElemVaoVboEbo

	-- Free an old elemvaobuf if we removed the reference.  Free the resource
	-- without a resource leak.
	() <- monadic -< case moldElemVaoVboEbo of
		Nothing -> pure ()
		Just oldElemVaoVboEbo -> liftIBIO . sdlGL1 sdlh $ do
			let (elemVao, elemVbo, elemEbo) = oldElemVaoVboEbo
			GLDeleteVertexArrays [elemVao] ()
			GLDeleteBuffers      [elemVbo] ()
			GLDeleteBuffers      [elemEbo] ()

	-- Return the new state context with the updated storage.
	returnA -< cxtnp1

	where
		unSingleton [me] = me
		unSingleton _    = error "Internal error: setElemVAOAndBuf expected a single result from GLGenVertexArrays or GLGenBuffers."

-- | Bool: set int32 vertex attrib?
setBallElemVaoVboEbo :: Wire ImmutaballM (GLData, Bool, IBStateContext) IBStateContext
setBallElemVaoVboEbo = proc (data_, setAttrib, cxtn) -> do
	(mballElemVaoVboEbo, cxtnp1) <- requireBallElemVaoVboEbo -< cxtn
	let sdlh = (cxtnp1^.ibContext.ibSDLManagerHandle)

	-- First create a new vao and buf, before exposing its ID to possible
	-- concurrency problems.
	newBallElemVaoVboEbo <- monadic -< liftIBIO . sdlGL1 sdlh $ do
		-- Create a new array and raw buffer of the array.
		elemVao <- unSingleton <$> GLGenVertexArrays 1 id
		elemVbo <- unSingleton <$> GLGenBuffers      1 id
		elemEbo <- unSingleton <$> GLGenBuffers      1 id

		-- Set the data, making it available for upload to the GPU.
		GLBindVertexArray elemVao ()

		GLBindBuffer GL_ELEMENT_ARRAY_BUFFER elemEbo                 ()
		GLBufferData GL_ELEMENT_ARRAY_BUFFER data_   GL_DYNAMIC_DRAW ()

		GLBindBuffer GL_ARRAY_BUFFER elemVbo                 ()
		GLBufferData GL_ARRAY_BUFFER data_   GL_DYNAMIC_DRAW ()

		let newBallElemVaoVboEbo = (elemVao, elemVbo, elemEbo)

		when setAttrib $ do
			let (si :: GLsizei) = fromIntegral $ sizeOf (error "Internal error: setBallElemVAOAndBuf: sizeOf accessed its argument!" :: Int32)
			GLVertexAttribIPointer 4 1 GL_INT si 0 ()  -- 4: location of ‘elem’ in the shader.
			GLEnableVertexAttribArray 4 ()

		-- Unbind the VAO.
		GLBindVertexArray 0 ()

		return newBallElemVaoVboEbo

	-- Now set the new ballelemvaoandbuf storage, noting if we removed an old
	-- reference so we can free the resource.
	moldBallElemVaoVboEbo <- monadic -< liftIBIO . flip Atomically id $ do
		moldBallElemVaoVboEbo <- tryTakeTMVar mballElemVaoVboEbo
		putTMVar mballElemVaoVboEbo newBallElemVaoVboEbo
		return $ moldBallElemVaoVboEbo

	-- Free an old ballelemvaobuf if we removed the reference.  Free the resource
	-- without a resource leak.
	() <- monadic -< case moldBallElemVaoVboEbo of
		Nothing -> pure ()
		Just oldBallElemVaoVboEbo -> liftIBIO . sdlGL1 sdlh $ do
			let (elemVao, elemVbo, elemEbo) = oldBallElemVaoVboEbo
			GLDeleteVertexArrays [elemVao] ()
			GLDeleteBuffers      [elemVbo] ()
			GLDeleteBuffers      [elemEbo] ()

	-- Return the new state context with the updated storage.
	returnA -< cxtnp1

	where
		unSingleton [me] = me
		unSingleton _    = error "Internal error: setBallElemVAOAndBuf expected a single result from GLGenVertexArrays or GLGenBuffers."

getElemVaoVboEbo :: Wire ImmutaballM IBStateContext (Maybe (GLuint, GLuint, GLuint), IBStateContext)
getElemVaoVboEbo = proc cxtn -> do
	(melemVaoVboEbo, cxtnp1) <- requireElemVaoVboEbo -< cxtn
	maelemVaoVboEbo <- monadic -< liftIBIO . flip Atomically id $ tryReadTMVar (melemVaoVboEbo)
	returnA -< (maelemVaoVboEbo, cxtnp1)

getBallElemVaoVboEbo :: Wire ImmutaballM IBStateContext (Maybe (GLuint, GLuint, GLuint), IBStateContext)
getBallElemVaoVboEbo = proc cxtn -> do
	(mballElemVaoVboEbo, cxtnp1) <- requireBallElemVaoVboEbo -< cxtn
	maballElemVaoVboEbo <- monadic -< liftIBIO . flip Atomically id $ tryReadTMVar (mballElemVaoVboEbo)
	returnA -< (maballElemVaoVboEbo, cxtnp1)

-- | Set the currently loaded sol, retrieving the old one.
--
-- The old one can be checked for equality with the new one to determine if
-- we're on a new SOL.
setCurrentlyLoadedSOL :: Wire ImmutaballM (String, IBStateContext) (Maybe String, IBStateContext)
setCurrentlyLoadedSOL = proc (identifyingPath, cxtn) -> do
	(mloadedSol, cxtnp1) <- requireLoadedSolStorage -< cxtn
	moldIdentifyingPath <- monadic -< liftIBIO . flip Atomically id $ do
		moldIdentifyingPath <- tryTakeTMVar mloadedSol
		putTMVar mloadedSol identifyingPath
		return moldIdentifyingPath
	returnA -< (moldIdentifyingPath, cxtnp1)

-- | Row-major.
setTransformation :: Wire ImmutaballM (Mat4 Double, IBStateContext) IBStateContext
setTransformation = proc (_mat@(Mat4 rows), cxtn) -> do
	let matArray = listArray (0, 15) [val' | r <- [rows^.x4, rows^.y4, rows^.z4, rows^.w4], val <- [r^.x4, r^.y4, r^.z4, r^.w4], val' <- return $ toShaderDoubleType val]
	let matArrayGPU = gpuEncodeArray matArray

	cxtnp1 <- setSSBO -< ((shaderSSBOTransformationLocation, matArrayGPU), cxtn)

	returnA -< cxtnp1