Pixmap, Pixbuf and memory lane

January 5th, 2009 posted by codders

So how’s the Haskell project going? So far, so good. When I last wrote, I’d managed to load a “Hello World!” GUI for my application. Since then I’ve managed to parse up some content and render it to the screen. Current revision is 121ed7f2.

Antediluvian file formats

The project, in case you’ve not been following on github, is to resurrect a long-since abandoned game. This game was released as a cover disk on an Amiga magazine back in “the day” and the code has since been made available by the authors (in 68000 assembly), ported to BlitzBasic and (abortively) ported to C++/SDL.

The maps for the BlitzBasic game were stored as tile maps. Each game level combines a tile set and a map to place the tiles on the screen. For additional kicks, some of the files are compressed under a scheme called “BPCK” which involves fairly simple run-length encoding. The code to parse up those files was… err… relatively straightforward to write although I did have some issues with bit-order and palette mapping.

Tiles as Pixbufs

The tiles are stored as sequences of nibbles mapping pixels on to a 16-colour palette. In order to draw these, I extracted the RGB for the nibbles and inserted that 3-byte-per-pixel sequence into a pixmap (this is the source):

buildTile :: [BPCK.PaletteEntry] -> BPCK.Gliph -> IO Pixbuf
buildTile palette g = do
    let gData = BPCK.gliphData g
    buf <- pixbufNew ColorspaceRgb False 8 gliphX gliphY
    pbData <- (pixbufGetPixels buf :: IO (PixbufData Int Word8))
    rowStride <- pixbufGetRowstride buf
    chan <- pixbufGetNChannels buf -- Hopefully this is 3 (R,G,B)
    bits <- pixbufGetBitsPerSample buf -- Hopefully this is 8
    doFromTo 0 (gliphX - 1) $ \y ->
      doFromTo 0 (gliphY - 1) $ \x -> do
        let pixbufoffset = x*chan + y*rowStride
        let gliphOffset = fromIntegral $ x + y*gliphX
        let paletteIndex = B.index gData gliphOffset
        let thiscolor = palette !! fromIntegral paletteIndex
        writeArray pbData (pixbufoffset) (fromIntegral $ BPCK.red thiscolor)
        writeArray pbData (1 + pixbufoffset) (fromIntegral $ BPCK.green thiscolor)
        writeArray pbData (2 + pixbufoffset) (fromIntegral $ BPCK.blue thiscolor)
    return buf
    where gliphX = BPCK.gliphWidth g
             gliphY = BPCK.gliphHeight g

Maps as Pixmaps

The difference between a pixbuf and a pixmap is, as I understand it, that a pixbuf is an X-client side block of data into which you can load pixel information and a pixmap is an X-server side drawable (in the GTK sense) object. Pixbuf is more like a brush for painting, and Pixmap is an off-screen canvas. So to build my map I blatted the tile pixbufs on to a pixmap:

createTiledPixmap :: BPCK.ParsedImage -> BPCK.ParsedTileMap -> IO Pixmap
createTiledPixmap tileSet tileMap = do
    putStrLn $ "Building tile pixmaps"
    tiles <- tilesFromImageData tileSet
    let tileCount = length tiles
    putStrLn $ "Creating new pixmap " ++ show totalWidthPixels ++ " x " ++ show totalHeightPixels
    pixmap <- pixmapNew (Nothing :: Maybe DrawWindow) totalWidthPixels totalHeightPixels (Just 24)
    gc <- gcNew pixmap
    doFromTo 0 (tilesHigh - 1) $ \iy ->
      doFromTo 0 (tilesAcross - 1) $ \ix -> do
        let tileIndex = ix + (iy * tilesAcross)
        let tileId = (min (fromIntegral (BPCK.tileMap tileMap !! tileIndex)) tileCount) `mod` tileCount
        let curX = ix * tileSizePixels
        let curY = iy * tileSizePixels
        postGUIAsync $ drawPixbuf pixmap gc (tiles !! tileId) 0 0 curX curY tileSizePixels tileSizePixels RgbDitherNone 0 0
    return pixmap
    where tileSizePixels = BPCK.gliphSize tileSet
             tilesAcross = BPCK.tilesAcross tileMap
             tilesHigh = BPCK.tilesHigh tileMap
             totalWidthPixels = tileSizePixels * tilesAcross
             totalHeightPixels = tileSizePixels * tilesHigh

Results

The finished article looks a bit like this:

Screenshot
or this:

Screenshot
and pressing the space bar cycles through the maps on account of this:

onKeyPress app (\x@(Key { eventKeyName = name,
                              eventKeyChar = char }) -> do
    case char of
      Just ' ' -> do
        putStrLn $ "Switching map"
        currentState <- readIORef mapStateRef
        nextState <- nextMapState currentState
        writeIORef mapStateRef nextState
        drawWin <- widgetGetDrawWindow canvas
        gc <- gcNew drawWin
        (width, height) <- drawableGetSize drawWin
        postGUIAsync $ drawDrawable drawWin gc (renderedMap nextState) 0 0 0 0 width height
      Just c -> putStrLn $ "Press " ++ name ++ "('" ++ [c] ++ "')"
      Nothing -> putStrLn $ "weird key: " ++ name
    return (eventSent x))

Conclusions

I’m now a lot more comfortable with Haskell and think I’ve mastered the “word-for-word translation of imperative to pseudo-functional” style. The file parsing code I’ve written performs like a dog on account of that, but it’s not on a critical path for anything. One thing that has struck me is how easy it is to refactor Haskell code (as long as you use hanging ‘do’s to avoid formatting upsets). So I’ll stick at it and see if I can’t finish this project inside of a year.

Why not SDL?

FYYFDANSEIC, etc.