Sunday, December 21st, 2014

Introducing VGA Console for Pygame

I just finished my initial work on a VGA Console emulator for Pygame. This is great, if say you need to add a retro style VGA Console into your game, or if you want to build an application that takes full advantage of the VGA video adapter. At the moment, I am releasing it as a Preview Demo, and it isn't ready for any production applications or games just yet. However, what is available in this preview is plentiful. You can download the full source from my Bitbucket page, which is linked in the right hand column of my blog. I will also include the source here and try to explain what it can currently so. I have also enclosed a screenshot of the demo app both in this blog post and in the bitbucket repository for those too lazy to download it and try it themselves in Pygame.

import pygame, sys
from pygame.locals import *
import mmap

clock = pygame.time.Clock()

class VGAConsole(object):
    VGA_PALETTE = (
        (0,0,0),
        (0,0,168),
        (0,168,0),
        (0,168,168),
        (168,0,0),
        (168,0,168),
        (168,87,0),
        (168,168,168),
        (87,87,87),
        (87,87,255),
        (87,255,87),
        (87,255,255),
        (255,87,87),
        (255,87,255),
        (255,255,87),
        (255,255,255),
    )
    US_SHIFTMAP = {
        49: 33,
        50: 64,
        51: 35,
        52: 36,
        53: 37,
        54: 94,
        55: 38,
        56: 42,
        57: 40,
        48: 41,
        96: 126,
        45: 95,
        61: 43,
        91: 123,
        93: 125,
        59: 58,
        39: 34,
        92: 124,
        44: 60,
        46: 62,
        47: 63,
    }
    def __init__(self):
        self.vgabuf = mmap.mmap(-1, 4000)
        pygame.display.init()
        pygame.font.init()
        self.screen = pygame.display.set_mode((640,400),0,8)
        pygame.display.set_caption('VGA Console test')
        self.font = pygame.font.Font('VGA.ttf', 16)
        self.cursor = self.font.render(chr(219),0,self.VGA_PALETTE[7])
        pygame.mouse.set_visible(False)
        self.pos = [0,0]
        self.background = self.VGA_PALETTE[1]
        self.shift = False
        self.cframe = 0
        self.cframes = []
        for c in ('|', '/', '-', '\\',):
            self.cframes.append(self.font.render(c,0,self.VGA_PALETTE[15],self.background))
    def draw(self):
        self.screen.fill(self.background)
        self.vgabuf.seek(0)
        for y in range(0,25):
            for x in range(0,80):
                attr = ord(self.vgabuf.read_byte())
                fg,bg = 7,0
                if attr > 0:
                    fg,bg = attr&0xf, (attr&0xf0)>>4
                c = self.vgabuf.read_byte()
                if ord(c) > 0:
                    self.screen.blit(self.font.render(c,0,self.VGA_PALETTE[fg],self.VGA_PALETTE[bg]), (x*8,y*16))
        self.drawMouse()
        self.drawCursor()
        pygame.display.update()
    def setXY(self, row, col, c):
        self.vgabuf.seek((80*row+col)*2)
        self.vgabuf.write(chr(0x1f)+chr(c))
    def type(self, c):
        if c == 13:
            self.pos[1] = 0
            self.pos[0] +=1
        elif c == 8:
            if self.pos[1] > 0:
                self.pos[1] -=1
                self.setXY(self.pos[0], self.pos[1], 0)
        elif c == 9:
            self.pos[1] += 8
        elif c == 27:
            pygame.quit()
            sys.exit()
        else:
            self.setXY(self.pos[0], self.pos[1], c)
            self.pos[1] +=1
        if self.pos[1] > 80:
            self.pos[1] = 0
            self.pos[0] += 1
    def write(self, text):
        for c in text:
            self.type(ord(c))
    def draw_ascii(self):
        row, col = 10,10
        for c in range(0,255):
            self.setXY(row,col,c)
            col +=1
            if col > 41:
                col = 10
                row+=1
    def draw_window(self, row, col, height, width, title=None):
        self.setPos(row, col)
        brd = chr(205)*(width-1)
        self.write(chr(213)+brd+chr(184))
        for y in range(row+1, row+height):
            self.setXY(y, col, 179)
            self.setXY(y, col+width, 179)
        self.setPos(row+height, col)
        self.write(chr(212)+brd+chr(190))
        if title:
            self.setPos(row, col+((width/2)-len(title)/2))
            self.write(title)
    def clear_window(self, row, col, height, width):
        for y in range(row, row+height+1):
            self.setPos(y, col)
            self.write(chr(0)*(width+1))
    def setPos(self, row, col):
        self.pos = [row, col]
    def clearScreen(self):
        self.vgabuf.seek(0)
        self.vgabuf.write(chr(0)*4000)
        self.setPos(0, 0)
    def mousePos(self):
        x,y = pygame.mouse.get_pos()
        return (y/16, x/8)
    def drawMouse(self):
        row,col = self.mousePos()
        self.screen.blit(self.cursor, (col*8,row*16))
    def drawCursor(self):
        self.screen.blit(self.cframes[self.cframe/3%4], (self.pos[1]*8,self.pos[0]*16))
        self.cframe+=1
    def main(self):
        self.draw_ascii()
        self.draw_window(9,9,9,33, ' ASCII ')
        self.setPos(0, 0)
        self.write('Welcome to VGAConsole!\rC:\>')
        self.draw()
        #pygame.event.set_blocked(MOUSEMOTION)
        while 1:
            clock.tick(30)
            for event in pygame.event.get():
                if event.type == QUIT:
                    pygame.quit()
                    sys.exit()
                elif event.type == MOUSEBUTTONUP:
                    oldpos = self.pos
                    self.clear_window(9, 9, 9, 33)
                    self.pos = oldpos
                elif event.type == KEYDOWN:
                    if event.key == K_LSHIFT or event.key == K_RSHIFT:
                        self.shift = True
                    if event.key > 0 and event.key < 256:
                        c = event.key
                        if self.shift:
                            if c > 96 and c < 123:
                                c-=32
                            elif c in self.US_SHIFTMAP.keys():
                                c = self.US_SHIFTMAP[c]
                        self.type(c)
                elif event.type == KEYUP:
                    if event.key == K_LSHIFT or event.key == K_RSHIFT:
                        self.shift = False
            self.draw()

