Sunday, 3 June 2012

Bytepusher for Java

In this post I discuss a java implementation of bytepusher.  Java is cross platform, so will work on windows, Linux and Macs.

I won't go through the Bytepusher specs as this was discussed in a previous post, however I will re-iterate the architecture and apply it to a java swing application.

Aim

Create a Bytepusher implementation in Java.

Tools

JDK 1.6
Gradle
Eclipse IDE
Git

Architecture


I will reuse the architecture from my c# solution, refined to take into account of how swing works:



BytePusherVM


public class BytePusherVM {



  private char[] mem = new char[0xFFFFFF];

  private BytePusherIODriver ioDriver;


  public BytePusherVM(BytePusherIODriver ioDriver) {
    this.ioDriver = ioDriver;
  }

  /**
   * Load ROM into memory
   * @param rom
   */
  public void load(InputStream rom) throws IOException {
    mem = new char[0xFFFFFF];
    int pc = 0;
    int i = 0;
    while ((i = rom.read()) != -1) {
      mem[pc++] = (char)i;
    }
  }

  /**
  * CPU loop, to be called every 60th of a second
  */
  public void run() {
    // run 65536 instructions
    short s = ioDriver.getKeyPress();
    mem[0] = (char) ((s & 0xFF00) >> 8);
    mem[1] = (char) (s & 0xFF);
    int i = 0x10000;
    int pc = getVal(2, 3);
    while (i-- != 0) {
      mem[getVal(pc + 3, 3)] = mem[getVal(pc, 3)];
      pc = getVal(pc + 6, 3);
    }
    ioDriver.renderAudioFrame(copy(getVal(6, 2) << 8, 256));
    ioDriver.renderDisplayFrame(copy(getVal(5, 1) << 16, 256 * 256));
  }

  private int getVal(int pc, int length) {
    int v = 0;
    for (int i = 0; i < length; i++) {
      v = (v << 8) + mem[pc++];
    }
    return v;
  }

  private char[] copy(int start, int length) {
    return Arrays.copyOfRange(mem, start, start + length);
  }
}

As java does not do unsigned byte, I've used a char which is effectively an unsigned byte.  In a deviation from the c# solution, I've decoupled the VM from the underlying file system by providing the load() method with an  InputStream.

Hardware Abstraction

public interface BytePusherIODriver {
  /**
   * Get the current pressed key (0-9 A-F)
   */
  short getKeyPress();

  /**
   * Render 256 bytes of audio 
  */
  void renderAudioFrame(char[] data);

  /**
   * Render 256*256 pixels.  
  */
  void renderDisplayFrame(char[] data);
}

Java Bytepusher Driver

public class BytePusherIODriverImpl extends KeyAdapter implements BytePusherIODriver {
  private SourceDataLine line;
  private int keyPress;
  private BufferedImage image;

  /**
   * Initializes the audio system
   */
  public BytePusherIODriverImpl() {
    try {
      AudioFormat f = new AudioFormat(15360, 8, 1, true, false );
      line = AudioSystem.getSourceDataLine(f);
      line.open();
      line.start();
    }
    catch( LineUnavailableException e ) {
      throw new RuntimeException( e );
    }
  }

  /**
   * Get the current pressed key (0-9 A-F
   */
  public short getKeyPress() {
    short k = 0;
    switch(  keyPress ) {
      case KeyEvent.VK_0: k+=1; break;
      case KeyEvent.VK_1: k+=2; break;
      case KeyEvent.VK_2: k+=4; break;
      case KeyEvent.VK_3: k+=8; break;
      case KeyEvent.VK_4: k+=16; break;
      case KeyEvent.VK_5: k+=32; break;
      case KeyEvent.VK_6: k+=64; break;
      case KeyEvent.VK_7: k += 128; break;
      case KeyEvent.VK_8: k += 256; break;
      case KeyEvent.VK_9: k += 512; break;
      case KeyEvent.VK_A: k += 1024; break;
      case KeyEvent.VK_B: k += 2048; break;
      case KeyEvent.VK_C: k += 4096; break;
      case KeyEvent.VK_D: k += 8192; break;
      case KeyEvent.VK_E: k += 16384; break;
      case KeyEvent.VK_F: k += 32768; break;
    }
    return k;
  }

  /**
   * Render 256 bytes of audio 
   */
  public void renderAudioFrame(char[] data) {
    // convert from char [] to byte []
    byte [] b = new byte[256];
    for ( int i=0; i < 256; i++ ) {
      b[i] = (byte) data[i];
    }
    // send buffer to audio device
    line.write( b, 0, 256);
  }

