Java tutorial
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() + "'"; } }