0
\$\begingroup\$

I have used the BufferStrategy, but my code runs still way too fast. xrandr reports that TearFree is "on" and the video mode has 60 Hz. But when I measure the time between two frames, I get one millisecond or less instead of about 16ms.

I am using OpenJDK 17.0.4 on Debian 11 using Linux 5.10.0-20-amd64. The hardware is an AMD Ryzen 7 5700G. I have visually tested TearFree with glxgears which reports about 60 frames.

Output of xrandr:

HDMI-A-0 connected 3840x2160+0+0 (normal left inverted right x axis y axis) 698mm x 393mm
        EDID: 
                00ffffffffffff0009d1507945540000
                1e1f0103804627782e5995af4f42af26
                0f5054a56b80d1c0b300a9c081808100
                81c0010101014dd000a0f0703e803020
                3500ba892100001a000000ff0058374d
                30313638353031390a20000000fd0018
                4c1e873c000a202020202020000000fc
                0042656e5120455733323730550a0165
                02034df1515d5e5f6061101f22212005
                14041312030123090707830100006d03
                0c002000387820006001020367d85dc4
                01788003681a00000101283cfde305e0
                01e40f180000e6060501544c2c565e00
                a0a0a029502f203500ba892100001abf
                650050a0402e6008200808ba89210000
                1c0000000000000000000000000000ad
        GAMMA_LUT_SIZE: 4096 
                range: (0, -1)
        DEGAMMA_LUT_SIZE: 4096 
                range: (0, -1)
        GAMMA_LUT: 0 
                range: (0, 65535)
        CTM: 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 
                0 1 
        DEGAMMA_LUT: 0 
                range: (0, 65535)
        TearFree: on 
                supported: off, on, auto
        HDCP Content Type: HDCP Type0 
                supported: HDCP Type0, HDCP Type1
        Content Protection: Undesired 
                supported: Undesired, Desired, Enabled
        vrr_capable: 0 
                range: (0, 1)
        max bpc: 8 
                range: (8, 16)
        underscan vborder: 0 
                range: (0, 128)
        underscan hborder: 0 
                range: (0, 128)
        underscan: off 
                supported: off, on, auto
        scaling mode: None 
                supported: None, Full, Center, Full aspect
        link-status: Good 
                supported: Good, Bad
        CONNECTOR_ID: 80 
                supported: 80
        non-desktop: 0 
                range: (0, 1)
   3840x2160     60.00*+  60.00    50.00    59.94    30.00    25.00    24.00    29.97    23.98  
   2560x1600     59.94  
   2560x1440     59.95  
   1920x1200     60.00  
   1920x1080     60.00    60.00    50.00    59.94    30.00    25.00    24.00    29.97    23.98  
   1600x1200     60.00  
   1680x1050     59.88  
   1600x900      60.00  
   1280x1024     75.02    60.02  
   1440x900      60.00  
   1280x800      59.91  
   1152x864      75.00  
   1280x720      60.00    50.00    59.94  
   1024x768      75.03    60.00  
   832x624       74.55  
   800x600       75.00    60.32  
   720x576       50.00  
   720x480       60.00    59.94  
   640x480       75.00    60.00    59.94  
   720x400       70.08  

Complete Java example code just drawing a single blue dot:

import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FileDialog;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Label;
import java.awt.Menu;
import java.awt.MenuBar;
import java.awt.MenuItem;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Map;

public class Gfx
{
  static void draw ()
  {
    pixel (width/2, height/2, blue);
  }

  static boolean done = false;
  
  static final int black = Color.BLACK.getRGB ();
  static final int white = Color.WHITE.getRGB ();
  static final int red   = Color.RED.getRGB ();
  static final int green = Color.GREEN.getRGB ();
  static final int blue  = Color.BLUE.getRGB ();

  static final int width   = 100;
  static final int height  = 100;
  static final int canvas_width  = 1000;
  static final int canvas_height = 1000;

  static Frame frame = null;
  static void frame (String title)
  {
    frame = new Frame (title);
    frame.addWindowListener (new WindowAdapter ()
      {
        @Override
        public void windowClosing (WindowEvent ev) { done = true; }
      });
    frame.setResizable (false);
    frame.setLayout (new BorderLayout ());
    frame.setFont (new Font(Font.MONOSPACED, Font.PLAIN, 18));
  }

  static FileDialog loadfile = null;
  static void loadfile ()
  {
    loadfile = new FileDialog (frame);
  }

  static BufferedImage image = null;
  static void image ()
  {
    image = new BufferedImage (width, height, BufferedImage.TYPE_INT_ARGB);
    draw();
  }

  static void pixel (int x, int y, int c) { image.setRGB (x, y, c); }
  static void pixel (int x, int y) { pixel (x, y, black); }

  static Canvas canvas = null;
  static void canvas ()
  {
    canvas = new Canvas ()
      {
        @Override
        public Dimension getPreferredSize () {
          return new Dimension (canvas_width, canvas_height); }
        @Override
        public void paint (Graphics graphics)
        {
          super.paint (graphics);
          graphics.drawImage (image, 0, 0, canvas_width, canvas_height, this);
        }
      };
    frame.add (canvas, BorderLayout.CENTER);
  }