  /**
   * Render 256*256 pixels.  
   */
  public void renderDisplayFrame(char[] data) {
    image = new BufferedImage( 256, 256, BufferedImage.TYPE_INT_RGB );
    int z = 0;
    for ( int y=0; y < 256; y++ ) {
      for ( int x=0; x < 256; x++ ) 
      {
        int c = data[z++];
        if ( c < 216 ) {
          int blue = c % 6;
          int green = ((c - blue) / 6) % 6;
          int red = ((c - blue - (6 * green)) / 36) % 6;
          image.setRGB(x, y, ( red *0x33 << 16 ) + (green * 0x33 <<8) + (blue * 0x33 ) );
        }
      }
    }
  }    

  /**
   * Invoked when a key has been pressed.
   * See the class description for {@link KeyEvent} for a definition of
   * a key pressed event.
   */
  public void keyPressed(KeyEvent e) {
    keyPress = e.getKeyCode();
  }

  /**
    * Detect the key being released so that we can clear 
    * the key press. 
  */
  public void keyReleased(KeyEvent e) { 
    keyPress=0;
  }    

  /**
   * Get the image
   * @return the bufferedImage
   */
  public BufferedImage getDisplayImage() {
    return image;
  }
}

Audio System - Very similar to the XNA audio model where you submit audio buffers.  In the case of java you write to an AudioDataLine.  It also natively supports 8 bit audio samples which cuts down on a conversion algorithm.

Graphics rendering - Much simpler to use than the c# .net libraries.  No messing about with unsafe arrays etc.  Very simplistically, you create a BufferedImage then set each pixel.

Keyboard input - You need to register listeners for KeyEvents.  The issue here is that these keyevent's can occur outside of the 60th of a second run cycle.  The solution is to just store the keyevent, then respond to it within the getKeyPress() call.  In this way everything is synchronized and happy.


Swing Application

public class BytePusher extends JFrame {

  private BytePusherVM vm;
  private BytePusherIODriverImpl driver;
  private Canvas c;
  private FrameTask frameTask;

  /**
   * Entry point
   * @param args
   */
  public static void main( String [] args ) {
    BytePusher b = new BytePusher();
    b.setVisible(true);
  }

  /**
   * Constructor
   */
  public BytePusher() {
    setUpWindow();
    setUpVm();
  }

  /**
   * Create a JFrame with a single canvas within.  
   * Setup a key listener which will record the keypress.  
   * This will subsequently be handled by the FrameTask
   * which is setup to run every 60th of a second.
   */
  private void setUpWindow() {
    // create window
    setTitle( "Bytepusher for Java" );
    setLayout( new GridBagLayout() );
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    getContentPane().setPreferredSize(new Dimension(256*3, 256*3) );
    c = new Canvas();
    getContentPane().add(c);
    // canvas must be mon focusable otherwise key listeners 
    // don't work
  c.setFocusable(false); 
    c.setSize(new Dimension(256*3, 256*3) );
    pack();
    c.createBufferStrategy(2);

    // when window is resized, also resize canvas
  this.addComponentListener( new ComponentAdapter() {
      public void componentResized(ComponentEvent e) {
        c.setSize( getWidth()-15, getHeight()-38 );
      }
    });
  }

  /**
   * Load ROM into VM.  
   * @param rom
   */
  private void loadRom( String rom ) {
    try {
      FileInputStream fis = new FileInputStream( rom );
      vm.load( fis );
      fis.close();
    }
    catch( IOException e ) {
      throw new RuntimeException( e );
    }
  }

  private void setUpVm() {
    // set up bytepusher vm
    driver = new BytePusherIODriverImpl();
    vm = new BytePusherVM( driver );;

    // register key listened which will be used by the driver
    this.addKeyListener( driver );
    loadRom( "roms/audio.BytePusher" );

    // startup vm
  frameTask = new FrameTask();
    new Timer().schedule(frameTask, 0, 1000/60);
  }

  /**
   * TimerTask which is setup to fire every 60th of a second
  */
  private class FrameTask extends TimerTask {

    /**
     * Runs the VM every 60th of a second and renders graphics
    */
    public void run() {
      vm.run();
      // render vm image to screen
      Graphics g = c.getBufferStrategy().getDrawGraphics();
      g.drawImage(driver.getDisplayImage(), 0, 0, c.getWidth(), c.getHeight(), null);
      // flip buffer 
      c.getBufferStrategy().show();
      g.dispose();
    }
  }    
}


1  Resizing window
The JFrame can be resized.  To detect this, we add a listener which triggers when the canvas sitting inside is resized. In order for the full bytpusher display to be visible, the canvas needs to be smaller than the JFrame. To be exact. the width needs to be 15 pixels smaller and the height 38 pixels smaller.  

2  KeyEvent
If you click on the canvas, then seemingly the JFrame stops responding to KeyEvent's.  To get around this, the canvas needs to be set to be non focusable.

 Scheduled Task
