Search code examples
javaswingjava-2d

Java Swing 2D - Draw many small stars quickly


I'm trying to make a game, and in the game is a background that has many small stars that slowly travel from right to left. With a full screen I may have 170 stars being drawn each frame. The majority of the CPU time rendering each frame is in the drawing of the stars. Is there's a way to use resources more effectively?

I have taken into consideration the points in this answer: https://stackoverflow.com/a/200493/21618530

An approach I've used is to use device compatible VolatileImages, using 1 of 3 variants to avoid scaling with BITMASK transparency. It takes on average 22ms to render each frame this way.

jvisualvm profiler output for drawImage

Using fillOval() for stars instead of drawing images reduces runtime to about 20ms each frame, with this profiler output:

jvisualvm profiler output for fillOval

This is on a laptop with an Intel UHD 620 with a 3840x2160 monitor. I intend for this game to be simple and playable on a wide range of graphics, so it would be good if it could run well on a device like this.


I'm open to other strategies to have the same outcome, but my goal here is to better understand swing 2d performance so I might improve the game overall.

MRE showing about how I've done this, on my machine the volatile image usage is slower on average that drawing the oval each time.

package space;

import java.awt.AWTException;
import java.awt.BufferCapabilities;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.ImageCapabilities;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.VolatileImage;
import java.util.Random;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class MRE {

    private static final int OVAL_SIZE = 4;
    private static final int NUM_STARS = 150;
    private static final boolean useImages = false;

    private static VolatileImage volatileImage;

    public static void main(String[] args) {
        System.setProperty("sun.java2d.opengl", "true");
        System.setProperty("sun.java2d.accthreshold", "0");
        JFrame frame = new JFrame();
        frame.setSize(new Dimension(1000, 800));
        frame.setIgnoreRepaint(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        RepaintManager manager = new RepaintManager() {
            @Override
            public synchronized void addDirtyRegion(JComponent c, int x, int y, int w, int h) {
                // Ignored
            }

            @Override
            public Rectangle getDirtyRegion(JComponent aComponent) {
                return aComponent.getBounds();
            }

            @Override
            public void markCompletelyDirty(JComponent aComponent) {
                // Ignored
            }

            @Override
            public void markCompletelyClean(JComponent aComponent) {
                // Ignored
            }
        };
        createVolatileImage(frame);
        RepaintManager.setCurrentManager(manager);
        Timer st = new Timer(250, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                long startTime;
                startTime = System.nanoTime();
                JComponent contentPanel = ((JComponent) frame.getContentPane());
                contentPanel.paintImmediately(contentPanel.getBounds());
                long elapsed = System.nanoTime() - startTime;
                System.out.println("Rendering time: " + elapsed / 1000000.0 + "ms.");
            }

        });

        frame.setContentPane(createContentPane());

        setWindowVisible(frame, true);
        st.start();
    }

    private static void createVolatileImage(JFrame frame) {
        GraphicsConfiguration gc = frame.getGraphicsConfiguration();
        volatileImage = gc.createCompatibleVolatileImage(4, 4, Transparency.BITMASK);
        Graphics2D g2 = volatileImage.createGraphics();
        g2.setColor(Color.WHITE);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.fillOval(0, 0, OVAL_SIZE, OVAL_SIZE);
        g2.dispose();
    }

    private static JPanel createContentPane() {
        JPanel contentPane = new JPanel() {
            private static final long serialVersionUID = 2500224655024127049L;

            @Override
            public void paintComponent(Graphics g) {
                Graphics2D g2 = (Graphics2D)g;
                Random rand = new Random();
                g2.setColor(Color.BLACK);
                g2.fillRect(0, 0, getWidth(), getHeight());
                for(int i = 0; i < NUM_STARS; i++) {
                    double x = rand.nextDouble() * getWidth();
                    double y = rand.nextDouble() * getHeight();
                    g2.translate(x, y);
                    if(useImages) {
                        g2.drawImage(volatileImage, 0, 0, null);
                    } else {
                        g2.setColor(Color.WHITE);
                        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                        g2.fillOval(0, 0, OVAL_SIZE, OVAL_SIZE);
                    }
                    g2.translate(-x, -y);
                }
            }
        };
        return contentPane;
    }

    private static void setWindowVisible(JFrame frame, final boolean visible) {
        SwingUtilities.invokeLater(() -> {
            frame.setVisible(visible);
            try {
                final BufferCapabilities caps = new BufferCapabilities(new ImageCapabilities(true),
                    new ImageCapabilities(true), BufferCapabilities.FlipContents.UNDEFINED);
                frame.createBufferStrategy(2, caps);
            } catch (final AWTException e) {
                frame.createBufferStrategy(2);
                System.err.println("Could not set double buffered FLIP strategy, using default double buffer: "
                    + e.getLocalizedMessage());
            }
        });
    }

}

