# Graphics.drawImage verlangsamt sich plötzlich



## Morten (13. Mrz 2010)

Hallo, 

ich zeichne eine Bildabfolge: 


```
public void run() {
		Graphics graphics = getGraphics();
		while (true) {
			update(graphics);
		}
	}
public void update(Graphics graphics) {
		if (doubleBufferImage == null) {
			doubleBufferImage = createImage(this.getSize().width, this.getSize().height);
			doubleBufferGraphic = doubleBufferImage.getGraphics();
		}
		doubleBufferGraphic.setColor(getBackground());
		doubleBufferGraphic.fillRect(0, 0, this.getSize().width, this.getSize().height);
		doubleBufferGraphic.setColor(getForeground());
		doubleBufferGraphic.drawImage(mapImage, 0, 0, this);
                // ... 
	}
```


```
mapImage
```
 ist ein Image geladen aus einer *.png-Datei und dient eigentlich nur als Hintergrund. Bei // ... wird dann eigentlich noch mehr gezeichnet (die eigentliche Animation), aber der Fehler tritt schon beim einfachen Zeichnen des Hintergrunds auf: 
Zu Anfang dauert die Ausführung der Zeile 
	
	
	
	





```
doubleBufferGraphic.drawImage(mapImage, 0, 0, this);
```
 ca. 7-8 ms. Nach gut 2000 Bildern steigt die Dauer plötzlich auf ca. 20 ms. 

Woher kommt das? Und wie kann ich das verhindern? Und ist das so die beste Möglichkeit, ein fixes Hintergrundbild zu zeichnen? (Der Hintergrund wird ebenfalls mit Java erzeugt, um den Vorgang aber nicht jedes mal durchführen zu müssen hab ich das Bild als *.png abgespeichert. Weiß aber nicht, ob das so die beste Möglichkeit ist.)

Bin über jede Hilfe und Anregungen dankbar, 

Morten


----------



## André Uhres (14. Mrz 2010)

Die Art der Bilddatei spielt eigentlich keine Rolle mehr, sobald das Bild geladen ist. Laden würde ich es mit "ImageIO.read".  Vorsicht: das Bild nur einmal laden und möglichst nicht im Malcode. Gewöhnlich laden wir ein Bild im Konstruktor der Klasse. Außerdem würde ich Swing benutzen, anstatt AWT. Hier findest du eine kleine Einführung in das Malen mit Swing:  Malen in Swing Teil 1: der grundlegende Mechanismus - Byte-Welt Wiki
Um die Leistung zu maximieren, können wir auch noch das "Clip"-Rechteck abfragen (g.getClipBounds()):

```
@Override
    public void paintComponent(final Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        Rectangle r = g.getClipBounds();
        g2.drawImage(image, r.x, r.y, r.width + r.x, r.height + r.y,
                r.x, r.y, r.width + r.x, r.height + r.y, this);
    }
```
Der drawImage(..)-Aufruf, mit den angegebenen Parametern, zeichnet nur den Teil des Bildes neu, der sich mit dem Cliprechteck überschneidet. Man spricht in diesem Zusammenhang auch von der "intelligenten Maltechnik" (smart painting).
Wenn wir für die Animation mit "setLocation" das JLabel bewegen, wird vom Toolkit automatisch repaint() mit den Argumenten aufgerufen, die nur das Rechteck definieren, das aktualisieren muß (und nicht die keine-Argumente-Version, die die gesamte Komponente veranlasst, neu gezeichnet zu werden).


----------



## Morten (14. Mrz 2010)

Danke für die Tipps, ich denke ich habe das jetzt entsprechend umgesetzt. Ein paar Fragen hätte ich aber noch. 

Die Struktur zur Erzeugung der Animation sieht folgendermaßen aus: 


```
public void run() {
		while (true) {
			repaint();
			try {
				Thread.sleep(40);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
```


```
@Override
	protected void paintComponent(final Graphics graphics) {
		super.paintComponent(graphics);
		if (doubleBufferImage == null) {
			doubleBufferImage = createImage(this.getSize().width, this.getSize().height);
			doubleBufferGraphic = doubleBufferImage.getGraphics();
		}
		doubleBufferGraphic.setColor(getBackground());
		doubleBufferGraphic.fillRect(0, 0, this.getSize().width, this.getSize().height);
		doubleBufferGraphic.setColor(getForeground());
		Rectangle r = graphics.getClipBounds();
		doubleBufferGraphic.drawImage(mapImage, r.x, r.y, r.width + r.x, r.height + r.y, r.x, r.y, r.width + r.x,
				r.height + r.y, this);
		paintChangingStuff(doubleBufferGraphic);
		graphics.drawImage(doubleBufferImage, r.x, r.y, r.width + r.x, r.height + r.y, r.x, r.y, r.width + r.x,
				r.height + r.y, this);

		framesPerSecond(graphics);
	}
```

