de.codesourcery.jasm16.emulator.devices.impl.DefaultScreen.java Source code

Java tutorial

Introduction

Here is the source code for de.codesourcery.jasm16.emulator.devices.impl.DefaultScreen.java

Source

package de.codesourcery.jasm16.emulator.devices.impl;

/**
 * Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.LockSupport;

import javax.imageio.ImageIO;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import de.codesourcery.jasm16.Address;
import de.codesourcery.jasm16.AddressRange;
import de.codesourcery.jasm16.Register;
import de.codesourcery.jasm16.Size;
import de.codesourcery.jasm16.WordAddress;
import de.codesourcery.jasm16.compiler.io.ClassPathResource;
import de.codesourcery.jasm16.compiler.io.IResource.ResourceType;
import de.codesourcery.jasm16.emulator.ICPU;
import de.codesourcery.jasm16.emulator.IEmulator;
import de.codesourcery.jasm16.emulator.ILogger;
import de.codesourcery.jasm16.emulator.devices.DeviceDescriptor;
import de.codesourcery.jasm16.emulator.devices.IDevice;
import de.codesourcery.jasm16.emulator.exceptions.DeviceErrorException;
import de.codesourcery.jasm16.emulator.memory.IMemory;
import de.codesourcery.jasm16.emulator.memory.MemoryRegion;
import de.codesourcery.jasm16.utils.Misc;

public final class DefaultScreen implements IDevice {

    private static final Logger LOG = Logger.getLogger(DefaultScreen.class);

    private static final boolean ENABLE_SCREEN_REDRAW = true;

    public static final int STANDARD_SCREEN_ROWS = 12;
    public static final int STANDARD_SCREEN_COLUMNS = 32;

    public static final Color DEFAULT_BORDER_COLOR = Color.black;

    private final int SCREEN_ROWS;
    private final int SCREEN_COLUMNS;

    private final int VIDEO_RAM_SIZE_IN_WORDS;

    public static final int GLYPH_WIDTH = 4;
    public static final int GLYPH_HEIGHT = 8;

    public static final int PALETTE_COLORS = 16;

    public static final int IMG_CHARS_PER_ROW = 32;

    public static final int BORDER_WIDTH = 10;
    public static final int BORDER_HEIGHT = 10;

    private final int SCREEN_WIDTH;
    private final int SCREEN_HEIGHT;

    private volatile ILogger out;

    public static final DeviceDescriptor DESC = new DeviceDescriptor("LEM-1802", "Low Energy Monitor", 0x7349f615,
            0x1802, 0x1c6c8b36);

    private static final BufferedImage DEFAULT_GLYPH_IMAGE;

    static {
        final ClassPathResource resource = new ClassPathResource("default_font.png", ResourceType.UNKNOWN);
        try {
            final InputStream in = resource.createInputStream();
            try {
                DEFAULT_GLYPH_IMAGE = ImageIO.read(in);
            } finally {
                IOUtils.closeQuietly(in);
            }
        } catch (IOException e) {
            LOG.error("getDefaultFontImage(): Internal error, failed to load default font image 'default_font.png'",
                    e);
            throw new RuntimeException(e);
        }
    }

    private final boolean mapVideoRamUponAddDevice;
    private final boolean mapFontRAMUponAddDevice;

    private final Object PEER_LOCK = new Object();

    // @GuardedBy( PEER_LOCK )
    private Component peer;

    private final ConsoleScreen consoleScreen;

    private volatile IEmulator emulator = null;

    // default background color
    private volatile int borderPaletteIndex = 0;

    // palette
    private volatile PaletteRAM paletteRAM = new PaletteRAM(WordAddress.ZERO);

    // glyph/font RAM
    private volatile FontRAM fontRAM = new FontRAM(WordAddress.ZERO);

    // Video RAM
    private volatile VideoRAM videoRAM = null;

    private volatile RefreshThread refreshThread = null;

    private volatile boolean blinkingCharactersOnScreen = false;
    private volatile boolean lastBlinkState;
    private volatile boolean blinkState;

    private final class RefreshThread extends Thread {

        private volatile boolean terminate = false;
        private final CountDownLatch latch = new CountDownLatch(1);

        private int fpsCounter;
        private int lastFps;
        private long lastTimestamp = System.currentTimeMillis();

        @Override
        public void run() {
            try {
                while (!terminate) {
                    LockSupport.parkNanos((1000 / 30) * 1000000);

                    if (ENABLE_SCREEN_REDRAW) {
                        renderScreen();
                    }

                    int counter = fpsCounter++;
                    if ((counter % 30) == 0) { // let characters blink every 30 frames
                        blinkState = !blinkState;
                    }

                    if ((counter % 300) == 0) {
                        final long now = System.currentTimeMillis();
                        final float delta = (now - lastTimestamp) / 1000.0f;
                        if (delta > 0) {
                            float fps = (counter - lastFps) / delta;
                            logDebug("FPS: " + fps);
                        }
                        lastFps = counter;
                        lastTimestamp = now;
                    }
                }
            } finally {
                latch.countDown();
            }
        }

        public void terminate() {
            terminate = true;
            try {
                latch.await();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    /**
     * 
     * @param mapVideoRamUponAddDevice  whether to map video RAM to 0x8000 when afterAddDevice() is called
     * @param mapFontRAMUponAddDevice whether to map font/glyph RAM to 0x8180 when afterAddDevice() is called
     */
    public DefaultScreen(boolean mapVideoRamUponAddDevice, boolean mapFontRAMUponAddDevice) {
        this(STANDARD_SCREEN_COLUMNS, STANDARD_SCREEN_ROWS, mapVideoRamUponAddDevice, mapFontRAMUponAddDevice);
    }

    /**
     * 
     * @param screenColumns
     * @param screenRows
     * 
     * @param mapVideoRamUponAddDevice  whether to map video RAM to 0x8000 when afterAddDevice() is called
     * @param mapFontRAMUponAddDevice whether to map font/glyph RAM to 0x8180 when afterAddDevice() is called     
     */
    public DefaultScreen(int screenColumns, int screenRows, boolean mapVideoRamUponAddDevice,
            boolean mapFontRAMUponAddDevice) {
        if (screenColumns < STANDARD_SCREEN_COLUMNS) {
            throw new IllegalArgumentException(
                    "Illegal column count " + screenColumns + ", must be at least " + STANDARD_SCREEN_COLUMNS);
        }
        if (screenRows < STANDARD_SCREEN_ROWS) {
            throw new IllegalArgumentException(
                    "Illegal row count " + screenRows + ", must be at least " + STANDARD_SCREEN_ROWS);
        }

        this.SCREEN_COLUMNS = screenColumns;
        this.SCREEN_ROWS = screenRows;
        this.VIDEO_RAM_SIZE_IN_WORDS = SCREEN_ROWS * SCREEN_COLUMNS;
        this.SCREEN_WIDTH = (SCREEN_COLUMNS * GLYPH_WIDTH) + 2 * (BORDER_WIDTH);
        this.SCREEN_HEIGHT = (SCREEN_ROWS * GLYPH_HEIGHT) + 2 * (BORDER_HEIGHT);

        this.consoleScreen = new ConsoleScreen(DEFAULT_GLYPH_IMAGE, SCREEN_WIDTH, SCREEN_HEIGHT,
                DEFAULT_BORDER_COLOR);
        setupDefaultFontRAM();
        renderScreenDisconnectedMessage();

        this.mapVideoRamUponAddDevice = mapVideoRamUponAddDevice;
        this.mapFontRAMUponAddDevice = mapFontRAMUponAddDevice;
        setupDefaultPaletteRAM();
    }

    protected void logError(String msg) {
        if (emulator != null) {
            emulator.getOutput().error(msg);
        }
    }

    protected void logError(String msg, Throwable t) {
        if (emulator != null) {
            emulator.getOutput().error(msg, t);
        }
    }

    protected void logDebugHeadline(String msg) {
        if (emulator != null && emulator.getOutput().isDebugEnabled()) {
            final String separator = StringUtils.repeat("*", msg.length() + 4) + "\n";
            emulator.getOutput().debug("\n" + separator + "* " + msg + " *\n" + separator);
        }
    }

    protected void logDebug(String msg) {
        if (emulator != null && emulator.getOutput().isDebugEnabled()) {
            emulator.getOutput().debug(msg);
        }
    }

    @Override
    public void reset() {
        synchronized (PEER_LOCK) {
            setupDefaultFontRAM(); // calls unmap()

            paletteRAM.unmap();
            paletteRAM.setDefaultPalette();

            if (videoRAM != null && !mapVideoRamUponAddDevice) {
                videoRAM.unmap();
                videoRAM = null;
            }
            renderScreenDisconnectedMessage();

            blinkingCharactersOnScreen = false;
            lastBlinkState = false;
            blinkState = false;
        }
    }

    protected class StatefulMemoryRegion extends MemoryRegion {

        private boolean isMapped = false;

        public StatefulMemoryRegion(String regionName, long typeId, AddressRange range, Flag... flags) {
            super(regionName, typeId, range, flags);
        }

        public synchronized boolean isMappedTo(Address startingAddress) {
            return isMapped && getAddressRange().getStartAddress().equals(startingAddress);
        }

        public synchronized void map() {
            if (!isMapped) {
                emulator.mapRegion(this);
                isMapped = true;
            }
        }

        public synchronized boolean unmap() {
            if (isMapped) {
                emulator.mapRegion(this);
                isMapped = false;
                return true;
            }
            return false;
        }
    }

    protected final class FontRAM extends StatefulMemoryRegion {
        private final AtomicBoolean hasChanged = new AtomicBoolean(true);

        public FontRAM(Address start) {
            super("Font RAM", TYPE_FONT_RAM, new AddressRange(start, Size.words(256)),
                    MemoryRegion.Flag.MEMORY_MAPPED_HW); // 2 words per character
        }

        public void setup(ConsoleScreen scr) {

            int adr = 0;
            for (int glyph = 0; glyph < 128; glyph++) {
                final int words = scr.readGylph(glyph);
                final int word0 = (words & 0xffff0000) >>> 16;
                final int word1 = (words & 0x0000ffff);
                super.write(adr++, word0);
                super.write(adr++, word1);
            }
            hasChanged.set(true);
        }

        @Override
        public void write(Address address, int value) {
            super.write(address, value);
            hasChanged.set(true);
        }

        @Override
        public void write(int wordAddress, int value) {
            super.write(wordAddress, value);
            hasChanged.set(true);
        }

        @Override
        public void clear() {
            super.clear();
            hasChanged.set(true);
        }

        public boolean hasChanged() {
            return hasChanged.getAndSet(false);
        }

        public void defineAllGlyphs() {

            final int end = getSize().getSizeInWords();

            for (int wordAddress = 0; wordAddress < end; wordAddress += 2) {
                final int glyphIndex = wordAddress >>> 1; // 2 words per glyph

                final int value1 = read(wordAddress);
                final int value2 = read(wordAddress + 1);
                // assemble into 32-bit word
                final int newGlyph = ((value1 & 0xffff) << 16) | value2;
                consoleScreen.defineGylph(glyphIndex, newGlyph);
            }
            return;
        }
    }

    protected final void repaintPeer() {
        synchronized (PEER_LOCK) {
            if (peer != null) {
                peer.repaint();
            }
        }
    }

    protected void setupDefaultFontRAM() {

        fontRAM.unmap();
        FontRAM tmp = new FontRAM(Address.wordAddress(0));
        tmp.setup(consoleScreen);
        this.fontRAM = tmp;
    }

    protected void mapFontRAM(Address address) {
        synchronized (PEER_LOCK) {
            final boolean wasAlreadyMapped = this.fontRAM.unmap();

            this.fontRAM = new FontRAM(address);
            this.fontRAM.map();

            // initialize RAM *AFTER* map() because the map() method
            // will OVERWRITE the palette RAMs contents
            if (!wasAlreadyMapped) {
                logDebug("Initializing font RAM");
                this.fontRAM.setup(consoleScreen);
            }
        }
    }

    protected final class PaletteRAM extends StatefulMemoryRegion {
        private final AtomicReferenceArray<Color> cache = new AtomicReferenceArray<Color>(PALETTE_COLORS);

        private final AtomicBoolean hasChanged = new AtomicBoolean(true);

        public PaletteRAM(Address start) {
            super("Palette RAM", TYPE_PALETTE_RAM, new AddressRange(start, Size.words(PALETTE_COLORS)),
                    MemoryRegion.Flag.MEMORY_MAPPED_HW);
        }

        public boolean hasChanged() {
            return hasChanged.getAndSet(false);
        }

        public void setDefaultPalette() {
            // default palette as used in notch's emulator
            final int[] defaultPalette = { 0x0000, 0x000a, 0x00a0, 0x00aa, 0x0a00, 0x0a0a, 0x0a50, 0x0aaa, 0x0555,
                    0x055f, 0x05f5, 0x05ff, 0x0f55, 0x0f5f, 0x0ff5, 0x0fff };

            if (defaultPalette.length != PALETTE_COLORS) {
                throw new RuntimeException("Internal error, default palette color count mismatch");
            }

            for (int i = 0; i < defaultPalette.length; i++) {
                write(i, defaultPalette[i]);
            }
        }

        @Override
        public void clear() {
            final int size = getSize().toSizeInWords().getValue();
            for (int i = 0; i < size; i++) {
                write(i, 0);
            }
        }

        private Color toJavaColor(int colorValue) {

            /*
             * The LEM1802 has a default built in palette. If the user chooses, they may
             * supply their own palette by mapping a 16 word memory region with one word
             * per palette entry in the 16 color palette.
             * 
             * Each color entry has the following bit format (in LSB-0): 0000rrrrggggbbbb
             *     
             * Where r, g, b are the red, green and blue channels. A higher value means a
             * lighter color.        
             */
            final int r = ((colorValue >>> 8) & (1 + 2 + 4 + 8)) << 4; // multiply by 16 to get full 0...255 (8-bit) range
            final int g = ((colorValue >>> 4) & (1 + 2 + 4 + 8)) << 4; // multiply by 16 to get full 0...255 (8-bit) range
            final int b = ((colorValue) & (1 + 2 + 4 + 8)) << 4; // multiply by 16 to get full 0...255 (8-bit) range
            return new Color(r, g, b);
        }

        public Color getColor(int index) {
            return cache.get(index);
        }

        @Override
        public void write(Address address, int value) {
            super.write(address, value);
            cache.set(address.getValue(), toJavaColor(value));
            hasChanged.set(true);
        }

        @Override
        public void write(int wordAddress, int value) {
            super.write(wordAddress, value);
            cache.set(wordAddress, toJavaColor(value));
            hasChanged.set(true);
        }
    }

    protected final class VideoRAM extends StatefulMemoryRegion {

        private final AtomicBoolean hasChanged = new AtomicBoolean(false);

        public VideoRAM(Address start) {
            super("Video RAM", TYPE_VRAM, new AddressRange(start, Size.words(VIDEO_RAM_SIZE_IN_WORDS)),
                    MemoryRegion.Flag.MEMORY_MAPPED_HW);
        }

        @Override
        public void clear() {
            super.clear();
            hasChanged.set(true);
        }

        public boolean hasChanged() {
            return hasChanged.getAndSet(false);
        }

        @Override
        public void write(Address address, int value) {
            super.write(address, value);
            hasChanged.set(true);
        }

        @Override
        public void write(int wordAddress, int value) {
            super.write(wordAddress, value);
            hasChanged.set(true);
        }
    }

    protected void setupDefaultPaletteRAM() {
        paletteRAM.unmap();
        paletteRAM = new PaletteRAM(Address.wordAddress(0));
        paletteRAM.setDefaultPalette();
    }

    protected void mapPaletteRAM(Address address) {
        synchronized (PEER_LOCK) {
            if (paletteRAM.isMappedTo(address)) {
                return;
            }

            final boolean wasAlreadyMapped = paletteRAM.unmap();
            paletteRAM = new PaletteRAM(address);
            paletteRAM.map();

            // initialize RAM *AFTER* map() because the map() method
            // will OVERWRITE the palette RAMs contents
            if (!wasAlreadyMapped) {
                logDebug("Initializing palette RAM");
                paletteRAM.setDefaultPalette();
            }
        }
    }

    private boolean isActive() {
        return videoRAM != null && isAttached();
    }

    private boolean isAttached() {
        synchronized (PEER_LOCK) {
            return peer != null;
        }
    }

    protected void mapVideoRAM(Address videoRAMAddress) {
        synchronized (PEER_LOCK) {
            if (videoRAM != null) {
                if (videoRAM.isMappedTo(videoRAMAddress)) {
                    return;
                }
                videoRAM.unmap();
            }
            videoRAM = new VideoRAM(videoRAMAddress);
            videoRAM.map();
        }
    }

    private void renderScreen() {
        synchronized (PEER_LOCK) {
            if (!isActive()) {
                renderScreenDisconnectedMessage();
                return;
            }

            final boolean fontRAMChanged = fontRAM.hasChanged();
            final boolean updateRequired = fontRAMChanged || paletteRAM.hasChanged() || videoRAM.hasChanged();

            if (updateRequired || (blinkingCharactersOnScreen && lastBlinkState != blinkState)) {
                if (fontRAMChanged) {
                    fontRAM.defineAllGlyphs();
                }

                final boolean blink = blinkState;
                lastBlinkState = blink;

                boolean blinkingChars = false;
                for (int i = 0; i < VIDEO_RAM_SIZE_IN_WORDS; i++) {
                    blinkingChars |= renderMemoryValue(i, videoRAM.read(i), blink);
                }
                blinkingCharactersOnScreen = blinkingChars;

                repaintPeer();
            }
        }
    }

    protected void disconnect() {
        if (videoRAM != null) {
            if (fontRAM != null) {
                fontRAM.unmap();
                fontRAM = null;
            }

            if (paletteRAM != null) {
                paletteRAM.unmap();
                paletteRAM = null;
            }

            if (videoRAM != null) {
                videoRAM.unmap();
                videoRAM = null;
            }
            renderScreenDisconnectedMessage();
        }
    }

    private void renderScreenDisconnectedMessage() {
        consoleScreen.renderScreenDisconnectedMessage();
        repaintPeer();
    }

    protected boolean renderMemoryValue(int wordAddress, int memoryValue, boolean blinkState) {
        /* The LEM1802 is a 128x96 pixel color display compatible with the DCPU-16.
         * The display is made up of 32x12 16 bit cells.
         * Each cell displays one monochrome 4x8 pixel character out of 128 available.
         */
        final int row = wordAddress / SCREEN_COLUMNS;
        final int column = wordAddress - (row * SCREEN_COLUMNS);

        final boolean blink = (memoryValue & (1 << 7)) != 0;
        final int asciiCode = memoryValue & (1 + 2 + 4 + 8 + 16 + 32 + 64);

        final int backgroundPalette = (memoryValue >>> 8) & (1 + 2 + 4 + 8);

        /*
         * The video RAM is made up of 32x12 cells of the following bit format (in LSB-0):
         * 
         * ffffbbbbBccccccc
         *
         * - The lowest 7 bits (ccccccc) select define character to display.
         * - If B (bit 7) is set the character color will blink slowly.
         * - ffff selects which foreground color to use.
         * - bbbb selects which background color to use.    
         */
        final int foregroundPalette = (memoryValue >>> 12) & (1 + 2 + 4 + 8);
        final Color fg = paletteRAM.getColor(foregroundPalette);
        final Color bg = paletteRAM.getColor(backgroundPalette);

        if (blink && !blinkState) {
            consoleScreen.putChar(column, row, asciiCode, bg, fg);
        } else {
            consoleScreen.putChar(column, row, asciiCode, fg, bg);
        }
        return blink;
    }

    @Override
    public void afterAddDevice(IEmulator emulator) {
        if (this.emulator != null) {
            throw new IllegalStateException("Device " + this + " is already associated with an emulator?");
        }

        this.emulator = emulator;

        if (mapVideoRamUponAddDevice) {
            mapVideoRAM(Address.wordAddress(0x8000));
        }

        if (mapFontRAMUponAddDevice) {
            mapFontRAM(Address.wordAddress(0x8180));
        }

        this.out = emulator.getOutput();

        if (refreshThread == null || !refreshThread.isAlive()) {
            refreshThread = new RefreshThread();
            refreshThread.start();
        }

        emulator.getOutput().debug("Screen attached to emulator.");
    }

    @Override
    public boolean supportsMultipleInstances() {
        return false;
    }

    @Override
    public void beforeRemoveDevice(IEmulator emulator) {
        disconnect();

        if (refreshThread != null && refreshThread.isAlive()) {
            refreshThread.terminate();
        }
        refreshThread = null;
        this.emulator = null;
        synchronized (PEER_LOCK) {
            this.peer = null;
        }
        emulator.getOutput().debug("Screen attached to emulator.");
    }

    @Override
    public DeviceDescriptor getDeviceDescriptor() {
        return DESC;
    }

    public void attach(Component uiComponent) {
        if (uiComponent == null) {
            throw new IllegalArgumentException("uiComponent must not be null");
        }
        synchronized (PEER_LOCK) {
            this.peer = uiComponent;
        }
    }

    public void detach() {
        synchronized (PEER_LOCK) {
            this.peer = null;
        }
    }

    public BufferedImage getScreenImage() {
        final ConsoleScreen screen = screen();
        return screen != null ? screen.getImage() : null;
    }

    public BufferedImage getFontImage() {
        final ConsoleScreen screen = screen();
        return screen != null ? screen.getFontImage() : null;
    }

    protected ConsoleScreen screen() {
        synchronized (PEER_LOCK) {
            if (peer == null) {
                return null;
            }
            return this.consoleScreen;
        }
    }

    @Override
    public int handleInterrupt(IEmulator emulator, ICPU cpu, IMemory memory) {
        /*
         * Interrupt behavior:
         * When a HWI is received by the LEM1802, it reads the A register and does one
         * of the following actions:
         */
        final int a = cpu.getRegisterValue(Register.A);
        switch (a) {
        /*
         * 0: MEM_MAP_SCREEN
         *    Reads the B register, and maps the video ram to DCPU-16 ram starting
         *    at address B. See below for a description of video ram.
         *    If B is 0, the screen is disconnected.
         *    When the screen goes from 0 to any other value, the the LEM1802 takes
         *    about one second to start up. Other interrupts sent during this time
         *    are still processed.
         */
        case 0:
            int b = cpu.getRegisterValue(Register.B);
            if (b == 0) {
                disconnect();
            } else {
                final Address ramStart = Address.wordAddress(b);
                final int videoRamEnd = ramStart.getWordAddressValue() + VIDEO_RAM_SIZE_IN_WORDS;

                // TODO: Behaviour if ramStart + vRAMSize > 0xffff ?
                if (videoRamEnd > 0xffff) {
                    final String msg = "Cannot map video ram to " + ramStart + " because it would " + " end at 0x"
                            + Misc.toHexString(videoRamEnd) + " which is outside the DCPU-16's address space";
                    out.error(msg);
                    throw new DeviceErrorException(msg, DefaultScreen.this);
                }

                logDebugHeadline("Mapping video RAM to " + ramStart);
                mapVideoRAM(ramStart);
            }
            break;
        /*
         * 1: MEM_MAP_FONT
         *    Reads the B register, and maps the font ram to DCPU-16 ram starting
         *    at address B. See below for a description of font ram.
         *    If B is 0, the default font is used instead.
         */
        case 1:

            int value = cpu.getRegisterValue(Register.B);
            if (value == 0) {
                synchronized (PEER_LOCK) {
                    ConsoleScreen screen = screen();
                    if (screen != null && peer != null) {
                        screen.setFontImage(DEFAULT_GLYPH_IMAGE);
                    }
                    setupDefaultFontRAM();
                }
            } else {
                logDebugHeadline("Mapping font RAM to 0x" + Misc.toHexString(value));
                mapFontRAM(Address.wordAddress(value));
            }
            break;
        /*
         * 2: MEM_MAP_PALETTE
         *    Reads the B register, and maps the palette ram to DCPU-16 ram starting
         *    at address B. See below for a description of palette ram.
         *    If B is 0, the default palette is used instead.
         */
        case 2:
            b = cpu.getRegisterValue(Register.B);
            logDebugHeadline("Mapping palette RAM to " + Misc.toHexString(b));

            if (b == 0) {
                setupDefaultPaletteRAM();
            } else {
                final Address ramStart = Address.wordAddress(b);
                // TODO: Behaviour if ramStart + vRAMSize > 0xffff ?
                mapPaletteRAM(ramStart);
            }
            break;
        /*
         * 3: SET_BORDER_COLOR
         *    Reads the B register, and sets the border color to palette index B&0xF
         */
        case 3:
            b = cpu.getRegisterValue(Register.B);
            borderPaletteIndex = b & 0x0f;
            final ConsoleScreen screen = screen();
            if (screen != null) {
                screen.setBorderColor(paletteRAM.getColor(borderPaletteIndex));
            }
            break;
        /*
         * 4: MEM_DUMP_FONT
         *    Reads the B register, and writes the default font data to DCPU-16 ram
         *    starting at address B.
         *    Halts the DCPU-16 for 256 cycles
         */
        case 4:
            int target = cpu.getRegisterValue(Register.B);

            logDebugHeadline("Dumping font RAM to 0x" + Misc.toHexString(target));

            final int len = fontRAM.getSize().getSizeInWords();
            for (int src = 0; src < len; src++) {
                memory.write(target + src, fontRAM.read(src));
            }
            return 256;
        /*
         * 5: MEM_DUMP_PALETTE
         *    Reads the B register, and writes the default palette data to DCPU-16
         *    ram starting at address B.       
         *    Halts the DCPU-16 for 16 cycles
         */
        case 5:
            Address start = Address.wordAddress(cpu.getRegisterValue(Register.B));
            logDebugHeadline("Dumping palette RAM to " + start);
            for (int words = 0; words < 16; words++) {
                value = paletteRAM.read(words);
                memory.write(start, value);
                start = start.incrementByOne(true);
            }
            return 16;
        default:
            out.warn("Clock " + this + " received unknown interrupt msg " + Misc.toHexString(a));
        }
        return 0;
    }

    protected static final class ConsoleScreen {

        // array holding image data from the generated image
        private final RawImage screen;

        private volatile Color borderColor;

        // an image containing the glyphs for our font
        private volatile RawImage glyphBitmap;

        private volatile Color awtGlyphForegroundColor;
        private volatile Color awtGlyphBackgroundColor;

        private volatile int glyphBackgroundColor;

        private final int screenWidth;
        private final int screenHeight;

        public ConsoleScreen(BufferedImage glyphBitmap, int screenWidth, int screenHeight, Color borderColor) {
            this.screenWidth = screenWidth;
            this.screenHeight = screenHeight;
            this.borderColor = borderColor;
            this.screen = new RawImage(screenWidth, screenHeight);
            setFontImage(glyphBitmap);
            renderBorder();
        }

        public synchronized void setFontImage(final BufferedImage image) {
            this.glyphBitmap = new RawImage(image.getWidth(), image.getHeight());
            this.glyphBitmap.getGraphics().drawImage(image, 0, 0, null);

            // choose darkest color as background color , lighest as foreground
            final int[] colors = this.glyphBitmap.getUniqueColors();
            int background = 0x00ffffff; // aaRRGGBB
            int foreground = 0x00000000;
            for (int col : colors) {
                if (col < background) {
                    background = col;
                }
                if (col > foreground) {
                    foreground = col;
                }
            }
            this.awtGlyphForegroundColor = new Color(foreground);
            this.glyphBackgroundColor = background;
            this.awtGlyphBackgroundColor = new Color(background);
        }

        public synchronized void defineGylph(int glyphIndex, int glyphData) {
            final int glyphRow = glyphIndex / IMG_CHARS_PER_ROW;
            final int glyphCol = glyphIndex - (glyphRow * IMG_CHARS_PER_ROW);

            final int bitmapY = GLYPH_HEIGHT * glyphRow;
            final int bitmapX = GLYPH_WIDTH * glyphCol;

            final Graphics2D g = glyphBitmap.getGraphics();
            for (int y = 0; y < GLYPH_HEIGHT; y++) {
                for (int x = 0; x < GLYPH_WIDTH; x++) {
                    Color c;
                    if (isGlyphPixelSet(x, y, glyphData)) {
                        c = awtGlyphForegroundColor;
                    } else {
                        c = awtGlyphBackgroundColor;
                    }
                    g.setColor(c);
                    g.drawLine(bitmapX + x, bitmapY + y, bitmapX + x, bitmapY + y);
                }
            }
        }

        public void debugDefineGlyph(int glyphIndex, int glyphData) {
            System.out.println("\nGlyph = " + glyphIndex + " , value = " + Misc.toHexString(glyphData));
            for (int y = 0; y < GLYPH_HEIGHT; y++) {
                for (int x = 0; x < GLYPH_WIDTH; x++) {
                    if (isGlyphPixelSet(x, y, glyphData)) {
                        System.out.print("X");
                    } else {
                        System.out.print("_");
                    }
                }
                System.out.println();
            }
        }

        public int readGylph(int glyphIndex) {
            final int glyphRow = glyphIndex / IMG_CHARS_PER_ROW;
            final int glyphCol = glyphIndex - (glyphRow * IMG_CHARS_PER_ROW);
            final int bitmapY = GLYPH_HEIGHT * glyphRow;
            final int bitmapX = GLYPH_WIDTH * glyphCol;

            final BufferedImage image = glyphBitmap.getImage();

            int result = 0;
            for (int y = 0; y < GLYPH_HEIGHT; y++) {
                for (int x = 0; x < GLYPH_WIDTH; x++) {
                    final int pixelColor = image.getRGB(bitmapX + x, bitmapY + y) & 0xffffff;

                    if (pixelColor != glyphBackgroundColor) {
                        final int bitInByte = y;
                        final int byteIndex = 3 - x;
                        final int bitsToShiftRight = (byteIndex * 8);
                        // pixel set
                        result = result | ((1 << bitInByte) << bitsToShiftRight);
                    }
                }
            }
            return result;
        }

        private boolean isGlyphPixelSet(int x, int y, int glyphBytes) {
            /*
             * word0 = 11111111 /
             *         00001001
             * word1 = 00001001 /
             *         00000000      
             *         
             *           
             * needs to be transformed to:
             *
             *           1110
             *           1000
             *           1000
             *           1110
             *           1000
             *           1000
             *           1000
             *           1000            
             */
            final int bitInByte = y;
            final int byteIndex = 3 - x;
            final int bitsToShiftRight = (byteIndex * 8);
            return ((glyphBytes >>> bitsToShiftRight) & (1 << bitInByte)) != 0;
        }

        protected void renderBorder() {
            final Graphics2D graphics = getGraphics();
            graphics.setColor(borderColor);
            graphics.fillRect(0, 0, screenWidth, BORDER_HEIGHT); // top border
            graphics.fillRect(0, 0, BORDER_WIDTH, screenHeight); // left border 
            graphics.fillRect(0, screenHeight - BORDER_HEIGHT, screenWidth, screenHeight); // bottom border
            graphics.fillRect(screenWidth - BORDER_WIDTH, 0, BORDER_WIDTH, screenHeight); // right border
        }

        public void setBorderColor(Color color) {
            if (color == null) {
                throw new IllegalArgumentException("color must not be null");
            }
            this.borderColor = color;
            renderBorder();
        }

        public void fillScreen(Color col) {
            fillRect(BORDER_WIDTH, BORDER_HEIGHT, screenWidth - (2 * BORDER_WIDTH),
                    screenHeight - (2 * BORDER_HEIGHT), col);
        }

        public void fillRect(int screenX, int screenY, int width, int height, Color color) {
            final int[] targetPixels = screen.getBackingArray();
            final int screenBitmapWidth = screen.getWidth();

            int firstTargetPixel = screenY * screenBitmapWidth + screenX;
            final int col = color.getRGB();

            for (int i = 0; i < height; i++) {
                int dst = firstTargetPixel;
                for (int j = 0; j < width; j++) {
                    targetPixels[dst++] = col;
                }
                firstTargetPixel += screenBitmapWidth;
            }
        }

        public Graphics2D getGraphics() {
            return screen.getGraphics();
        }

        public int getWidth() {
            return screenWidth;
        }

        public int getHeight() {
            return screenHeight;
        }

        public BufferedImage getImage() {
            return screen.getImage();
        }

        public BufferedImage getFontImage() {
            return glyphBitmap.getImage();
        }

        public void renderScreenDisconnectedMessage() {
            renderMessage("Screen offline", Color.BLACK, Color.WHITE);
        }

        public void renderMessage(String s, Color foreground, Color background) {

            Graphics2D graphics = getGraphics();

            Rectangle2D bounds = graphics.getFontMetrics().getStringBounds(s, graphics);

            final int x = (int) (screen.getWidth() - bounds.getWidth()) / 2;
            final int y = (int) (screen.getHeight() - bounds.getHeight()) / 2;

            graphics.setColor(background);
            graphics.fillRect(0, 0, screen.getWidth(), screen.getHeight());

            graphics.setColor(foreground);
            graphics.drawString(s, x, y);
        }

        public void putChar(int screenColumn, int screenRow, int glyphIndex, Color fg, Color bg) {
            final int glyphRow = glyphIndex / IMG_CHARS_PER_ROW;
            final int glyphColumn = glyphIndex - (glyphRow * IMG_CHARS_PER_ROW);

            final int glyphX0 = GLYPH_WIDTH * glyphColumn;
            final int glyphY0 = GLYPH_HEIGHT * glyphRow;

            final int screenX0 = BORDER_WIDTH + GLYPH_WIDTH * screenColumn;
            final int screenY0 = BORDER_HEIGHT + GLYPH_HEIGHT * screenRow;

            final int glyphBitmapWidth = glyphBitmap.getWidth();
            final int screenBitmapWidth = screen.getWidth();

            final int fgColor = fg.getRGB();
            final int bgColor = bg.getRGB();

            final int[] glyphPixels = glyphBitmap.getBackingArray();
            final int[] targetPixels = screen.getBackingArray();

            int srcRow = glyphY0 * glyphBitmapWidth + glyphX0;
            int dstRow = screenY0 * screenBitmapWidth + screenX0;

            for (int y = 0; y < GLYPH_HEIGHT; y++) {
                int src = srcRow;
                int dst = dstRow;
                for (int x = 0; x < GLYPH_WIDTH; x++) {
                    final int valueFromArray = glyphPixels[src++] & 0xffffff;

                    if (valueFromArray != glyphBackgroundColor) {
                        targetPixels[dst++] = fgColor;
                    } else {
                        targetPixels[dst++] = bgColor;
                    }
                }
                srcRow += glyphBitmapWidth;
                dstRow += screenBitmapWidth;
            }
        }

    }

    @Override
    public String toString() {
        return "'" + DESC.getDescription() + "'";
    }
}