Solution

  • I used a BufferedImage instead of a VolatileImage and the timing dropped from 40 to 4:

    import java.awt.*;
    import java.awt.BufferCapabilities;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.GraphicsConfiguration;
    import java.awt.ImageCapabilities;
    import java.awt.Rectangle;
    import java.awt.RenderingHints;
    import java.awt.Transparency;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.image.*;
    import java.util.Random;
    
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.RepaintManager;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    
    public class MRE1 {
    
        private static final int OVAL_SIZE = 4;
        private static final int NUM_STARS = 150;
        private static final boolean useImages = true;
    
        private static VolatileImage volatileImage;
        private static BufferedImage bufferedImage;
    
        public static void main(String[] args) {
            System.setProperty("sun.java2d.opengl", "true");
            System.setProperty("sun.java2d.accthreshold", "0");
            JFrame frame = new JFrame();
            frame.setSize(new Dimension(1000, 800));
            frame.setIgnoreRepaint(true);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            createVolatileImage(frame);
    
            Timer st = new Timer(250, new ActionListener() {
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    long startTime;
                    startTime = System.nanoTime();
                    JComponent contentPanel = ((JComponent) frame.getContentPane());
                    contentPanel.paintImmediately(contentPanel.getBounds());
                    long elapsed = System.nanoTime() - startTime;
                    System.out.println("Rendering time: " + elapsed / 1000000.0 + "ms.");
                }
    
            });
    
            frame.setContentPane(createContentPane());
    
            setWindowVisible(frame, true);
            st.start();
        }
    
        private static void createVolatileImage(JFrame frame) {
            GraphicsConfiguration gc = frame.getGraphicsConfiguration();
            volatileImage = gc.createCompatibleVolatileImage(4, 4, Transparency.BITMASK);
            Graphics2D g2 = volatileImage.createGraphics();
            g2.setColor(Color.WHITE);
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2.fillOval(0, 0, OVAL_SIZE, OVAL_SIZE);
            g2.dispose();
    
            bufferedImage = new BufferedImage(OVAL_SIZE, OVAL_SIZE, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2d = bufferedImage.createGraphics();
            g2d.setColor( Color.WHITE );
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            g2d.fillOval(0, 0, OVAL_SIZE, OVAL_SIZE);
            g2d.dispose();
        }
    
        private static JPanel createContentPane() {
            JPanel contentPane = new JPanel() {
                private static final long serialVersionUID = 2500224655024127049L;
    
                @Override
                public void paintComponent(Graphics g) {
                    Graphics2D g2 = (Graphics2D)g;
                    g2.setColor(Color.BLACK);
                    g2.fillRect(0, 0, getWidth(), getHeight());
    
                    Random rand = new Random();
    
                    for(int i = 0; i < NUM_STARS; i++) {
                        double x = rand.nextDouble() * getWidth();
                        double y = rand.nextDouble() * getHeight();
                        g2.translate(x, y);
                        if(useImages) {
    //                        g2.drawImage(volatileImage, 0, 0, null);
                            g2.drawImage(bufferedImage, 0, 0, null);
                        } else {
                            g2.setColor(Color.WHITE);
                            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                            g2.fillOval(0, 0, OVAL_SIZE, OVAL_SIZE);
                        }
                        g2.translate(-x, -y);
                    }
                }
            };
            contentPane.setBackground( Color.BLACK );
            return contentPane;
        }
    
        private static void setWindowVisible(JFrame frame, final boolean visible) {
            SwingUtilities.invokeLater(() -> {
                frame.setVisible(visible);
                try {
                    final BufferCapabilities caps = new BufferCapabilities(new ImageCapabilities(true),
                        new ImageCapabilities(true), BufferCapabilities.FlipContents.UNDEFINED);
                    frame.createBufferStrategy(2, caps);
                } catch (final AWTException e) {
                    frame.createBufferStrategy(2);
                    System.err.println("Could not set double buffered FLIP strategy, using default double buffer: "
                        + e.getLocalizedMessage());
                }
            });
        }
    
    }
    

    Also got rid of the RepaintManager since I see no need for it.

    I've used is to use device compatible VolatileImages, using 1 of 3 variants to avoid scaling with BITMASK transparency

    If that means you don't want the image to scale from 4 to 6 pixels when a scaling factor of 150 is used then maybe you can use the NoScalingIcon. Check out: https://stackoverflow.com/a/65742492/131872

    You should be able to create the Icon using your image and then paint the Icon to the BufferedImage and the image will still be 4 pixels.