2022-04-19

How to limit repainting to monitor refresh rate (vsync)

I have a weird issue that every other guide and answer seems to contradict, but the issue seems to be deeper. OS-level deep.

System details:
Ubuntu 18.04, Unity window manager, nVidia graphics (proprietary driver)
Tried with the following Java VMs:
-OpenJDK 11 & 17,
-Temurin 11 & 17,
-JBR-17

I have an application where I draw an image on a canvas, zoomed (so it's pixelated), and I can pan around and edit with the mouse (sort of like photoshop). I do this by defining a small rectangle in the image and drawing that to the entire panel (at 4K resolution):

    g.drawImage(image,
                x, // dst
                y,
                x + visibleImageWidth * blockSize,
                y + visibleImageHeight * blockSize,
                ul.x, // src
                ul.y,
                ul.x + visibleImageWidth,
                ul.y + visibleImageHeight,
                this);

This works fine statically, but when I start working it with the mouse, it progressively slows down to a crawl.

I analyzed this, and it seems that the mouse fires events at 1000Hz. Then paintComponent() somehow manages to finish within that same 1ms. The OS however chokes on the amount of visual data thrown at it, and every (visual) update takes longer than the last. As long as I keep dragging the mouse, the OS crawls to a complete stop. (It seems everything non-graphical still works at normal speed, e.g. my program keeps processing input) Also visual updates of other programs stop, so it's like the graphics card or driver chokes on the data and can't process/discard it fast enough. When I let go of the mouse it stays frozen until next visual update. Then all programs instantly update their visuals and everything is back to normal.

The (heavyweight) JPanel that I have doesn't collate repaint() calls where I expect it should. Every time I call it, it immediately (from my perspective) calls paintComponent(), which finishes within 1ms, before the next call to repaint().

* Why is Java so insistent on sending graphics data to the OS at such a ridiculous speed? My monitor runs at 60Hz, not 1000Hz.

I found a very dirty workaround that at least lets me use the program in any reasonable way at all: Adding

    try { 
        Thread.sleep(10);
    } catch (InterruptedException ignored) {
    }

at the end of the paintComponent() method. The program now works super smooth, without visible tearing or microstuttering (even though 10ms != 60Hz).

Why do I need this delay to, of all things, speed up graphics? How can I make Java respect the monitor's refresh rate in a 'neater' way?

[Edit] MSSCC

import java.awt.Frame;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;

public class Temp extends JPanel {
    public static void main(String... args) {
        System.out.println(LocalDate.now());
        SwingUtilities.invokeLater(() -> {
            JFrame f = new JFrame();
            f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            f.setContentPane(new Temp());
            f.setExtendedState(Frame.MAXIMIZED_BOTH);
            f.setVisible(true);
        });
    }

    private final BufferedImage image = new BufferedImage(10000, 10000, BufferedImage.TYPE_INT_RGB);

    public Temp() {
        super(null);
        
        addMouseMotionListener(new MouseAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                int x   = e.getX() / 5;
                int y   = e.getY() / 5;
                int rgb = ThreadLocalRandom.current().nextInt(0xFFFFFF);

                int[] pixels = new int[100];
                Arrays.fill(pixels, rgb);
                image.getRaster().setDataElements(x, y, 8, 8, pixels);

                repaint();

                ((Frame)getTopLevelAncestor()).setTitle(" (" + x + ", " + y + ')');
            }
        });
    }

    @Override
    public void paintComponent(Graphics g) {
        g.drawImage(image,
                    0, 0, getWidth(), getHeight(),
                    0, 0, getWidth() / 5, getHeight() / 5,
                    null);
    }
}


No comments:

Post a Comment