  static MenuBar menubar = null;
  static void menubar ()
  {
    menubar = new MenuBar();
    Menu menu_app = new Menu("App");
    menubar.add(menu_app);
    MenuItem app_loadfile = new MenuItem("Load File");
    app_loadfile.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent ev)
        {
          loadfile.setVisible (true);
          System.out.println (loadfile.getDirectory() + loadfile.getFile());
        }});
    menu_app.add(app_loadfile);
    frame.setMenuBar (menubar);
  }

  static Label statusbar = null;
  static void statusbar ()
  {
    statusbar = new Label("Hallo");
    frame.add (statusbar, BorderLayout.SOUTH);
  }

  static void start ()
  {
    frame.pack ();
    frame.setVisible (true);
    canvas.createBufferStrategy(2);
    BufferStrategy strategy = canvas.getBufferStrategy();
    long t0 = -1;
    long t1 = -1;
    DecimalFormat df = new DecimalFormat("###");
    // Main loop
    while (!done) {
      // Prepare for rendering the next frame
      if (t1 > 0) t0 = t1;
      t1 = System.currentTimeMillis();
      if (t0 > 0)
        System.out.print("     \r" + df.format(t1 - t0));

      // Render single frame
      do {
        // The following loop ensures that the contents of the drawing buffer
        // are consistent in case the underlying surface was recreated
        do {
          // Get a new graphics context every time through the loop
          // to make sure the strategy is validated
          Graphics graphics = strategy.getDrawGraphics();
          
          // Render to graphics
          // ...
          image();
          graphics.drawImage (image, 0, 0, canvas_width, canvas_height, canvas);
          
          // Dispose the graphics
          graphics.dispose();

          // Repeat the rendering if the drawing buffer contents
          // were restored
        } while (strategy.contentsRestored());

        // Display the buffer
        strategy.show();

        // Repeat the rendering if the drawing buffer was lost
      } while (strategy.contentsLost());
    }

    // Dispose the window
    frame.setVisible(false);
    frame.dispose();
  }
  
  public static void main (String[] args)
  {
    frame ("Gfx");
    loadfile ();
    menubar ();
    image ();
    canvas ();
    statusbar ();
    start ();
  }
}

I tried also this example, but it does not compile anymore:

VSyncTest.java:7: error: package sun.java2d.pipe.hw is not visible
import sun.java2d.pipe.hw.ExtendedBufferCapabilities;
                      ^
  (package sun.java2d.pipe.hw is declared in module java.desktop, which does not export it)
1 error
\$\endgroup\$
2
  • 1
    \$\begingroup\$ V-Sync is used to avoid screen tearing, not to throttle the framerate. Do you actually want to throttle the framerate? If it is so, you might want to edit the question and ask specifically about that instead of asking about the solution you've come up with. (That's the XY problem.) \$\endgroup\$ Commented Mar 21, 2023 at 11:09
  • \$\begingroup\$ V-Sync means adjust the frame rate so that it is the same as the frame rate of the monitor. This implies throttling. I have not come up with any solution. My code shows just the failure, because it lacks the throttling. \$\endgroup\$ Commented Mar 21, 2023 at 11:41

1 Answer 1

2
\$\begingroup\$

drawImage() returns immediately - it doesn't wait until the content is drawn.

Similarly, I don't see anything in the docs that says any of the other graphics code is blocks until drawn to screen. As such, you aren't measuring the refresh rate of the monitor, you are measuring frame rate of your simulation. specifically, you are measuring how quickly your code is looping & attempting to push stuff out.

The buffering prevents the screen from tearing - it promises that the hardware will update the entire screen at once using a buffer that can't be changed while the hardware is reading from it. But it doesn't do anything to throttle the rate at which you can change the other buffer(s).

If you want to get info about how fast the hardware is doing stuff, you might want to look into OpenGL Timer queries.

If you want to pursue synching up your code with the GL activities, (as discussed on this SO post), then you should look into using sync objects.

If the real goal is to throttle your framerate, we have a number of posts about dealing with timesteps in Java. If none of them address your issue, you should ask a new question about that specifically.

\$\endgroup\$
3
  • \$\begingroup\$ I want to know which of the two buffers is currently in use by the hardware. During the time the hardware displays buffer A, I want to write buffer B. And when the hardware renders buffer B, I want to write buffer A. The hardware should trigger, when I can start drawing into the currently unused buffer. I did this about 25 years ago, when I wrote directly into the memory of my VGA card. I do not understand, why this should not be the case anymore. Video memory has two interfaces: the CPU writes and the monitor "reads". This needs synchronization and I thought V-Sync is the name of it. \$\endgroup\$ Commented Mar 22, 2023 at 12:19
  • \$\begingroup\$ This example shows how to wait for a Cathode-ray tube. The trigger is always the video hardware. I can not believe that this should be different nowadays. \$\endgroup\$ Commented Mar 22, 2023 at 12:39
  • \$\begingroup\$ Yes, I learned similar techniques. Since then graphics systems & OSes have gotten more complex & there's additional layers of abstraction. Modern graphics API + GPU based rendering doesn't always work the same way that hardware interrupt + CPU based rendering did & so trying to leverage side effects from those techniques doesn't always translate. Case in point, BufferStrategy only promises an area of contiguous memory - it doesn't guarantee you're working with actually VRAM. \$\endgroup\$ Commented Mar 22, 2023 at 14:41

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.