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