This sets up a scheduled task which runs every 60th of a second.

Downloads


Binary: bytepusher4j.zip
Github: Source code
Git repository: git@github.com:coder36/bytepusher4j.git

Linux Install instructions:
         unzip bytepusher4j.zip   
    cd bytepusher
    bytepusher.sh

Windows Install instructions:
         Unzip bytepusher4j.zip   
         double click bytepusher.bat




Friday, 1 June 2012

The problem of checked exceptions in java


One thing that I really don't like about java is the use and abuse of checked exceptions.  They:

1) Pollute the code.  

With checked exceptions you either need to have try catch blocks all over the place or you need declare that the method throws a checked exception ie.

public void myMethod() {
  try {
     FileInputStream fis = new FileInputStream( "c:/file" );
  }

  catch( IOException e ) {
    //do something
  }
}

or

public void myMethod() throws IOException {
    FileInputStream fis = new FileInputStream( "c:/file" );

}

The problem with the second example is that in the calling class you then need to deal with the exception.

2) Lead to naive developers thinking they know best 


public void myMethod() throws CorporateException {
  try {
     FileInputStream fis = new FileInputStream( "c:/file" );

  }
  catch( IOException e ) {
    throw new CorporateException( "An error has occured: " + e.getMessage() );
  }
}



This is not particularly helpfull as the actual real reason of the failure (ie. the IOException) has been swallowed.

The number of times I've seen this, with the only thing to go on a message saying "CorporateException : message: java.lang.NullPointeException".  It's useless to me as I've got no line number to go on as the original NullPointerException has been swallowed.

3) Nested stack traces

public void myTopLevelMethod() throws CorporateException {
  try {
     myMethod1();
  myMethod2();
      FileInputStream fis = new FileInputStream( "c:/file" );
  }
  catch(  Exception  e ) {
     throw new CorporateException( "An error orccured in the top level method", e );
  }
}



This could result in a very long stack trace full of repeated exceptions!  The number of times I've seen this.

4) Transactions not rolling back

When using EJB's or Spring to manage transactions, the key is to ensure that a RuntimeException crosses the transaction boundary if the rollback is to be triggered.  If the naive developer declares that his methods throws a checked exception, then the transaction management will not work as expected.

------------------


What can we do?


  1. Do not use checked exceptions.
  2. Do not create a CorporateException class
  3. When a method does trigger a checked exception convert it immediately into a RuntimeException
  4. Policy that no methods are allowed to throw checked exceptions.
  5. If you do need to add context do this in a dedicated single layer which all code will fall through ie. a Servlet in a web app. or via a hook in whatever framework you are using eg. spring batch @OnWriteError. This should worked out as part of the software architecture and not left to each and every developer.



Gradle Build automation


Gradle

Gradle is a next generation build automation tool.  "Gradle combines the power and flexibility of Ant with the dependency management and conventions of Maven into a more effective way to build. Powered by a Groovy DSL and packed with innovation, Gradle provides a declarative way to describe all kinds of builds through sensible defaults. Gradle is quickly becoming the build system of choice for many open source projects, leading edge enterprises and legacy automation challenges."

I've used gradle to automate the build of bytepusher4j. I haven't really used it before in a commercial setting (big companies prefer ant or maven), but I can really see how its simplicity is making it more popular.  The entire gradle config for bytepusher4j consists of:

apply plugin: 'java'
apply plugin: 'eclipse'


task zip(type: Zip) {
  from(jar.outputs.files) {
    into('bytepusher/lib')
  }
  from('roms') {
    into('bytepusher/roms')
  }
  from('src/bin') {
    into('bytepusher')
    fileMode(0755)
  }
}

This simple configuration allows a java project to be compiled into a jar file, and then packaged up into a zip file.  It also does dependency management in much the same way as maven (although I don'y need it for bytepusher4j).

Demo

This walkthroguh assumes that you already have  JDK6 ,  GIT and  Eclipse IDE installed.  It also assumes a windows OS, but could equally be applied to linux.

Setup Gradle:
1) Download  gradle and extract zip to c:/tools.  This will create a folder called c:/tools/gradle-1.0-nnn-n.

2) Add a new system environment variable GRADLE_HOME=c:/tools/gradle-1.0-nnn-n

3) Update the PATH environment variable adding %GRADLE_HOME%/bin; to the start

Download git source
4) mkdir c:/src
5) cd c:/src
6) git clone git@github.com:coder36/bytepusher4j.git
7) cd bytepusher4j

Build the code
8) gradle zip
9) gradle eclipse


10) Start Eclipse
11) File ->Import->Existing Projects into Workspace -> Browse
12) Open up C:/src/bytepusherj and select bytepusher4j
13) Finish
17) Select BytePusher.java then hit the green play button to start up the application.