imj-base-0.1.0.2: src/Imj/Graphics/Render/Delta/Flush.hs
{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Imj.Graphics.Render.Delta.Flush
( deltaFlush
) where
import Imj.Prelude
import qualified Prelude(putStr, putChar)
import Control.Monad(when)
import Data.IORef( IORef , readIORef )
import Data.Vector.Unboxed.Mutable( IOVector, read, write, length )
import System.IO( stdout, hFlush )
import qualified Imj.Data.Vector.Unboxed.Mutable.Dynamic as Dyn
(unstableSort, accessUnderlying, length,
clear, pushBack )
import Imj.Graphics.Color.Types
import Imj.Graphics.Render.Delta.Types
import Imj.Graphics.Render.Delta.Buffers
import Imj.Graphics.Render.Delta.Cell
import Imj.Graphics.Render.Delta.Clear
import Imj.Graphics.Render.Delta.Internal.Types
-- | Flushes the frame, i.e renders it to the console.
-- Then, resizes the context if needed (see 'ResizePolicy')
-- and clears the back buffer (see 'ClearPolicy').
deltaFlush :: IORef Buffers -> IO ()
deltaFlush ioRefBuffers =
readIORef ioRefBuffers
>>=
render
>> do
updateSize ioRefBuffers
-- TODO if buffers resized because the terminal resized, send a clearScreen command or re-render with new size
hFlush stdout -- TODO is flush blocking? slow? could it be async?
render :: Buffers -> IO ()
render buffers@(Buffers _ _ width (Delta delta) _) = do
computeDelta buffers 0
clearIfNeeded OnFrame buffers
-- On average, foreground and background color change command is 20 bytes :
-- "\ESC[48;5;167;38;5;255m"
-- On average, position change command is 9 bytes :
-- "\ESC[150;42H"
-- So we want to minimize the number of color changes first, and then mimnimize
-- the number of position changes.
-- In 'Cell', color is encoded in higher bits than position, so this sort
-- sorts by color first, then by position, which is what we want.
Dyn.unstableSort delta
szDelta <- Dyn.length delta
under <- Dyn.accessUnderlying delta
-- We ignore this color value. We could store it and use it to initiate the recursion
-- at next render but if the client renders with another library in-betweeen, this value
-- would be wrong, so we can ignore it here for more robustness.
_ <- renderDelta under (fromIntegral szDelta) width 0 Nothing Nothing
Dyn.clear delta
-- We pass the underlying vector, and the size instead of the dynamicVector
renderDelta :: IOVector Cell
-> Dim BufferSize
-> Dim Width
-> Dim BufferIndex
-> Maybe LayeredColor
-> Maybe (Dim BufferIndex)
-> IO LayeredColor
renderDelta delta size width index prevColors prevIndex
| fromIntegral size == index =
return whiteOnBlack -- the value is not used
| otherwise = do
c <- read delta $ fromIntegral index
let (bg, fg, idx, char) = expandIndexed c
prevRendered = (== Just (pred idx)) prevIndex
setCursorPositionIfNeeded width idx prevRendered
usedColor <- renderCell bg fg char prevColors
renderDelta delta size width (succ index) (Just usedColor) (Just idx)
computeDelta :: Buffers
-> Dim BufferIndex
-- ^ the buffer index
-> IO ()
computeDelta
b@(Buffers (Buffer backBuf) (Buffer frontBuf) _ (Delta delta) _)
idx
| fromIntegral idx == size = return ()
| otherwise = do
let i = fromIntegral idx
-- read from back buffer
valueToDisplay <- read backBuf i
-- read from front buffer
valueCurrentlyDisplayed <- read frontBuf i
-- if differences are found, update front buffer and push the difference
-- in delta vector
when (valueToDisplay /= valueCurrentlyDisplayed) $ do
write frontBuf i valueToDisplay
Dyn.pushBack delta $ mkIndexedCell valueToDisplay idx
-- recurse
computeDelta b (succ idx)
where
size = length backBuf
-- TODO merge with color change command to save 2 bytes
-- | The command to set the cursor position to 123,45 is "\ESC[123;45H",
-- its size is 9 bytes : one order of magnitude more than the size
-- of a char, so we avoid sending this command when not strictly needed.
{-# INLINE setCursorPositionIfNeeded #-}
setCursorPositionIfNeeded :: Dim Width
-> Dim BufferIndex
-- ^ the buffer index
-> Bool
-- ^ True if a char was rendered at the previous buffer index
-> IO ()
setCursorPositionIfNeeded width idx predPosRendered = do
let (colIdx, rowIdx) = xyFromIndex width idx
shouldSetCursorPosition =
-- We assume that the buffer width is not equal to terminal width,
-- so even if the previous position was rendered,
-- the cursor may not be located at the beginning of the line.
colIdx == 0
-- If the previous buffer position was rendered, the cursor position has
-- automatically advanced to the next column (or to the beginning of
-- the next line if it was the last terminal column).
|| not predPosRendered
when shouldSetCursorPosition $ Prelude.putStr $ setCursorPositionCode (fromIntegral rowIdx) (fromIntegral colIdx)
setCursorPositionCode :: Int -- ^ 0-based row to move to
-> Int -- ^ 0-based column to move to
-> String
setCursorPositionCode n m = csi [n + 1, m + 1] "H"
{-# INLINE renderCell #-}
renderCell :: Color8 Background
-> Color8 Foreground
-> Char
-> Maybe LayeredColor
-> IO LayeredColor
renderCell bg fg char maybeCurrentConsoleColor = do
let (bgChange, fgChange, usedFg) =
maybe
(True, True, fg)
(\(LayeredColor bg' fg') ->
-- use foreground color if we don't draw a space
let useFg = char /= ' ' -- I don't use Data.Char.isSpace, it could be slower
usedFg' = if useFg
then
fg
else
fg'
in (bg'/=bg, fg'/=usedFg', usedFg'))
maybeCurrentConsoleColor
sgrs = concat $ [color8FgSGRToCode fg | fgChange] ++
[color8BgSGRToCode bg | bgChange]
if bgChange || fgChange
then
Prelude.putStr $ csi sgrs "m" ++ [char]
else
Prelude.putChar char
return $ LayeredColor bg usedFg
csi :: [Int]
-> String
-> String
csi args code = "\ESC[" ++ intercalate ";" (map show args) ++ code
{-# INLINE xyFromIndex #-}
xyFromIndex :: Dim Width -> Dim BufferIndex -> (Dim Col, Dim Row)
xyFromIndex width idx =
getRowCol idx width
-- TODO use this formalism
{-
newtype SetPosition = Move2d !Int !Int
| Move1 Direction !Int
type Value = (Color8 Background, Color8 Foreground, Char)
type Location = (Row, Column)
screenLocations = { (row, column) | row <- [0..screenHeight], column <- [0..screenWidth] }
type Step = Int -- represents a temporal game / animation step
frame :: Step -> Location -> Value -- defines the desired content of animations
identicalLocations n = {loc | loc <- screenLocations && frame n loc == frame (pred n) loc}
deltaLocations n = screenLocations \\ (identicalLocations n)
newtype RenderCmd = SetPosition | SetColor | Char | !String
newtype SetPosition = Move2d Int Int | Move Direction Int
newtype SetColor = SetColorForeground | SetColorBackground | SetColorBoth
newtype Direction = Up | Left | Down | Right
cheapestChangePosition' :: (Row,Col) -> SetPosition
cheapestChangePosition :: (Row,Col) -> (Row,Col) -> Maybe SetPosition
cheapestChangeColor' :: (Background Color, Foreground Color) -> SetColor
cheapestChangeColor :: (Background Color, Foreground Color) -> (Background Color, Foreground Color) -> Maybe SetColor
cost :: RenderCmd -> Int -- the cost is in bytes
render :: [(Location,Value)] -> IO ()
render l = do
let cmds = cheapestCmds l
str = concatMap asString cmds
printStr str
hFlush stdout
cheapestCmds :: [(Location,Value)] -> [RenderCmd]
cheapestCmds [] = []
cheapestCmds l =
let l' = sortDelta l
in renderFirst (head l') ++ cheapestCmds' l'
cheapestCmds' :: [(Location,Value)] -> [RenderCmd]
cheapestCmds' [] = error ""
cheapestCmds' [a] = []
cheapestCmds' l@(a:b:_) = renderNext a b ++ cheapestCmds' (tail l)
sortDelta :: [(Location,Value)] -> [(Location,Value)]
sortDelta = sortByColorThenIncreasingLocation
renderFirst :: (Location, Value) -> RenderCmd
renderNext :: (Location,Value) -> (Location,Value) -> RenderCmd -- Choses the best command (the cheapest one) when there are multiple possibilities.
-}