VGAConsole().main()

As you can see from the screenshot, it's very complete looking, with full support for all those ASCII characters you knew and loved. The display itself is rendered from the same format as the VGA memory, where each character on screen takes 2 bytes. One byte stores some attribute information, such as the foreground and background colors, and the other byte is the ASCII character code. In the actual VGA memory, the attribute also supported a blink flag, which I omitted in this VGA Console, as it's annoying and I really don't want to see it used... So, rather than supporting blink, it supports a full 16 colors for both the foreground and the background. In the original VGA memory, it only supported 8-color backgrounds. Since the memory format for VGA is the same as located at &0xB800 memory segment on real hardware, you can technically use a memory dump file created by say BSAVE commands. I may enable native BSAVEing and BLOADing of the memory buffer in future version.

During the draw() function, the VGA memory space is read and a surface is blitted. Currently, this surface is a display Surface object, but in the next release this will change to render to a normal Surface so that it can be blitted by the developer onto whatever Surface or display they want. This may cause issues with the mouse emulation however, I will need to look into that and see how the Pygame mouse works with Surfaces. So, yes, mouse emulation is also present. It displays a familiar block cursor we all used to remember. There is an API to grab it's location on the text display, so that you can determine if a text object or widget has been clicked.

One of the major features which is missing is input and output buffering support. At the moment, the current API allows you to write directly to the VGA memory buffer to draw text onto the console. This isn't very ideal for several reasons. It will not work correctly with stdin/stdout applications, and taking input into a string isn't currently possible. Typing onto the console is currently possible, but the input is not buffered, and the user may edit anything on the screen. Proper stdin and stdout is supported in the next release. This will enable many standard Python functions which require either or both to actually work on the VGA console. This will really open the doors to what will be possible with VGA Console.

About Me

My Photo
Names Kevin, hugely into UNIX technologies, not just Linux. I've dabbled with the demons, played with the Sun, and now with the Penguins.




Kevin Veroneau Consulting Services
Do you require the services of a Django contractor? Do you need both a website and hosting services? Perhaps I can help.

This Month

If you like what you read, please consider donating to help with hosting costs, and to fund future books to review.

Python Powered | © 2012-2014 Kevin Veroneau