Java Effiziente 2D Grafikdarstellung

[Moepi]1

Lt. Commander
Registriert
Jan. 2002
Beiträge
1.233
Hallo,
Langsam aber sicher bin ich am Ende mit meinem Latein. Vielleicht hat ja hier jemand ne Idee...

ToDo:
Der Bildschirminhalt, by default ca. 320x240 Pixel, wird in einem Byte Array 50mal pro Sekunde aktualisiert. Aus diesem Byte Array soll möglichst effizient ein Bild aus nur 16 Farben erzeugt werden. Dieses Bild soll auf ein JLabel gezeichnet und bei Bedarf mit diesem skaliert werden.

Bislang:

Ich erzeuge ein ColorModel:
Code:
private static ColorModel generateColorModel(int type) {
        byte[] r = new byte[16];
        byte[] g = new byte[16];
        byte[] b = new byte[16];
        ...
        return new IndexColorModel(4, 16, r, g, b, Transparency.OPAQUE);
}

Daraus erzeuge ich ein BufferedImage:
Code:
buffer = new DataBufferByte(content, __screen_width * __screen_height);
raster = Raster.createPackedRaster(buffer, __screen_width, __screen_height, 8, new Point(0,0));
img =  new BufferedImage(colorModel, raster, false, null);

Dieses wird an eine Instanz einer anderen Klasse übergeben, die es dann zeichnet:
Code:
public class Display extends JLabel {
	private BufferedImage img;
	private int __frame_width;
	private int __frame_height;
	
	public void paint (Graphics g) {
		Graphics2D g2d = (Graphics2D)g;
		
		__frame_width = this.getWidth();
		__frame_height = this.getHeight();
	
  	        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
		
		g2d.drawImage(img, 0, 0, __frame_width, __frame_height, Color.black, null);
	}
}

Das funktioniert soweit auch. Leider ist aber die Skalierung höllisch ineffizient. An ein Hochskalieren auf übliche Bildschirmauflösungen ist nicht zu denken. Ich hatte es zuvor auch schon ohne BufferedImages versucht. Hier schien mir zwar das Skalieren an sich effizienter, aber leider nach wie vor zu langsam.
Die Interpolationsmethode ist ganz nett, hat aber keinen Einfluss auf die Performance (also bilinear is genauso lahm wie bikubisch).

Jetzt stellt sich mir langsam die Frage, ob und wie ich Java noch dazu bringen könnte, den Vorgang etwas zu beschleunigen. Muss doch möglich sein, ein Bild aus 16 Farben 50mal in der Sekunde neu zu zeichnen und ggf. zu skalieren...
 
Ich würde mal tippen, dass das Problem die Klasse JLabel ist. Ich glaube nicht, dass diese für derartige Zwecke konzipiert wurde.

Mein Tipp:
schreib dir eine eigene Klasse und benutze ein java.awt.Graphics Objekt aus der paintComponent()-Methode des JPanels, mit der du dann dein Bild direkt zeichnest (drawImage(...), im Prinzip so wie oben).
 
Zuletzt bearbeitet:
Erzeuge ein BufferedImage mit
Code:
GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().createCompatibleImage(height, width, Transparency.OPAQUE);
Damit wird sichergestellt, dass es von der Grafikkarte beschleunigt werden kann.
Des Weiteren musst du dieses BufferedImage dann wiederverwenden (sehe ich jetzt bei dir nicht weil der Code ja unvollständig ist) und nicht laufend neu erzeugen. Dazu musst du natürlich komplett anders zeichnen: Eben ein createGraphics() und dann darin malen oder notfalls mit getWritableRaster() das Raster holen und zeichnen.
Außerdem könntest du mal als RenderingHint Antialiasing ausschalten und im Konstruktor folgendes probieren:
Code:
this.setDoubleBuffered(true);  //evtl. auch mal false probieren; ist systemabhängig
this.setOpaque(true);
Zudem sollte der g2d immer mit g2d.dispose() entsorgt werden.
Eine weitere Ineffizienz kann darin liegen wie du das Bild aktualisierst. Wird denn tatsächlich nur das JLabel neu gezeichnet? Würde nicht einfach eine JComponent anstatt dem JLabel reichen (macht aber normal nichts groß performancemäßig aus)? Machen 50fps überhaupt Sinn (das nimmt der Mensch ja gar nicht mehr wahr; normalerweise reichen 25fps)?

http://java.sun.com/products/java-media/2D/samples/index.html hier gibts ein Sample zum Rendering. Es gibt dort auch eine "OnScreen"-Methode die teils sehr schnell ist. Diese überschreibt den RepaintManager in der paintImmediately-Methode (kannst dir ja im Code ansehen).

http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html hier gibts ne Abhandlung über das Skalieren von Bildern.

Für detailliertere Aussagen müsstest du mal genau erklären was du da zeichnest und/oder Code bereitstellen, den man selbst testen kann. Ich habe selbst eine Anwendung in Java erstellt, die sehr performant zeichnet, aber es gibt da so viele Ansatzpunkte (siehe oben), dass es mit so nem Ausschnitt schwer zu sagen ist wo du da nen Fehler machst.
 
Ich wills jetzt nicht verschreien, da ich einige Codeänderungen noch vorzunehmen habe, aber ein erster Test ist sehr sehr vielversprechend. Einstweilen danke, ich werde das Ergebnis morgen früh posten.
 