Die Methode repaint() braucht ca. 0-1 ms zur Ausführung. Daher habe ich die Pause von 40 ms eingebaut, scheinbar wurden zuviele Bilder pro Sekunde gezeichnet (ruckelte stark). Aber auch wenn ich diese Pause runtersetze (auf z.B. 10 ms) komme ich nicht auf mehr als ca. 20 Frames pro Sekunde (die messe ich in paintComponent). Woher kommt das? Eigentlich müssten da ja rein rechnerisch mehr Frames rauskommen (max. 1 ms pro repaint(), 10 ms Pause => mind. 90 Frames pro Sekunde).

Außerdem besteht immer noch das Phänomen, dass sich nach einiger Zeit plötzlich die Anzahl der Frames pro Sekunde merkbar verringt und zusätzlich  stockt es von Anfang ca. ein mal pro Sekunde ganz leicht. Dafür habe ich gar keine Erklärung. Kann mir da noch jemand weiterhelfen?


----------



## André Uhres (14. Mrz 2010)

Im gezeigten Code ist kein Anhaltspunkt für deine Probleme erkennbar. Vielleicht willst du ein kurzes, selbständiges und kompilierbares Beispiel machen, wo wir die genannten Probleme ohne Weiteres nachvollziehen können.

EDIT: bei schnell aufeinander folgenden repaints kann es allerdings sein, dass der RepaintManager zwei oder mehr repaint-Anfragen zusammenfasst. In dem Fall hast du natürlich entsprechend weniger "frames per second".


----------



## Morten (14. Mrz 2010)

Ok, dann wollen wir erstmal einem Phänomen auf den Grund gehen: 


```
package dk.dsb.resim.graphics;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class TestPanel extends JPanel {

	private Image doubleBufferImage;
	private Graphics2D doubleBufferGraphic;
	private static Image testImage;
	private static final long serialVersionUID = 1L;

	long firstFrame;
	int frames;
	long currentFrame;
	int fps;

	public TestPanel() {
		setSize(500, 500);
		setBackground(Color.WHITE);
		loadTestImage();
	}
	
	public void run() {
		while (true) {
			repaint();
			try {
				Thread.sleep(40);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	@Override
	protected void paintComponent(final Graphics graphics) {
		super.paintComponent(graphics);
		Graphics2D graphics2D = (Graphics2D) graphics;
		if (doubleBufferImage == null) {
			doubleBufferImage = createImage(this.getSize().width, this.getSize().height);
			doubleBufferGraphic = (Graphics2D) doubleBufferImage.getGraphics();
		}
		doubleBufferGraphic.setColor(getBackground());
		doubleBufferGraphic.fillRect(0, 0, this.getSize().width, this.getSize().height);
		doubleBufferGraphic.setColor(getForeground());
		Rectangle r = graphics.getClipBounds();
		doubleBufferGraphic.drawImage(testImage, r.x, r.y, r.width + r.x, r.height + r.y, r.x, r.y, r.width + r.x,
				r.height + r.y, this);
		graphics2D.drawImage(doubleBufferImage, r.x, r.y, r.width + r.x, r.height + r.y, r.x, r.y, r.width + r.x,
				r.height + r.y, this);
		framesPerSecond(graphics);
	}

	private void framesPerSecond(Graphics graphics) {
		frames++;
		currentFrame = System.currentTimeMillis();
		if (currentFrame > firstFrame + 1000) {
			firstFrame = currentFrame;
			fps = frames;
			frames = 0;
		}
		graphics.drawString(fps + " fps", 400, 400);
	}

	private void loadTestImage() {
		File testImageFile = new File("data/testImage.jpeg");
		try {
			testImage = ImageIO.read(testImageFile);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	public static void main(String[] args) throws ParseException, IOException {
		JFrame frame = new JFrame("TestFrame");
		TestPanel testPanel = new TestPanel();
		frame.setSize(1680, 1050);
		frame.getContentPane().add(testPanel, BorderLayout.CENTER);
		frame.setVisible(true);
		
		testPanel.run();
	}
}
```

Die benötigte Bilddatei habe ich angehängt. Entsprechend der Methode loadTestImage() abspeichern. Dann ausführen und laufen lassen... im Bild werden die Frames pro Sekunde angezeigt. Bei mir so 25 fps, nach einiger Zeit (2-3 Minuten, schwankt aber komischerweise) sackt es dann plötzlich auf 10-15 fps ab. Der Rechner arbeitet dann auch plötzlich hörbar mehr. 

Danke schon mal für die Hilfe, 
Morten


----------



## Morten (15. Mrz 2010)

Ok, Kommando zurück. Der Fehler resultiert wohl aus fehlerhaften Grafikkarten-Treibern unter Linux... ich hab gerade mal unter Windows getestet und dort tritt der Fehler nicht auf. Da muss ich wohl an anderer Stelle ansetzen (natürlich nicht das richtig Forum dafür, aber wenn mir da jemand gute Tipss geben kann, immer gerne). 

Damit hätten sich die seltsamen Performance-Fehler erledigt. Es bleibt eine grundsätzliche Frage: 

Welches ist der beste Weg, um in einer Animation einen statischen Hintergrund einzublenden. Ich habe bisher zwei Möglichkeiten: 

1. Hintergrund jedes mal per Java neu zeichnen. 

2. Hintergrund ein einziges Mal per Java zeichnen, in einer Bild-Datei abspeichern und diese Bild-Datei jedes mal zeichnen. 

Dabei ist Möglichkeit 2 deutlich langsamer wie ich mittlerweile festgestellt habe. Irgendwie klingen für mich aber beide Möglichkeiten nicht gerade berauschend, gibt es da nichts Besseres? 

Versuche, den animierten Teil mit durchsichtigem Hintergrund zu versehen und über ein fixes Panel zu legen sind bisher kläglich gescheitert, keine Ahnung ob eine solche Lösung von der Performance her gut wäre. 

Morten


----------



## André Uhres (15. Mrz 2010)

Morten hat gesagt.:


> Damit hätten sich die seltsamen Performance-Fehler erledigt. Es bleibt eine grundsätzliche Frage:
> Welches ist der beste Weg, um in einer Animation einen statischen Hintergrund einzublenden


Hier ist ein Beispiel (Quellcode im jar):


----------



## Morten (21. Mrz 2010)

Wenn ich das richtig verstehe, werden in dem Beispiel die kompletten JComponents (TimerAnimation) auf dem Hintergrund bewegt. Ich habe ein JPanel, dessen Position immer gleich bleibt, aber immer wieder neu gezeichnet wird. Es werden sehr viele kleine Objekte gezeichnet, die sich bewegen, von daher wäre es zu aufwendig für jedes dieser Objekte einen eigenen JComponent zu erschaffen. 

Meine paintComponent() des Panels auf dem die sich bewegenden Objekte gezeichnet werden: 


```
@Override
	protected void paintComponent(final Graphics graphics) {
		Graphics2D graphics2D = (Graphics2D) graphics;
		Composite newComposite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f);
		graphics2D.setComposite(newComposite);
		paintTrains(graphics2D);
	}
```

In paintTrains(graphics2D) werden einfach ein Haufen Befehle der Form graphics2D.draw(...) aufgerufen.
Problem: Die beweglichen Objekte werden zwar immer neu gezeichnet, die alten jedoch nicht gelöscht. Wie kriege ich das hin? Und ist das so überhaupt richtig? 

Und eine andere Idee. Ursprünglich sah meine paintComponent() so aus: 


```
@Override
	protected void paintComponent(final Graphics graphics) {
		Graphics2D graphics2D = (Graphics2D) graphics;
		Rectangle r = graphics2D.getClipBounds();
		if (doubleBufferImage == null) {
			doubleBufferImage = createImage(this.getSize().width, this.getSize().height);
			doubleBufferGraphic = (Graphics2D) doubleBufferImage.getGraphics();
		}
		doubleBufferGraphic.setColor(getBackground());
		doubleBufferGraphic.fillRect(0, 0, this.getSize().width, this.getSize().height);
		doubleBufferGraphic.setColor(getForeground());
		Map.drawTrackMap(doubleBufferGraphic);
		paintTrains(doubleBufferGraphic);
		graphics.drawImage(doubleBufferImage, r.x, r.y, r.width + r.x, r.height + r.y, r.x, r.y, r.width + r.x,
				r.height + r.y, this);
		calculateFramesPerSecond();
	}
```

Dabei wird eben jedes Mal der Hintergrund neu gezeichnet (Map.drawTrackMap(doubleBufferGraphic)). Ist es nicht möglich, folgendermaßen vorzugehen:
1. Beim ersten Aufruf von paintComponent den Hintergrund zeichnen und abspeichern. 
2. Bei jedem weiteren Aufruf von paintComponent graphics mit dem abgespeicherten Hintergrund überschreiben und dann den Rest drüberzeichnen

Ich hab das natürlich schon versucht, aber so ganz haut es nicht hin... meistens der gleiche Effekt wie oben, also neu Bilder werden gezeichnet, alte sind aber nicht gelöscht. 

Wäre über weitere Hilfe sehr froh


----------



## André Uhres (21. Mrz 2010)

Morten hat gesagt.:


> wäre es zu aufwendig für jedes dieser Objekte einen eigenen JComponent zu erschaffen.


Versuch's einfach mal. Es ist ja einfach umzusetzen und die Anzahl der Objekte wird ja wohl kaum in die hunderttausende gehen


----------



## Morten (21. Mrz 2010)

Das muss ich mir dann mal genauer anschauen, ganz so einfach ist es definitiv nicht. Die beweglichen Objekte müssen dynamisch gedreht, verkleinert, vergrößert und inhaltlich anders gefüllt werden können. Außerdem gibt es Verbindungslinien zwischen manchen Objekten. Spätestens die kann ich ja kaum noch mit JComponents umsetzen. 

Wie siehts denn mit meiner anderen Idee aus? Wo ist da der Denkfehler?


----------



## Ein Keks (21. Mrz 2010)

naja gibt schon genug gründe nicht von JComponent zu erben.... so 83 Stück^^



			
				Morten hat gesagt.:
			
		

> Die beweglichen Objekte werden zwar immer neu gezeichnet, die alten jedoch nicht gelöscht. Wie kriege ich das hin?


Einfach den Hintergrund übermalen entweder mit super.paintComponent(graphics) -> einfarbig getBackground oder mit drawImage(...) und halt deinen Hintergrundbild (BufferedImage sollte dabei zumindest unter Windows schnell genug sein da dort eine Kopie im VRAM erstellt wird; von Image soweit ich weiß nicht)



			
				Morten hat gesagt.:
			
		

> Und eine andere Idee. Ursprünglich sah meine paintComponent() so aus:


ähm Swing hat bereits DoubleBuffering du kannst also stinknormal das übergebene Graphics Object zum zeichnen verwenden, das (Buffered)Image ist nicht notwedig

falls wirklich akute Performance Probleme sind endweder auf Active Rendering umsteigen (Stichwort BufferStrategie), die Details verringern (ok nicht die beste Lösung) oder OpenGL verwenden (viel komplizierter...)

ach und niemals den EDT schlafenlegen^^


----------



## Morten (21. Mrz 2010)

Danke für die ganzen Anregungen. Zwei Fragen dazu: 

1. Ich arbeite unter Linux. Gilt da das gleiche für BufferedImage? 

2. Und was genau meinst du mit "EDT nicht schlafen legen"? Meinst du Thread.sleep in der run()? Eher mit Thread.yield() arbeiten?


----------



## Quaxli (22. Mrz 2010)

Morten hat gesagt.:


> 2. Und was genau meinst du mit "EDT nicht schlafen legen"? Meinst du Thread.sleep in der run()? Eher mit Thread.yield() arbeiten?



Dieses Konstrukt mit Panel.rum() ist nicht toll, das solltest Du in einen eingen Thread auslagern.
Klick mal auf den Link in meiner Signatur, da findest Du ein Tutorial zur Spieleprogrammierung, wo Du das abgucken kannst.


----------



## André Uhres (22. Mrz 2010)

Ein Keks hat gesagt.:


> naja gibt schon genug gründe nicht von JComponent zu erben.... so 83 Stück


Die "Gründe" für die Untauglichkeit irgendeiner Klasse auf die Anzahl der im konkreten Fall überflüssigen Attribute zu reduzieren, ist reichlich grotesk. Das beweist nur, dass jemand sich nicht mit stichhaltigen Gründen auseinander setzen kann und/oder will.


----------



## Quaxli (22. Mrz 2010)

Deine Lösung, die Objekte mit add() dem Hintergrundbild zuzuordnen gefällt mir wirklich gut. Sehr komfortabel. 
Ich wäre aber durchaus geneigt dem Keks zuzustimmen und würde es nicht pauschal als grotesk verurteilen, da nichtsdestotrotz etliche unnötige Informationen "mitgeschleppt" werden.
Es wäre schön, wenn Du uns darlegst, warum es aus Deiner Sicht vorteilhafter ist von JComponent zu erben. Ich für meinen Teil lerne ja gerne noch was dazu, wenn ich überzeugt werde.


----------



## André Uhres (22. Mrz 2010)

Standardklassen "schleppen" doch in den allermeisten konkreten Awendungsfällen unnötige Informationen mit, obwohl "schleppen" ja eigentlich nicht stimmt, sie "ruhen" vielmehr im Heap und fallen kaum zur Last. Als wesentlicher Bestandteil des Swing-Malsystems profitiert JComponent automatisch von den leistungsstarken Swing-Malmechanismen. Ein weiteres Argument hast du ja schon selbst genannt: die Lösung ist sehr komfortabel.


----------

