Anuncio

miércoles, 3 de octubre de 2012

Java 2D: Hardware Accelerating - Part 1 - Volatile Images

http://www.javalobby.org/java/forums/m91823967.html


Java 1.4 introduced support for hardware acceleration in Java 2D functionality. Hardware acceleration is great to have, no question - but effectively using the java.awt.image.VolatileImage class is at least bit more complicated than using a traditional 'buffered image' format. The details of working with this low-level 'hardware acceleration' is only really important if you are doing complex Java 2D rendering on your own. If you are only working with pre-built widgets/components in Swing for instance, most of this tip isn't really relevant. This tip is very helpful, however, for people working with 2D game programming in Java, or perhaps someone working on a graphics-intensive GUI such as charts and diagrams.

I'm going to assume that readers of this tip are familiar with at least the concept of double-buffering - if you aren't, please read here . In short, double buffering is a technique of deferring the rendering process to an 'offscreen' buffer, and then quickly copying that buffer to the onscreen device, which gives the illusion of higher performance (by making the onscreen rendering smoother). To simply produce a standard double buffer (not hardware accelerated), your code may look something like this:

// other classes may be extended - canvas is a common selection however.
public class CustomGUI extends Canvas {
 
 private Image offscreenImage;
 private Graphics offscreenGraphics;
 private Dimension offscreenDimension;
 
 // ...
 public void paint(Graphics g) {
  Dimension currentSize = getSize();
  if(offscreenImage == null || !currentSize.equals(offscreenDimension)) {
   // call the 'java.awt.Component.createImage(...)' method to get an
   // image
   offscreenImage = createImage(currentSize.width, currentSize.height);
   offscreenGraphics = offscreenImage.getGraphics();
   offscreenDimension = currentSize;   
  }
  
  // rendering code here (use offscreenGraphics 'Graphics' object)
  // this algorithm assumes the background will be re-filled because it 
  // reuses the image object (otherwise artifacts will 
  // remain from previous renderings)
  offscreenGraphics.setColor(Color.WHITE);
  offscreenGraphics.fillRect(0, 0, offscreenDimension.width, offscreenDimension.height);
  offscreenGraphics.setColor(Color.BLACK);
  offscreenGraphics.drawLine(0, 0, 10, 10); // arbitrary rendering logic
  
  // paint back buffer to main graphics
  g.drawImage(offscreenImage, 0, 0, this);
 }
 
 public void update(Graphics g) {
  paint(g);
 }
}


This sort of 'double-buffer' situation is the perfect case for hardware acceleration. The question becomes, however, how do you utilize classes such as the java.awt.image.VolatileImage class to accelerate this code? Well, adapted from the Javadocs of VolatileImage and the contents of this Java whitepaper on volatile images (which I highly recommend you read for a more thorough understanding) - here is a new version of the same class above that uses volatile images for rendering - don't worry, I will take a stab at explaining the 'wierdness' after:

// other classes may be extended - canvas is a common selection however.
public class CustomGUI extends Canvas {
 
 private VolatileImage volatileImg;
 
 // ...
 
 public void paint(Graphics g) {
  // create the hardware accelerated image.
  createBackBuffer();
 
  // Main rendering loop. Volatile images may lose their contents. 
  // This loop will continually render to (and produce if neccessary) volatile images
  // until the rendering was completed successfully.
  do {
   
   // Validate the volatile image for the graphics configuration of this 
   // component. If the volatile image doesn't apply for this graphics configuration 
   // (in other words, the hardware acceleration doesn't apply for the new device)
   // then we need to re-create it.
   GraphicsConfiguration gc = this.getGraphicsConfiguration();
   int valCode = volatileImg.validate(gc);
   
   // This means the device doesn't match up to this hardware accelerated image.
   if(valCode==VolatileImage.IMAGE_INCOMPATIBLE){
    createBackBuffer(); // recreate the hardware accelerated image.
   }
  
   Graphics offscreenGraphics = volatileImg.getGraphics();   
   offscreenGraphics.setColor(Color.WHITE);
   offscreenGraphics.fillRect(0, 0, getWidth(), getHeight());
   offscreenGraphics.setColor(Color.BLACK);
   offscreenGraphics.drawLine(0, 0, 10, 10); // arbitrary rendering logic
   
   // paint back buffer to main graphics
   g.drawImage(volatileImg, 0, 0, this);
   // Test if content is lost   
  } while(volatileImg.contentsLost());
 }
 
 // This method produces a new volatile image.
 private void createBackBuffer() {
   GraphicsConfiguration gc = getGraphicsConfiguration();
   volatileImg = gc.createCompatibleVolatileImage(getWidth(), getHeight());
 }
 
 public void update(Graphics g) {
  paint(g);
 }
}


Whew - things are getting complicated. Long story short, however, volatile images are just as their name suggests - temperamental. Volatile images can be considered to represent an image in non-standard memory (e.g. video card memory). This makes them reliant on an underlying hardware feature (the video card) that may or may not be the active 'weapon of choice' at all times, and as such the image may or may not apply (it represents memory in a video card not being used). This is specifically why the VolatileImage.IMAGE_INCOMPATIBLE check is performed. We ask the volatile image to validate itself against the graphics configuration for the current component (remember the graphics configuration represents, at its core, the destination 'device' of our component), and if it doesn't match, we reproduce the volatile image for the current graphics configuration (current video card for instance).

In addition to the foreign 'validate' check, we also have that nasty (and rare) use of a do-while loop. There are two reasons that all of this logic is wrapped in the do-while loop. First, after we have begun rendering to the volatile image, the image may once again become incompatible. In this case, the contentsLost() method will return true to let us know that something went wrong. Second, certain platforms (I won't name names) allow for a case where video memory may be 'lost' (*poof*! where'd it go?), and as such requires that you re-write any data to that block of memory. In these cases the 'valCode' returned from the VolatileImage.validate(...) method will actually be VolatileImage.IMAGE_RESTORED (there are three results - IMAGE_OK, IMAGE_RESTORED, IMAGE_INCOMPATIBLE). The way the code is structured above, however, this return-type doesn't need to be handled explicitly. In general, VolatileImage.contentsLost() returns true if the contents of the image have been lost for one of these reasons since the last call to VolatileImage.validate(...) was performed. Because of that distinction I just emphasized, our code works rather simply by performing two validates each loop: the call to VolatileImage.validate() before our rendering logic (to ensure our image is in good shape to use), and then a call to VolatileImage.contentsLost() after our rendering logic to determine if the image stayed happy since we last called validate (which implies all of our rendering logic since we called validate worked as expected).

Stay tuned, as tomorrow I will show how to hide some of this code craziness by using the java.awt.image.BufferStrategy class.

No hay comentarios:

Publicar un comentario