Ja sorry für die verspätete Antwort. Ich hab es jetzt umgeschrieben. Da ich jetzt ein BufferedImage mit vorgegebenem ColorModel bekomme, hab ich mein 16-Farben Indexed ColorModel gekippt und mir ein int[][] array bestehend aus [r,g,b][farbwert] gebaut und schreibe die entsprechenden Werte direkt ins WritableRaster des BufferedImages. Die setRBG() Funktion des BufferedImages war zu ineffizient.
Ergebnis: Enorme Beschleunigung, je nach Zoomstufe.

Sehr interessant ist auch, dass die Umstellung auf Java 1.6 (ich entwickel und MacOS) nochmals nen deutlichen Sprung nach vorne machte.

Das einzige, was leider nicht funktioniert, ist die Filterung des Bilds. Wenn ich es durch nen bilinearen oder bikubsichen Filter schicke, heißt es Ende im Gelände... :(

Code:
public void paint (Graphics g) {
		if( img == null )
			return;
		Graphics2D g2d = (Graphics2D)g;
		
		__frame_width = this.getWidth();
		__frame_height = this.getHeight();
		
		//g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
		
		g2d.drawImage(img, 0, 0, __frame_width, __frame_height, Color.black, null);
		g2d.dispose();
	}

Die Variable img ist vom Typ BufferedImage und ne Referenz auf die Instanz, auf die ich in einer anderen Klasse während des Bildaufbaus schreibe. Von dieser aus wird dann auch repaint() aufgerufen, welches wiederum die obige paint() Mehtode aufruft.
Wenn ich die auskommentierte Zeile zur Filterung verwende, wird die Sache wieder zur Katastrophe. Ansonsten wird das Bild annehmbar flott skaliert.

Grund für die (noch vorhandene) Aufteilung in 2 Klassen war der Gedanke, das Filtern und Skalieren des Bildes in nen eigenen WorkerThread auszulagern. Das funktionierte aber nicht so leicht und ist (effiziente Filterung vorausgesetzt) auch nicht mehr nötig.


PS: Was programmier ich da eigentlich? Siehe Screenshot. Bitte keine Diskussion entfachen, warum ich den (n+1)ten schreibe (:
 

Anhänge

  • Picture 3.png
    Picture 3.png
    108,7 KB · Aufrufe: 292
Das Projekt sieht wirklich interessant aus ;)

Ich bin mir über die genaue Technik des C64 nicht im Klaren. Aber kanns du nicht mit drawLine(), drawRect() usw. evtl. effizienter malen als wenn du nur die Rasterpunkte füllst? Die Funktionen sind nämlich wie gesagt meist Hardwarebeschleunigt (aufm Mac müsste es über OpenGL gehen) während das Schreiben jedes einzelnen Punktes eben nicht wirklich beschleunigt werden kann.

Auch möglich wäre die Verwendung eines VolatileImage anstatt des BufferedImage. Das hab ich bislang noch nicht gemacht, da die Verwendung ein bisschen komplizierter ist weil das Bild jederzeit "verloren" gehen kann. Gibts aber auch Lösungen und Tutorials dafür. Ist auch machbar und einen Versuch wert wenn man wirklich das Letzte an Performance braucht.

Auch wichtig wäre es (sofern technisch möglich; kenn mich wie gesagt mit C64 nicht so aus), dass man nur die tatsächlich geänderten Bildinhalte updated und nicht bei jedem Durchgang alles.

Des Weiteren könntest du als Zeichenfläche ein Bild in der bereits korrekt skalierten Größe verwenden. Auf das Graphics2D-Objekt dieser Zeichenfläche kann man per AffineTransform (g2d.setTransform()) die Zoomstufe übertragen, so dass man mit den ganz normalen Koordinaten zeichnen kann und selbst nicht umrechnen braucht (allerdings nur über drawLine und Konsorten, nicht über das Raster!). Skalierung ist dann nur beim Zeichnen der einzelnen Linien und anderen Formen nötig (aber nicht fürs ganze Bild) und wird in der Regel von der Grafikkarte erledigt.
 
Ich hatte auch schon an inkrementelle Bildschirmupdates gedacht. Dabei ergeben sich aber leider mehrerelei Probleme:

- Der VIC des C64 beherrscht ein halbes Dutzend unterschiedlicher Grafikmodi. Die meisten davon (Bitmap) haben wenig Schema. Bei den Standard-Character Modi wie im Screenshot gezeigt könnte man sicher ne Menge optimieren. Es ist halt fraglich, ob es sich hier lohnt, wenn es im Bitmap Modus hinfällig wird.

- Der VIC des C64 baut das Bild selber auf ne recht komplizierte Weise Zeile für Zeile auf (der VIC ist gleichzeitig Memory Controller und kann den Prozessor blockieren). Ich weiß noch nicht, in wie weit die Simulation hier wirklich notwendig ist. Das klärt sich erst mit mehr Erfahrung.

- Der VIC kann Interrupts bei Hardware-Kollisionen auslösen. Damit ist ne Kollisionserkennung von bis zu 8 "Sprites" in Hardware gemeint. Bislang hab ich dazu nichts implementiert. Allerdings will ich jetzt nicht beträchtlichen Aufwand in etwas stecken, was ich in 2 Wochen eh wieder kippe.


Insgesamt muss ich dazu sagen, dass dieses Projekt für ein Seminar gedacht ist. Mir ist absolut klar, dass es ne ganze Reihe (auch freier) Emulatoren gibt (auch in Java). Grund für dieses Projekt war nur, mal ein etwas anderes Thema zu wählen, etwas Praxiserfahrung mit Emulation zu bekommen, ne Programmiersprache zu lernen, von der ich keine Ahnung habe, und mein Nostalgie auszuleben. ;)
 
Zurück
Oben