Java 2D mit Grafikbeschleunigung unter Linux

Martin Kompf

Java 2D

Wochenlang habe ich mich über die schlechte Performance einer Java 2D Anwendung unter Linux geärgert. Dabei ist die Lösung so einfach - man muss nur darauf kommen.

sun.java2d.opengl=true

Das Beispielprogramm setzt massiv Java 2D zum Zeichnen von Images und anderer grafischer Objekte, wie Linien und Punkte, ein. Dabei zeigte sich, dass die Performance unter Linux, konkret Ubuntu 12.10, deutlich schlechter ist als unter Windows 7. Diverse Profiling Sitzungen erbrachten nur eine marginale Verbesserung. Weitere Recherchen, insbesondere die Konsultation der Seite System Properties for Java 2D™ Technology, führten dann zur Lösung.

Da Java unter Linux standardmäßig keine 2D Grafikbeschleunigung benutzt, muss man sie explizit durch das Setzen der Systemproperty sun.java2d.opengl auf den Wert true einschalten! Am besten erfolgt dies auf der Kommandozeile beim Start des Programms:

java -Dsun.java2d.opengl=true <weitere Parameter>

Damit erzielt das Programm auf einer Ubuntu Linux Maschine mit Intel Grafikhardware eine ähnlich gute Performance wie auf einem vergleichbaren Windows PC. Das Setzen von sun.java2d.opengl=true unter Windows ist dagegen nicht zu empfehlen, auf dem Testsystem führte dies zu Darstellungsfehlern. Siehe dazu auch Support for Hardware-Accelerated Rendering Using OpenGL in der Java SE Dokumentation.

Anwendungsmuster von Java 2D

Das Beispielprogramm verwendet eine Reihe typischer Patterns zum Zeichnen von Java 2D Objekten. Ausgangspunkt ist dabei immer eine java.awt.Component (oder eine davon abgeleitete Klasse, wie JPanel), deren paint() Methode man überlädt.

Laden und Zeichnen eines Images

Die folgende Komponente zeigt ein Image, zum Beispiel ein Foto, mittels Java 2D an. Die paint() Methode testet dabei zuerst, ob das Image schon im Speicher geladen ist. Falls ja, verwendet sie den übergebenen Graphics2D Kontext zum Zeichnen des Images mittels drawImage. Die vorangestellten Berechnungen kümmern sich um die korrekte Skalierung und Zentrierung des Bildes.

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;

import javax.imageio.ImageIO;

/**
 * Panel to display a photo from an URL.
 */
public class ImagePanel extends Component {

  private URL url;
  private BufferedImage img;

  /**
   * Construct a new photo panel.
   * @param photoUrl The URL of the photo to display.
   */
  public ImagePanel(URL photoUrl) {
    url = photoUrl;
  }

  @Override
  public void paint(Graphics g) {
    super.paint(g);
    if (img == null) {
      loadImage();
    } else {
      drawImage((Graphics2D) g);
    }
  }

  private void drawImage(Graphics2D g) {
    // scale 
    final int imw = img.getWidth(null);
    final int imh = img.getHeight(null);
    final int pw = getWidth();
    final int ph = getHeight();

    int w = pw;
    int h = w * imh / imw;
    if (h > ph) {
      h = ph;
      w = h * imw / imh;
    }

    // center
    int x = (pw - w) / 2;
    int y = (ph - h) / 2;

    // draw
    g.drawImage(img, x, y, w, h, null);
  }

Falls das Image noch nicht im Hauptspeicher steht, muss man es mittels ImageIO.read von der angegebenen URL laden. Das sollte man jedoch tunlichst nicht innerhalb der paint() Methode machen, da diese im Event Dispatch Thread der Anwendung läuft. Aufwendige Operationen wie Netzwerk- oder Dateisystemzugriffe sollten immer in einem eigenen Background-Thread laufen, da sonst ein Blockieren der Operation das «Einfrieren» der Benutzeroberfläche zur Folge hätte. Zu dessen Erzeugung und Kontrolle verwendet das Beispiel einen SwingWorker:

  private void loadImage() {
    // load image in separate thread!
    SwingWorker<BufferedImage, Void> worker = new SwingWorker<BufferedImage, Void>() {

      @Override
      protected BufferedImage doInBackground() throws Exception {
        return ImageIO.read(url);
      }
      
      @Override
      protected void done() {
        try {
          img = get();
          // ask the component to call paint()
          repaint();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    };
    
    worker.execute();
  }
}

Zeichnen von grafischen Objekten

Java 2D stellt Methoden zum Zeichnen einfacher grafischer Objekte, wie Linien, Ellipsen und Text, bereit. Ist die Grafikbeschleunigung verfügbar und eingeschaltet, dann delegiert Java 2D diese Methoden direkt an die Grafikhardware. Im Beispielprogramm waren es letztendlich eine Vielzahl dieser grafischen Objekte, die das Programm auf Systemen ohne Grafikbeschleunigung ausgebremst haben.

Das Zeichnen erfolgt wiederum innerhalb der paint() Methode unter Zuhilfenahme des übergebenen Graphics2D Kontext. Der Kontext ist statusbehaftet. Insbesondere merkt er sich die mittels setColor(), setStroke() oder setFont() gesetzten Eigenschaften hinsichtlich Farbe, Liniendicke, Font und so weiter. Das folgende Beispiel veranschaulicht das Zeichnen von Linien, Ellipsen, Text und Rechtecken:

import java.awt.*;
import java.awt.geom.*;
import java.util.List;

/**
 * Component to show some basic Java 2D drawing operations.
 */
public class DiagramPanel extends Component {
  
  private List<Point2D> pointList;
  private String text;

  public DiagramPanel(List<Point2D> pointList, String text) {
    this.pointList = pointList;
    this.text =  text;
  }

  @Override
  public void paint(Graphics g) {
    super.paint(g);
    drawFilledRectangle((Graphics2D) g);
    drawPath((Graphics2D) g);
    drawText((Graphics2D) g);
  }
  
  /**
   * Draw a filled rectangle.
   */
  private void drawFilledRectangle(Graphics2D g) {
    final int width = getWidth();
    final int height = getHeight();
    final Paint fillColor = new GradientPaint(
        new Point(0, 0), Color.BLUE, 
        new Point(width, height), Color.LIGHT_GRAY);
    g.setPaint(fillColor);
    g.fillRect(0, 0, width, height);
  }

  /**
   * Draw line with dots at each point.
   */
  private void drawPath(Graphics2D g) {
    final Color pathColor = new Color(128, 128, 128, 128);
    g.setPaint(pathColor);
    g.setStroke(new BasicStroke(2));

    Point2D p0 = null;
    for (Point2D p  : pointList) {
      g.draw(new Ellipse2D.Double(p.getX() - 2, p.getY() - 2, 4, 4));
      if (p0 != null) {
        g.draw(new Line2D.Double(p0, p));
      }
      p0 = p;
    }
  }
  
  /**
   * Draw centered text.
   */
  private void drawText(Graphics2D g) {
    final Color textColor = Color.RED;
    final Font textFont = new Font("SansSerif", Font.BOLD, 24);
    
    // measure text
    final FontMetrics metrics = g.getFontMetrics(textFont);
    final int x = (getWidth() - metrics.stringWidth(text)) / 2;
    final int y = (getHeight() + metrics.getHeight()) / 2;
    
    g.setColor(textColor);
    g.setFont(textFont);
    g.drawString(text, x, y);
  }
}