Results 1 to 7 of 7
Like Tree1Likes
  • 1 Post By kjkrum

Thread: "Terminal" component - am I doin' it right?

  1. #1
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default "Terminal" component - am I doin' it right?

    I just starting learning Java2D a couple days ago. I found a lot of tutorials online, and many of them included things I'm pretty sure are wrong, like overriding paint() instead of paintComponent(). So I don't know what to believe.

    I'm creating a "terminal" component for a game client. There's no VT100 emulation or anything, just a component that mimics a text terminal. My questions are:

    1. Have I done anything glaringly stupid in this code?

    2. Are there any obvious ways I can improve the performance of this code?

    In particular, I'm seeing no performance difference with or without -Dsun.java2d.opengl=True. This surprised me, and makes me wonder if I'm doing something wrong. OpenGL is working; it says "OpenGL pipeline enabled for default config on screen 0" on the console.

    Thanks...

    (I made the JComponent into a SSCCE, though it's not very S...)

    Java Code:
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Font;
    import java.awt.FontMetrics;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.RenderingHints;
    import java.awt.Toolkit;
    import java.awt.geom.Rectangle2D;
    import java.awt.image.BufferedImage;
    
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public class TerminalComponent extends JComponent {
    	private static final long serialVersionUID = 1L;
    
    	public static final int MIN_COLUMNS = 1;
    	public static final int MIN_ROWS = 1;
    	
    	protected BufferedImage image;
    	protected Font font;
    	protected FontMetrics metrics;
    	protected int columns;
    	protected int rows;
    	protected int glyphWidth;
    	protected int glyphHeight;
    	protected int baselineOffset;
    
    	
    	@SuppressWarnings("deprecation")
    	public TerminalComponent(int columns, int rows, Font font) {
    		if(columns < MIN_COLUMNS) columns = MIN_COLUMNS;
    		if(rows < MIN_ROWS) rows = MIN_ROWS;
    		this.font = font;
    		
    		this.columns = columns;
    		this.rows = rows;
    		//Graphics2D g2d = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).createGraphics();
    		//FontMetrics metrics = g2d.getFontMetrics(font);
    		metrics = Toolkit.getDefaultToolkit().getFontMetrics(font);
    		baselineOffset = metrics.getMaxAscent();
    		glyphHeight = metrics.getMaxAscent() + metrics.getMaxDescent();
    		glyphWidth = metrics.getMaxAdvance();
    		Dimension componentSize = new Dimension(columns * glyphWidth, rows * glyphHeight);
    		image = new BufferedImage(componentSize.width, componentSize.height, BufferedImage.TYPE_INT_ARGB);
    		Graphics2D g2d = image.createGraphics();
    		g2d.setBackground(Color.BLACK);
    		g2d.clearRect(0, 0, componentSize.width, componentSize.height);
    		this.setPreferredSize(componentSize);		
    		//this.setDoubleBuffered(true);
    	}
    	
    	
    	public void drawString(int column, int row, String text) {
    		drawString(column, row, text, Color.WHITE);
    	}
    	
    	
    	public void drawString(int column, int row, String text, Color color) {
    		Graphics2D g2d = image.createGraphics();
    		
    		// clear the area
    		g2d.setBackground(Color.BLACK);
    		g2d.clearRect(column * glyphWidth, row * glyphHeight, text.length() * glyphWidth, glyphHeight);
    		
    		// draw the text
    		g2d.setColor(color);
    		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    		g2d.setFont(font);
    		g2d.drawString(text, column * glyphWidth, row * glyphHeight + baselineOffset);
    		Rectangle2D line = metrics.getStringBounds(text, g2d);
    		this.repaint(column * glyphWidth, row * glyphHeight, (int) line.getWidth(), glyphHeight);
    	}
    	
    	
    	@Override
    	public void paintComponent(Graphics g) {
    		Graphics2D g2d = (Graphics2D) g;
    		g2d.drawImage(image, 0, 0, null);
    	}
    	
    	
    	public static void main(String[] args) {
    		try {
    			//InputStream in = TerminalComponent.class.getResourceAsStream("/resource/VeraMono.ttf");
    			//Font font = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(18f);
    			Font font = new Font(Font.MONOSPACED, Font.PLAIN, 18);
    			JFrame frame = new JFrame("Terminal");
    			JPanel panel = new JPanel();
    			TerminalComponent term = new TerminalComponent(80, 25, font);
    			panel.add(term);
    			frame.getContentPane().add(panel);
    			frame.pack();
    			frame.setResizable(false);
    			frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    			frame.setVisible(true);
    			
    			Thread.sleep(3000);
    			/*
    			for(int r = 0; r < 16; ++r) {
    				StringBuilder sb = new StringBuilder();
    				for(int c = 0; c < 16; ++c) {
    					sb.append(CP437.decoded[r * 16 + c]);
    				}
    				term.text(0, r, sb.toString());				
    			}
    			*/
    			
    			long begin = System.currentTimeMillis();
    			for(int i = 0; i < 1000; ++i) {
    				int x = (int) (Math.random() * 80);
    				int y = (int) (Math.random() * 25);
    				term.drawString(x, y, "Beer");
    			}
    			long now = System.currentTimeMillis();
    			term.drawString(0, 24, "1000 strings drawn in " + (now - begin) + "ms", Color.RED);
    
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    Last edited by kjkrum; 02-16-2012 at 12:14 AM.
    Get in the habit of using standard Java naming conventions!

  2. #2
    DarrylBurke's Avatar
    DarrylBurke is offline Forum Police
    Join Date
    Sep 2008
    Location
    Madgaon, Goa, India
    Posts
    11,457
    Rep Power
    20

    Default Re: "Terminal" component - am I doin' it right?

    Quote Originally Posted by kjkrum View Post
    In particular, I'm seeing no performance difference with or without -Dsun.java2d.opengl=True. This surprised me, and makes me wonder if I'm doing something wrong. OpenGL is working; it says "OpenGL pipeline enabled for default config on screen 0" on the console.
    AFAIK, starting from Java 6u10 (Version 1.6.0_10), OpenGL is enabled by default.

    db
    If you're forever cleaning cobwebs, it's time to get rid of the spiders.

  3. #3
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: "Terminal" component - am I doin' it right?

    Thanks for the reply.

    But, hmm. Explicitly setting it to "false" has no effect, either.

    My main concern is whether there's anything terribly naive about my basic design. I'm trying to walk the line between premature optimization and doing something stupid that'll force me to completely rework a key part of my program later on.

    To summarize, I'm drawing on a BufferedImage and calling repaint(). If I understand correctly, this triggers an asynchronous call to paint() which calls paintComponent(). I've overridden paintComponent() to simply draw the BufferedImage onto the component. And if I understand that part correctly, the clipping area of the Graphics object passed to paintComponent() will be a bounding box around those areas marked dirty by repaint(), not necessarily the whole image. I've tested this with some printlns and it seems to be the case.

    Basically... am I doing it right?
    Get in the habit of using standard Java naming conventions!

  4. #4
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: "Terminal" component - am I doin' it right?

    Do Graphics2D.drawString(...) and Graphics2D.drawImage(...) both benefit from OpenGL?

    If I use the trace option, I see that the text rendering "primitive" (native method?) is called 1001 times, as expected. But the "blit" primitive is only called about 40 times. I guess that would be the asynchronous repainting in action. If only the "blit" is benefiting from OpenGL, that would explain the negligible performance difference.

    When I next have time, I'm going to try rendering the text once and use drawImage(...) instead of drawString(...) to draw it 1000 times. I'll report the results here.
    Get in the habit of using standard Java naming conventions!

  5. #5
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: "Terminal" component - am I doin' it right?

    Java Code:
    $ java -Dsun.java2d.opengl=true -Dsun.java2d.trace=count krum.weaponm.terminal.TerminalComponent
    1 call to sun.java2d.opengl.OGLTextureToSurfaceBlit::Blit("OpenGL Texture", AnyAlpha, "OpenGL Surface")
    12 calls to sun.java2d.opengl.OGLRTTSurfaceToSurfaceBlit::Blit("OpenGL Surface (render-to-texture)", AnyAlpha, "OpenGL Surface")
    1001 calls to sun.java2d.loops.DrawGlyphListAA::DrawGlyphListAA(AnyColor, SrcNoEa, IntRgb)
    1002 calls to sun.java2d.loops.FillRect::FillRect(AnyColor, SrcNoEa, AnyInt)
    1 call to sun.java2d.opengl.OGLSwToTextureBlit::Blit(IntRgb, SrcNoEa, "OpenGL Texture")
    11 calls to sun.java2d.opengl.OGLSwToSurfaceBlit::Blit(IntRgb, AnyAlpha, "OpenGL Surface")
    2028 total calls to 6 different primitives
    BTW, OpenGL does not appear to be enabled by default:

    Java Code:
    java -Dsun.java2d.trace=count krum.weaponm.terminal.TerminalComponent
    1001 calls to sun.java2d.loops.DrawGlyphListAA::DrawGlyphListAA(AnyColor, SrcNoEa, IntRgb)
    1002 calls to sun.java2d.loops.FillRect::FillRect(AnyColor, SrcNoEa, AnyInt)
    39 calls to sun.java2d.loops.Blit::Blit(IntRgb, SrcNoEa, IntRgb)
    44 calls to sun.java2d.x11.X11PMBlitLoops::Blit("Integer RGB Pixmap", SrcNoEa, "Integer RGB Pixmap")
    44 calls to X11FillRect
    2130 total calls to 5 different primitives
    Possibly because I'm not using Oracle's JVM:

    Java Code:
    $ java -version
    java version "1.6.0_22"
    OpenJDK Runtime Environment (IcedTea6 1.10.4) (6b22-1.10.4-0ubuntu1~11.04.1)
    OpenJDK 64-Bit Server VM (build 20.0-b11, mixed mode)
    Get in the habit of using standard Java naming conventions!

  6. #6
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: "Terminal" component - am I doin' it right?

    Oh hell yes. This version runs six times faster (~75ms vs. ~450ms on my Asus 1015T). I broke encapsulation in the main method. This is just a proof of concept.

    In practice, I won't be drawing the same string every time. So this potential performance gain might be hard to realize. I would have to pre-render each individual character, possibly in multiple colors, and call drawImage(...) multiple times to render a string. But it might still be faster than Graphics2D.drawString(...).

    Java Code:
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Font;
    import java.awt.FontMetrics;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.GraphicsConfiguration;
    import java.awt.GraphicsDevice;
    import java.awt.GraphicsEnvironment;
    import java.awt.RenderingHints;
    import java.awt.Toolkit;
    import java.awt.Transparency;
    import java.awt.geom.Rectangle2D;
    import java.awt.image.BufferedImage;
    
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public class TerminalComponent extends JComponent {
    	private static final long serialVersionUID = 1L;
    
    	public static final int MIN_COLUMNS = 1;
    	public static final int MIN_ROWS = 1;
    	
    	protected BufferedImage image;
    	protected Font font;
    	protected FontMetrics metrics;
    	protected int columns;
    	protected int rows;
    	protected int glyphWidth;
    	protected int glyphHeight;
    	protected int baselineOffset;
    
    	
    	@SuppressWarnings("deprecation")
    	public TerminalComponent(int columns, int rows, Font font) {
    		if(columns < MIN_COLUMNS) columns = MIN_COLUMNS;
    		if(rows < MIN_ROWS) rows = MIN_ROWS;
    		this.font = font;
    		
    		this.columns = columns;
    		this.rows = rows;
    
    		metrics = Toolkit.getDefaultToolkit().getFontMetrics(font);
    		baselineOffset = metrics.getMaxAscent();
    		glyphHeight = metrics.getMaxAscent() + metrics.getMaxDescent();
    		glyphWidth = metrics.getMaxAdvance();
    		Dimension preferredSize = new Dimension(columns * glyphWidth, rows * glyphHeight);
    		this.setPreferredSize(preferredSize);
    		
    		///*
    		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    		GraphicsDevice gd = ge.getDefaultScreenDevice();
    		GraphicsConfiguration gc = gd.getDefaultConfiguration();
    		image = gc.createCompatibleImage(preferredSize.width, preferredSize.height, Transparency.OPAQUE);
    		//*/
    		
    		//image = new BufferedImage(preferredSize.width, preferredSize.height, BufferedImage.TYPE_INT_ARGB);
    		
    		Graphics2D g2d = image.createGraphics();
    		g2d.setBackground(Color.BLACK);
    		g2d.clearRect(0, 0, preferredSize.width, preferredSize.height);
    		this.repaint();
    	}
    	
    	
    	public void drawString(int column, int row, String text) {
    		drawString(column, row, text, Color.WHITE);
    	}
    	
    	
    	public void drawString(int column, int row, String text, Color color) {
    		Graphics2D g2d = image.createGraphics();
    		//Graphics2D g2d = (Graphics2D) this.getGraphics();
    		
    		// clear the area
    		g2d.setBackground(Color.BLACK);
    		g2d.clearRect(column * glyphWidth, row * glyphHeight, text.length() * glyphWidth, glyphHeight);
    		
    		// draw the text
    		g2d.setColor(color);
    		g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    		g2d.setFont(font);
    		g2d.drawString(text, column * glyphWidth, row * glyphHeight + baselineOffset);
    		Rectangle2D line = metrics.getStringBounds(text, g2d);
    		this.repaint(column * glyphWidth, row * glyphHeight, (int) line.getWidth(), glyphHeight);
    	}
    	
    
    	@Override
    	public void paintComponent(Graphics g) {
    		Graphics2D g2d = (Graphics2D) g;
    		g2d.drawImage(image, 0, 0, null);
    	}
    
    	
    	public static void main(String[] args) {
    		try {
    			//InputStream in = TerminalComponent.class.getResourceAsStream("/resource/VeraMono.ttf");
    			//Font font = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(18f);
    			Font font = new Font(Font.MONOSPACED, Font.PLAIN, 18);
    			JFrame frame = new JFrame("Terminal");
    			//JPanel panel = new JPanel();
    			TerminalComponent term = new TerminalComponent(80, 25, font);
    			//panel.add(term);
    			//frame.getContentPane().add(panel);
    			frame.getContentPane().add(term);
    			frame.pack();
    			frame.setResizable(false);
    			frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    			frame.setVisible(true);
    			
    			Thread.sleep(3000);
    			
    			/*
    			for(int r = 0; r < 16; ++r) {
    				StringBuilder sb = new StringBuilder();
    				for(int c = 0; c < 16; ++c) {
    					sb.append(CP437.decoded[r * 16 + c]);
    				}
    				term.text(0, r, sb.toString());				
    			}
    			*/
    			
    			String word = "Beer";
    			BufferedImage pre = new BufferedImage(term.glyphWidth * word.length(), term.glyphHeight, BufferedImage.TYPE_INT_RGB);
    			Graphics2D g2d = (Graphics2D) pre.getGraphics();
    			g2d.setColor(Color.WHITE);
    			g2d.setBackground(Color.BLACK);
    			g2d.setFont(term.font);
    			g2d.drawString(word, 0, term.baselineOffset);
    			
    			g2d = term.image.createGraphics();
    			long begin = System.currentTimeMillis();
    			for(int i = 0; i < 1000; ++i) {
    				int x = (int) (Math.random() * 80);
    				int y = (int) (Math.random() * 25);
    				//term.drawString(x, y, "Beer");
    				g2d.drawImage(pre, term.glyphWidth * x, term.glyphHeight * y, null);
    				term.repaint(x * term.glyphWidth, y * term.glyphHeight, term.glyphWidth * word.length(), term.glyphHeight);
    				
    			}
    			long now = System.currentTimeMillis();
    			term.drawString(0, 24, "1000 strings drawn in " + (now - begin) + "ms", Color.RED);
    
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    	}
    }
    Get in the habit of using standard Java naming conventions!

  7. #7
    kjkrum's Avatar
    kjkrum is offline Senior Member
    Join Date
    Apr 2011
    Location
    Tucson, AZ
    Posts
    1,060
    Rep Power
    6

    Default Re: "Terminal" component - am I doin' it right?

    I just wanted to follow up on this thread in case anyone reads it in the future. I ended up doing two things to my code that improved its performance dramatically. The changes are spread out over several classes, so I'll describe them instead of posting them.

    The first thing I did was to pre-render all the characters I might want to draw onto a sprite sheet, and then copy from that image instead of calling Graphics2D.drawString(). This resulted in the approximately 6x performance increase from my previous comment.

    The second thing I did was to completely get away from drawing on a BufferedImage. Instead, my component class now stores a representation of the on-screen characters and their color attributes packed into an int[][]. The paintComponent() method figures out the row/column coordinates of the Graphics argument's clipping rectangle and draws the characters in that rectangle, again from a pre-rendered sprite sheet. And to take full advantage of Swing's double buffering, the sprite sheet is now a VolatileImage. With these changes, performance improved an additional 40x!
    DarrylBurke likes this.
    Get in the habit of using standard Java naming conventions!

Similar Threads

  1. Replies: 1
    Last Post: 02-01-2012, 09:37 PM
  2. Replies: 5
    Last Post: 12-21-2011, 07:44 PM
  3. Replies: 4
    Last Post: 09-16-2011, 11:08 PM
  4. Replies: 2
    Last Post: 01-24-2009, 07:56 PM
  5. Replies: 1
    Last Post: 10-20-2008, 08:35 AM

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •