module Game.LambdaHack.Client.UI.Frontend.Sdl
( startup, frontendName
#ifdef EXPOSE_INTERNAL
, FontAtlas, FrontendSession(..), startupFun, shutdown, forceShutdown
, display, drawFrame, printScreen, modTranslate, keyTranslate, colorToRGBA
#endif
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import Control.Concurrent
import qualified Data.Char as Char
import qualified Data.EnumMap.Strict as EM
import Data.IORef
import qualified Data.Text as T
import Data.Time.Clock.POSIX
import Data.Time.LocalTime
import qualified Data.Vector.Unboxed as U
import Data.Word (Word32, Word8)
import Foreign.C.String (withCString)
import Foreign.C.Types (CInt)
import Foreign.Ptr (nullPtr)
import Foreign.Storable (peek)
import System.Directory
import System.Exit (exitSuccess)
import System.FilePath
import qualified SDL
import qualified SDL.Font as TTF
import SDL.Input.Keyboard.Codes
import qualified SDL.Internal.Types
import qualified SDL.Raw.Basic as SDL (logSetAllPriority)
import qualified SDL.Raw.Enum
import qualified SDL.Raw.Types
import qualified SDL.Raw.Video
import qualified SDL.Vect as Vect
import Game.LambdaHack.Client.ClientOptions
import Game.LambdaHack.Client.UI.Content.Screen
import Game.LambdaHack.Client.UI.Frame
import Game.LambdaHack.Client.UI.Frontend.Common
import qualified Game.LambdaHack.Client.UI.Key as K
import Game.LambdaHack.Common.File
import Game.LambdaHack.Common.Misc
import Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import Game.LambdaHack.Content.TileKind (floorSymbol)
import qualified Game.LambdaHack.Definition.Color as Color
type FontAtlas = EM.EnumMap Color.AttrCharW32 SDL.Texture
data FrontendSession = FrontendSession
{ swindow :: SDL.Window
, srenderer :: SDL.Renderer
, sfont :: TTF.Font
, satlas :: IORef FontAtlas
, stexture :: IORef SDL.Texture
, spreviousFrame :: IORef SingleFrame
, sforcedShutdown :: IORef Bool
, scontinueSdlLoop :: IORef Bool
, sframeQueue :: MVar SingleFrame
, sframeDrawn :: MVar ()
}
frontendName :: String
frontendName = "sdl"
startup :: ScreenContent -> ClientOptions -> IO RawFrontend
startup coscreen soptions = startupBound $ startupFun coscreen soptions
startupFun :: ScreenContent -> ClientOptions -> MVar RawFrontend -> IO ()
startupFun coscreen soptions@ClientOptions{..} rfMVar = do
SDL.initialize [SDL.InitEvents]
SDL.logSetAllPriority $ toEnum $ fromMaybe 5 slogPriority
let title = fromJust stitle
fontFileName = T.unpack (fromJust sdlFontFile)
fontFileOrig | isRelative fontFileName = fromJust sfontDir </> fontFileName
| otherwise = fontFileName
(fontFileExists, fontFile) <- do
fontFileOrigExists <- doesFileExist fontFileOrig
if fontFileOrigExists
then return (True, fontFileOrig)
else do
let fontFileAlt = dropExtension fontFileOrig <.> "fnt"
fontFileAltExists <- doesFileExist fontFileAlt
return (fontFileAltExists, fontFileAlt)
unless fontFileExists $
fail $ "Font file does not exist: " ++ fontFileOrig
let fontSize = fromJust sscalableFontSize
TTF.initialize
sfont <- TTF.load fontFile fontSize
let isBitmapFile = "fon" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "fnt" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "bdf" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "FON" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "FNT" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "BDF" `isSuffixOf` T.unpack (fromJust sdlFontFile)
sdlSizeAdd = fromJust $ if isBitmapFile
then sdlBitmapSizeAdd
else sdlScalableSizeAdd
boxSize <- (+ sdlSizeAdd) <$> TTF.height sfont
if slogPriority == Just 0 then do
rf <- createRawFrontend coscreen (\_ -> return ()) (return ())
putMVar rfMVar rf
TTF.free sfont
TTF.quit
SDL.quit
else do
SDL.initialize [SDL.InitVideo]
let screenV2 = SDL.V2 (toEnum $ rwidth coscreen * boxSize)
(toEnum $ rheight coscreen * boxSize)
windowConfig = SDL.defaultWindow {SDL.windowInitialSize = screenV2}
rendererConfig = SDL.RendererConfig
{ rendererType = if sbenchmark
then SDL.AcceleratedRenderer
else SDL.AcceleratedVSyncRenderer
, rendererTargetTexture = True
}
swindow <- SDL.createWindow title windowConfig
srenderer <- SDL.createRenderer swindow (-1) rendererConfig
let initTexture = do
texture <- SDL.createTexture srenderer SDL.ARGB8888
SDL.TextureAccessTarget screenV2
SDL.rendererRenderTarget srenderer SDL.$= Just texture
SDL.rendererDrawBlendMode srenderer SDL.$= SDL.BlendNone
SDL.rendererDrawColor srenderer SDL.$= colorToRGBA Color.Black
SDL.clear srenderer
SDL.rendererRenderTarget srenderer SDL.$= Nothing
SDL.copy srenderer texture Nothing Nothing
return texture
texture <- initTexture
satlas <- newIORef EM.empty
stexture <- newIORef texture
spreviousFrame <- newIORef $ blankSingleFrame coscreen
sforcedShutdown <- newIORef False
scontinueSdlLoop <- newIORef True
sframeQueue <- newEmptyMVar
sframeDrawn <- newEmptyMVar
let sess = FrontendSession{..}
rfWithoutPrintScreen <-
createRawFrontend coscreen (display sess) (shutdown sess)
let rf = rfWithoutPrintScreen {fprintScreen = printScreen sess}
putMVar rfMVar rf
let pointTranslate :: forall i. (Enum i) => Vect.Point Vect.V2 i -> Point
pointTranslate (SDL.P (SDL.V2 x y)) =
Point (fromEnum x `div` boxSize) (fromEnum y `div` boxSize)
redraw = do
atlas <- readIORef satlas
writeIORef satlas EM.empty
oldTexture <- readIORef stexture
newTexture <- initTexture
mapM_ SDL.destroyTexture $ EM.elems atlas
SDL.destroyTexture oldTexture
writeIORef stexture newTexture
prevFrame <- readIORef spreviousFrame
writeIORef spreviousFrame (blankSingleFrame coscreen)
drawFrame soptions sess prevFrame
loopSDL :: IO ()
loopSDL = do
me <- SDL.pollEvent
case me of
Nothing -> do
mfr <- tryTakeMVar sframeQueue
case mfr of
Just fr -> do
prevFrame <- readIORef spreviousFrame
unless (prevFrame == fr) $ do
drawFrame soptions sess fr
when sprintEachScreen $ printScreen sess
putMVar sframeDrawn ()
Nothing -> threadDelay $ if sbenchmark then 150 else 15000
Just e -> handleEvent e
continueSdlLoop <- readIORef scontinueSdlLoop
if continueSdlLoop
then loopSDL
else do
TTF.free sfont
TTF.quit
SDL.destroyRenderer srenderer
SDL.destroyWindow swindow
SDL.quit
forcedShutdown <- readIORef sforcedShutdown
when forcedShutdown
exitSuccess
handleEvent e = case SDL.eventPayload e of
SDL.KeyboardEvent keyboardEvent
| SDL.keyboardEventKeyMotion keyboardEvent == SDL.Pressed -> do
let sym = SDL.keyboardEventKeysym keyboardEvent
ksm = SDL.keysymModifier sym
shiftPressed = SDL.keyModifierLeftShift ksm
|| SDL.keyModifierRightShift ksm
key = keyTranslate shiftPressed $ SDL.keysymKeycode sym
modifier = modTranslate ksm
modifierNoShift =
if modifier == K.Shift then K.NoModifier else modifier
p <- SDL.getAbsoluteMouseLocation
when (key == K.Esc) $ resetChanKey (fchanKey rf)
saveKMP rf modifierNoShift key (pointTranslate p)
SDL.MouseButtonEvent mouseButtonEvent
| SDL.mouseButtonEventMotion mouseButtonEvent == SDL.Released -> do
modifier <- modTranslate <$> SDL.getModState
let key = case SDL.mouseButtonEventButton mouseButtonEvent of
SDL.ButtonLeft -> K.LeftButtonRelease
SDL.ButtonMiddle -> K.MiddleButtonRelease
SDL.ButtonRight -> K.RightButtonRelease
_ -> K.LeftButtonRelease
p = SDL.mouseButtonEventPos mouseButtonEvent
saveKMP rf modifier key (pointTranslate p)
SDL.MouseWheelEvent mouseWheelEvent -> do
modifier <- modTranslate <$> SDL.getModState
let SDL.V2 _ y = SDL.mouseWheelEventPos mouseWheelEvent
mkey = case (compare y 0, SDL.mouseWheelEventDirection
mouseWheelEvent) of
(EQ, _) -> Nothing
(LT, SDL.ScrollNormal) -> Just K.WheelSouth
(GT, SDL.ScrollNormal) -> Just K.WheelNorth
(LT, SDL.ScrollFlipped) -> Just K.WheelNorth
(GT, SDL.ScrollFlipped) -> Just K.WheelSouth
p <- SDL.getAbsoluteMouseLocation
maybe (return ())
(\key -> saveKMP rf modifier key (pointTranslate p)) mkey
SDL.WindowClosedEvent{} -> forceShutdown sess
SDL.QuitEvent -> forceShutdown sess
SDL.WindowRestoredEvent{} -> redraw
SDL.WindowExposedEvent{} -> redraw
_ -> return ()
loopSDL
shutdown :: FrontendSession -> IO ()
shutdown FrontendSession{..} = writeIORef scontinueSdlLoop False
forceShutdown :: FrontendSession -> IO ()
forceShutdown sess@FrontendSession{..} = do
writeIORef sforcedShutdown True
shutdown sess
display :: FrontendSession
-> SingleFrame
-> IO ()
display FrontendSession{..} curFrame = do
continueSdlLoop <- readIORef scontinueSdlLoop
if continueSdlLoop then do
putMVar sframeQueue curFrame
takeMVar sframeDrawn
else do
forcedShutdown <- readIORef sforcedShutdown
when forcedShutdown $
threadDelay 50000
drawFrame :: ClientOptions
-> FrontendSession
-> SingleFrame
-> IO ()
drawFrame ClientOptions{..} FrontendSession{..} curFrame = do
let isBitmapFile = "fon" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "fnt" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "bdf" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "FON" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "FNT" `isSuffixOf` T.unpack (fromJust sdlFontFile)
|| "BDF" `isSuffixOf` T.unpack (fromJust sdlFontFile)
sdlSizeAdd = fromJust $ if isBitmapFile
then sdlBitmapSizeAdd
else sdlScalableSizeAdd
boxSize <- (+ sdlSizeAdd) <$> TTF.height sfont
let tt2 = Vect.V2 (toEnum boxSize) (toEnum boxSize)
vp :: Int -> Int -> Vect.Point Vect.V2 CInt
vp x y = Vect.P $ Vect.V2 (toEnum x) (toEnum y)
drawHighlight !x !y !color = do
SDL.rendererDrawColor srenderer SDL.$= colorToRGBA color
let rect = SDL.Rectangle (vp (x * boxSize) (y * boxSize)) tt2
SDL.drawRect srenderer $ Just rect
SDL.rendererDrawColor srenderer SDL.$= colorToRGBA Color.Black
chooseAndDrawHighlight !x !y !bg = case bg of
Color.HighlightNone -> return ()
_ -> drawHighlight x y $ Color.highlightToColor bg
setChar :: Int -> (Word32, Word32) -> IO Int
setChar !i (!w, !wPrev) | w == wPrev = return $! i + 1
setChar i (w, _) = do
atlas <- readIORef satlas
let Point{..} = toEnum i
Color.AttrChar{acAttr=Color.Attr{fg=fgRaw,bg}, acChar=acCharRaw} =
Color.attrCharFromW32 $ Color.AttrCharW32 w
fg | py `mod` 2 == 0 && fgRaw == Color.White = Color.AltWhite
| otherwise = fgRaw
ac = Color.attrChar2ToW32 fg acCharRaw
textTexture <- case EM.lookup ac atlas of
Nothing -> do
let acChar = if not (Color.isBright fg)
&& acCharRaw == floorSymbol
then if isBitmapFile
then Char.chr 7
else Char.chr 8901
else acCharRaw
textSurfaceRaw <- TTF.shadedGlyph sfont (colorToRGBA fg)
(colorToRGBA Color.Black) acChar
Vect.V2 sw sh <- SDL.surfaceDimensions textSurfaceRaw
let width = min boxSize $ fromEnum sw
height = min boxSize $ fromEnum sh
xsrc = max 0 (fromEnum sw - width) `div` 2
ysrc = max 0 (fromEnum sh - height) `div` 2
srcR = SDL.Rectangle (vp xsrc ysrc)
(Vect.V2 (toEnum width) (toEnum height))
xtgt = (boxSize - width) `divUp` 2
ytgt = (boxSize - height) `div` 2
tgtR = vp xtgt ytgt
textSurface <- SDL.createRGBSurface tt2 SDL.ARGB8888
SDL.surfaceFillRect textSurface Nothing (colorToRGBA Color.Black)
void $ SDL.surfaceBlit textSurfaceRaw (Just srcR)
textSurface (Just tgtR)
SDL.freeSurface textSurfaceRaw
textTexture <- SDL.createTextureFromSurface srenderer textSurface
SDL.freeSurface textSurface
writeIORef satlas $ EM.insert ac textTexture atlas
return textTexture
Just textTexture -> return textTexture
let tgtR = SDL.Rectangle (vp (px * boxSize) (py * boxSize)) tt2
SDL.copy srenderer textTexture Nothing (Just tgtR)
chooseAndDrawHighlight px py bg
return $! i + 1
texture <- readIORef stexture
prevFrame <- readIORef spreviousFrame
writeIORef spreviousFrame curFrame
SDL.rendererRenderTarget srenderer SDL.$= Just texture
SDL.rendererDrawColor srenderer SDL.$= colorToRGBA Color.Black
U.foldM'_ setChar 0 $ U.zip (PointArray.avector $ singleFrame curFrame)
(PointArray.avector $ singleFrame prevFrame)
SDL.rendererRenderTarget srenderer SDL.$= Nothing
SDL.copy srenderer texture Nothing Nothing
SDL.present srenderer
printScreen :: FrontendSession -> IO ()
printScreen FrontendSession{..} = do
dataDir <- appDataDir
tryCreateDir dataDir
tryCreateDir $ dataDir </> "screenshots"
utcTime <- getCurrentTime
timezone <- getTimeZone utcTime
let unspace = map $ \c -> case c of
' ' -> '_'
':' -> '.'
_ -> c
dateText = unspace $ take 25 $ show $ utcToLocalTime timezone utcTime
fileName = dataDir </> "screenshots" </> "prtscn" <> dateText <.> "bmp"
SDL.Internal.Types.Renderer renderer = srenderer
Vect.V2 sw sh <- SDL.get $ SDL.windowSize swindow
ptrOut <- SDL.Raw.Video.createRGBSurface 0 sw sh 32 0 0 0 0
surfaceOut <- peek ptrOut
void $ SDL.Raw.Video.renderReadPixels
renderer
nullPtr
SDL.Raw.Enum.SDL_PIXELFORMAT_ARGB8888
(SDL.Raw.Types.surfacePixels surfaceOut)
(sw * 4)
withCString fileName $ \fileNameCString ->
void $! SDL.Raw.Video.saveBMP ptrOut fileNameCString
SDL.Raw.Video.freeSurface ptrOut
modTranslate :: SDL.KeyModifier -> K.Modifier
modTranslate m =
modifierTranslate
(SDL.keyModifierLeftCtrl m || SDL.keyModifierRightCtrl m)
(SDL.keyModifierLeftShift m || SDL.keyModifierRightShift m)
(SDL.keyModifierLeftAlt m
|| SDL.keyModifierRightAlt m
|| SDL.keyModifierAltGr m)
False
keyTranslate :: Bool -> SDL.Keycode -> K.Key
keyTranslate shiftPressed n = case n of
KeycodeEscape -> K.Esc
KeycodeReturn -> K.Return
KeycodeBackspace -> K.BackSpace
KeycodeTab -> if shiftPressed then K.BackTab else K.Tab
KeycodeSpace -> K.Space
KeycodeExclaim -> K.Char '!'
KeycodeQuoteDbl -> K.Char '"'
KeycodeHash -> K.Char '#'
KeycodePercent -> K.Char '%'
KeycodeDollar -> K.Char '$'
KeycodeAmpersand -> K.Char '&'
KeycodeQuote -> if shiftPressed then K.Char '"' else K.Char '\''
KeycodeLeftParen -> K.Char '('
KeycodeRightParen -> K.Char ')'
KeycodeAsterisk -> K.Char '*'
KeycodePlus -> K.Char '+'
KeycodeComma -> if shiftPressed then K.Char '<' else K.Char ','
KeycodeMinus -> if shiftPressed then K.Char '_' else K.Char '-'
KeycodePeriod -> if shiftPressed then K.Char '>' else K.Char '.'
KeycodeSlash -> if shiftPressed then K.Char '?' else K.Char '/'
Keycode1 -> if shiftPressed then K.Char '!' else K.Char '1'
Keycode2 -> if shiftPressed then K.Char '@' else K.Char '2'
Keycode3 -> if shiftPressed then K.Char '#' else K.Char '3'
Keycode4 -> if shiftPressed then K.Char '$' else K.Char '4'
Keycode5 -> if shiftPressed then K.Char '%' else K.Char '5'
Keycode6 -> if shiftPressed then K.Char '^' else K.Char '6'
Keycode7 -> if shiftPressed then K.Char '&' else K.Char '7'
Keycode8 -> if shiftPressed then K.Char '*' else K.Char '8'
Keycode9 -> if shiftPressed then K.Char '(' else K.Char '9'
Keycode0 -> if shiftPressed then K.Char ')' else K.Char '0'
KeycodeColon -> K.Char ':'
KeycodeSemicolon -> if shiftPressed then K.Char ':' else K.Char ';'
KeycodeLess -> K.Char '<'
KeycodeEquals -> if shiftPressed then K.Char '+' else K.Char '='
KeycodeGreater -> K.Char '>'
KeycodeQuestion -> K.Char '?'
KeycodeAt -> K.Char '@'
KeycodeLeftBracket -> if shiftPressed then K.Char '{' else K.Char '['
KeycodeBackslash -> if shiftPressed then K.Char '|' else K.Char '\\'
KeycodeRightBracket -> if shiftPressed then K.Char '}' else K.Char ']'
KeycodeCaret -> K.Char '^'
KeycodeUnderscore -> K.Char '_'
KeycodeBackquote -> if shiftPressed then K.Char '~' else K.Char '`'
KeycodeUp -> K.Up
KeycodeDown -> K.Down
KeycodeLeft -> K.Left
KeycodeRight -> K.Right
KeycodeHome -> K.Home
KeycodeEnd -> K.End
KeycodePageUp -> K.PgUp
KeycodePageDown -> K.PgDn
KeycodeInsert -> K.Insert
KeycodeDelete -> K.Delete
KeycodePrintScreen -> K.PrintScreen
KeycodeClear -> K.Begin
KeycodeKPClear -> K.Begin
KeycodeKPDivide -> if shiftPressed then K.Char '?' else K.Char '/'
KeycodeKPMultiply -> K.KP '*'
KeycodeKPMinus -> K.Char '-'
KeycodeKPPlus -> K.Char '+'
KeycodeKPEnter -> K.Return
KeycodeKPEquals -> K.Return
KeycodeKP1 -> if shiftPressed then K.KP '1' else K.End
KeycodeKP2 -> if shiftPressed then K.KP '2' else K.Down
KeycodeKP3 -> if shiftPressed then K.KP '3' else K.PgDn
KeycodeKP4 -> if shiftPressed then K.KP '4' else K.Left
KeycodeKP5 -> if shiftPressed then K.KP '5' else K.Begin
KeycodeKP6 -> if shiftPressed then K.KP '6' else K.Right
KeycodeKP7 -> if shiftPressed then K.KP '7' else K.Home
KeycodeKP8 -> if shiftPressed then K.KP '8' else K.Up
KeycodeKP9 -> if shiftPressed then K.KP '9' else K.PgUp
KeycodeKP0 -> if shiftPressed then K.KP '0' else K.Insert
KeycodeKPPeriod -> K.Char '.'
KeycodeKPComma -> K.Char '.'
KeycodeF1 -> K.Fun 1
KeycodeF2 -> K.Fun 2
KeycodeF3 -> K.Fun 3
KeycodeF4 -> K.Fun 4
KeycodeF5 -> K.Fun 5
KeycodeF6 -> K.Fun 6
KeycodeF7 -> K.Fun 7
KeycodeF8 -> K.Fun 8
KeycodeF9 -> K.Fun 9
KeycodeF10 -> K.Fun 10
KeycodeF11 -> K.Fun 11
KeycodeF12 -> K.Fun 12
KeycodeLCtrl -> K.DeadKey
KeycodeLShift -> K.DeadKey
KeycodeLAlt -> K.DeadKey
KeycodeLGUI -> K.DeadKey
KeycodeRCtrl -> K.DeadKey
KeycodeRShift -> K.DeadKey
KeycodeRAlt -> K.DeadKey
KeycodeRGUI -> K.DeadKey
KeycodeMode -> K.DeadKey
KeycodeNumLockClear -> K.DeadKey
KeycodeUnknown -> K.Unknown "KeycodeUnknown"
_ -> let i = fromEnum $ unwrapKeycode n
in if | 97 <= i && i <= 122
&& shiftPressed -> K.Char $ Char.chr $ i - 32
| 32 <= i && i <= 126 -> K.Char $ Char.chr i
| otherwise -> K.Unknown $ show n
sDL_ALPHA_OPAQUE :: Word8
sDL_ALPHA_OPAQUE = 255
colorToRGBA :: Color.Color -> SDL.V4 Word8
colorToRGBA Color.Black = SDL.V4 0 0 0 sDL_ALPHA_OPAQUE
colorToRGBA Color.Red = SDL.V4 0xD5 0x05 0x05 sDL_ALPHA_OPAQUE
colorToRGBA Color.Green = SDL.V4 0x05 0x9D 0x05 sDL_ALPHA_OPAQUE
colorToRGBA Color.Brown = SDL.V4 0xCA 0x4A 0x05 sDL_ALPHA_OPAQUE
colorToRGBA Color.Blue = SDL.V4 0x05 0x56 0xF4 sDL_ALPHA_OPAQUE
colorToRGBA Color.Magenta = SDL.V4 0xAF 0x0E 0xAF sDL_ALPHA_OPAQUE
colorToRGBA Color.Cyan = SDL.V4 0x05 0x96 0x96 sDL_ALPHA_OPAQUE
colorToRGBA Color.White = SDL.V4 0xB8 0xBF 0xCB sDL_ALPHA_OPAQUE
colorToRGBA Color.AltWhite = SDL.V4 0xC4 0xBE 0xB1 sDL_ALPHA_OPAQUE
colorToRGBA Color.BrBlack = SDL.V4 0x6F 0x5F 0x5F sDL_ALPHA_OPAQUE
colorToRGBA Color.BrRed = SDL.V4 0xFF 0x55 0x55 sDL_ALPHA_OPAQUE
colorToRGBA Color.BrGreen = SDL.V4 0x65 0xF1 0x36 sDL_ALPHA_OPAQUE
colorToRGBA Color.BrYellow = SDL.V4 0xEB 0xD6 0x42 sDL_ALPHA_OPAQUE
colorToRGBA Color.BrBlue = SDL.V4 0x4D 0x98 0xF4 sDL_ALPHA_OPAQUE
colorToRGBA Color.BrMagenta = SDL.V4 0xFF 0x77 0xFF sDL_ALPHA_OPAQUE
colorToRGBA Color.BrCyan = SDL.V4 0x52 0xF4 0xE5 sDL_ALPHA_OPAQUE
colorToRGBA Color.BrWhite = SDL.V4 0xFF 0xFF 0xFF sDL_ALPHA_OPAQUE