RoanH / KeysPerSecond

A keys-per-second meter & counter. Written for osu! but should work for other rhythm games too.
https://osu.ppy.sh/forum/t/552405/
GNU General Public License v3.0
346 stars 28 forks source link

Flickering in Linux if background's opacity is not 100% and update rate is high #58

Open UlyssesZh opened 2 years ago

UlyssesZh commented 2 years ago

Just as the title says. The Java I use is OpenJDK 18.

RoanH commented 2 years ago

Thanks for the report, there used to be another open issue describing an issue very similar to this. But it appears that the issue and original author are both deleted from GitHub so I completely lost track of this...

I assume that the entire window/frame is disappearing and not just part part of it?

It's also highly likely that this is a problem with the display/graphics driver and therefore possibly not something I can fix. I'll try to run some transparency tests on Linux when I have more free time.

UlyssesZh commented 2 years ago

Sorry because I reinstalled the whole OS on this computer to an Ubuntu 20.04 and I cannot reproduce the bug with X11 GNOME... I'll just describe the phenomenon: the whole window of KeysPerSecond (the one that shows keyboard actions) is quickly switching between the normal appearance and the abnormal appearance where the window's rect is totally black (not transparent). The fast switching makes it looks like flickering.

RoanH commented 2 years ago

Alright, that does sounds slightly different from the previous issue, in the old issue the entire window disappeared. If it goes black then there might be something I can do, assuming I manage to reproduce it so I can debug it.

UlyssesZh commented 2 years ago

I suddenly reproduced this (Ubuntu 20.04, X11).

RoanH commented 2 years ago

Thanks for the information, I should have a system that fits those specifications so I'll try to reproduce it.

RoanH commented 2 years ago

Alright, I managed to reproduce this. Seems like the entire window goes invisible for a bit when the GUI is redrawn, the result also isn't very transparent either. So out of curiosity I tried some older versions again that I know used to work fine with transparency. Version 5.5 sort of still works, but it leaves after images for some reason? Version 3.12 on the other hand looks like a mess. Interestingly the transparency logic between v5.5 and v8.7 is nearly identical, so I have no idea how the results are so different. Unfortunately, I don't really have the time right now to investigate this much further. This seems like it would probably be fairly hard to fix and possibly also something that could break really easily again in the future.

RoanH commented 2 years ago

Still not entirely sure if this issue and https://github.com/RoanH/KeysPerSecond/issues/37 are caused by the same root problem, but it seems probable.

T4M1L0S commented 1 year ago

That does happen to me as well.

UlyssesZh commented 10 months ago

Seems related: https://stackoverflow.com/q/15704947/10245493

RoanH commented 10 months ago

Yeah that looks like it's probably the exact same issue, bit unfortunate that it has no answer though...

UlyssesZh commented 10 months ago

I managed to produce the same bug with this minimal code:

import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class test {
    static final int WIDTH = 200;
    static final int HEIGHT = 100;
    static final int REFRESH_RATE_MS = 1;
    static final Color BACKGROUND_COLOR = new Color(255, 0, 0, 127);

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JPanel content = new JPanel() {
                @Override
                public void paintComponent(Graphics g1) {
                    Graphics2D g = (Graphics2D) g1;
                    g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, BACKGROUND_COLOR.getAlpha() / 255f));
                    g.setColor(BACKGROUND_COLOR);
                    g.fillRect(0, 0, getWidth(), getHeight());
                }
            };
            content.setOpaque(false);
            content.setBackground(new Color(0, 0, 0, 0));

            JFrame frame = new JFrame("test");
            frame.setUndecorated(true);
            frame.setBackground(new Color(0, 0, 0, 0));
            frame.add(content);
            frame.setSize(WIDTH, HEIGHT);
            frame.setVisible(true);

            Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(content::repaint, 0, REFRESH_RATE_MS, TimeUnit.MILLISECONDS);
        });
    }
}

The key is that REFRESH_RATE_MS cannot be too high. Here it is 1. The bug would not happen if it is set to something larger than that. Although the minimal example does not have this bug when the refresh rate is at least 2ms (on my machine), there is still some occasional flickering with KPS even if I set the update rate to 500ms (though much less intense than the situation with 1ms).

My guess is that, with setOpaque(false) and AlphaComposite.SRC, for each cycle, the graphics gets cleared before redrawing. However, the next cycle kicks in before the redrawing finishes, and they kind of conflict. For low refresh rates, flickering can still occasionally happen if the buffer somehow gets written to the display before the redrawing finishes. Those who implemented this logic for Linux must haven't thought thoroughly enough about manipulating the buffers...

With this guess, I think if we can let the UI redraw on demand (only redraw when something updates, and only redraw the area where there is some update), the flickering should be mostly gone except for graphs (which are updated every cycle).

RoanH commented 10 months ago

Seems like the underlying issue could be with the double buffering implementation being used, I wonder if it's even double buffered at all at this point.... But there are also a number of seemingly related JDK bugs (e.g., JDK-8303950).

Probably the amount of content that has to be drawn affects how easily it occurs too. Swing is supposed to be single threaded though, so it shouldn't be possible for two repaints to happen concurrently (repaint just schedules a repaint, it doesn't actually repaint). I'm surprised it's possible to flush an intermediate state to the display at all though given that Swing is supposed to be double buffered.

There's not really much to change when it comes to UI updates though. Graph and data panels need to be updated each cycle and key panels already repaint only themselves on demand. So the only save there is not repainting key panels during an update cycle, which I'm not expecting to change much.

I wonder if things could be fixed or mitigated if I implement double/triple buffering myself though, but I'm expecting some underlying JDK issue to be the core problem.