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