Java Swing/AWT, Screen Tearing, and Double Buffering

Started by
6 comments, last by larsbutler 11 years, 8 months ago
Hi there,

I'm working on a 2D platformer game in Java. I'm writing everything from scratch using basic Swing and AWT components.

I'm making good progress on things (collision detection with platforms, level rendering, level scrolling, etc.). However, I've stopped feature development until I can get an issue resolved: jittering/screen tearing.

I've extracted the basic rendering technique from my code base and have been able to (visually) reproduce the error with just a little bit of code.

In my project, I'm using an RK4 integration technique (describe by http://gafferongames...gration-basics/) in the main game loop. For this example, however, I've simplified the update loop to a simple 10 millisecond SwingTimer.

Main.java: https://gist.github.com/3389133
GameWindow.java: https://gist.github.com/3389137
GameCanvas.java: https://gist.github.com/3389138
GameKernel.java: https://gist.github.com/3389139

This example just makes an orange rectangle (the "player") move back and forth across the screen. Even with this simple example, I can see the screen tearing.

I've been reading tons about double buffering techniques (including several forum threads and this series of articles: http://docs.oracle.c.../doublebuf.html). What I've implemented in the code above seems to be the simplest possible way to achieve hardware-accelerated double-buffered rendering. (At least, I'm assuming this taking advantage of hardware acceleration; I'm pretty newb when it comes to graphics programming.) I've tried other more sophisticated approaches suggested in the Java documentation of BufferStrategy (http://docs.oracle.c...erStrategy.html). I've followed this almost to the letter, and even added a call to Toolkit.sync() (a suggestion from a thread I read). Here's basically what I have in my game right now: https://gist.github.com/3389202

Unfortunately, all of this fancy code doesn't seem to affect any change (good or bad), compared to the simple double buffering technique I pasted above. At this point, I'm kind of doing things in "cargo cult" fashion: thrashing around until _something_ works.

I could use some help. Can anyone tell me what I'm doing wrong? Or perhaps the better question to ask first is, can anyone reproduce this screen tearing issue with the code I posted above (so I can rule out the possibility of this just being an issue with my machine)?

Platform info:

Mid 2010 MacBook Pro
OS X 10.7.4
JDK 1.6.0_33
NVIDIA GeForce GT 330M 256mb
1680 x 1050 display

Any tips are appreciated. Cheers!
-Thok
Advertisement
Things to watch out for:

  • Swing Timer: Java timers are notoriously inaccurate and actually do not bear a guarantee beyond "will not fire below a certain threshold". Thus, you still need a traditional gameloop-esque mechanism to account for deltas. Also, I would not use a timer for a game loop and would instead perform the update manually. You can - and is a reasonable idea - to use a Timer as your time base (e.g. per tick update some delta time value and grab that) as that helps mitigate cross-platform timing issues but relying on Java to manage your loop for you is asking for problems, particularly under the discretion of Swing. Furthermore, as mentioned in a couple of research discussions, if your rendering becomes complex the timer thread may get bogged down in events waiting for your render to complete (or, depending on what you're doing, it might just wig out for no reason). Furthermore, it is important to remember that the entire point of a BufferStrategy is for you to manually control the buffers; it doesn' t make a lot of sense to grab that manual control and then immediately hand it back off to Swing. You would be better off just using paint and pumping repaint events.
  • Hardware acceleration: HA is a tricky subject with Java. Realistically, whether or not you get it is at the discretion of the JVM and the system you're using. To my knowledge, the only reliable way to accelerate a drawing context without using VolatileImage directly in Java2D is to use full-screen exclusive mode and remains the only way to do so on older systems, since FSEM reserves the drawing context for your application only. Newer systems shouldn' t have that issue but it still remains a difficult thing to guarantee on an end user.


Things to check:

  • Check the value of the sun.java2d.opengl property. It will likely be null but it's still worth doing so.
  • Check your buffer capabilities and ensure it supports page flipping and has multiple buffers. Grab both the front and back buffer capabilities and ensure they check out as well. E.g.

    • [source lang="java"]// check
      System.out.println("Page flipping: " + strategy.getCapabilities().isPageFlipping());
      System.out.println("Multiple Buffers: " + strategy.getCapabilities().isMultiBufferAvailable());
      System.out.println("FSEM Required: " + strategy.getCapabilities().isFullScreenRequired());
      System.out.println("Backbuffer accelerated: " + strategy.getCapabilities().getBackBufferCapabilities().isAccelerated());
      System.out.println("Frontbuffer accelerated: " + strategy.getCapabilities().getFrontBufferCapabilities().isAccelerated());[/source]
    • As an example, the above on my system returns:

      • True
      • True
      • False
      • True
      • True




Things I would try:

  • Lose the JScrollPane.
  • Ensure your context is accelerated by checking the above. If FSEM returns true then you must enable fullscreen exclusive mode to be accelerated. If any of the others return abnormal results then, unfortunately, there's not a lot you can do to fix that manually other than playing Jeopardy! with various graphics configurations. If sun.java2d.opengl returns false (instead of null) then I would try passing that as a VM argument with the value of true and see if that improves anything.
  • Ditch the timer. Use a while(true) loop and stagger your player updates yourself (use nanoTime()) and don't forget an event listener so you can kill your app. If you do not experience tearing then your problem is with Swing's EDT. I assure you that is a battle you will lose horribly so I would suggest reverting to a more traditional game-loop and moving on.


For what it's worth, this is why a lot of people prefer to use LWJGL or Slick2D over Java2D. You can still use simple draw commands without needing to battle AWT/Swing at the same time.

Edit: Spacing.

Things to watch out for:

  • Swing Timer: Java timers are notoriously inaccurate and actually do not bear a guarantee beyond "will not fire below a certain threshold". Thus, you still need a traditional gameloop-esque mechanism to account for deltas. Also, I would not use a timer for a game loop and would instead perform the update manually. You can - and is a reasonable idea - to use a Timer as your time base (e.g. per tick update some delta time value and grab that) as that helps mitigate cross-platform timing issues but relying on Java to manage your loop for you is asking for problems, particularly under the discretion of Swing. Furthermore, as mentioned in a couple of research discussions, if your rendering becomes complex the timer thread may get bogged down in events waiting for your render to complete (or, depending on what you're doing, it might just wig out for no reason). Furthermore, it is important to remember that the entire point of a BufferStrategy is for you to manually control the buffers; it doesn' t make a lot of sense to grab that manual control and then immediately hand it back off to Swing. You would be better off just using paint and pumping repaint events.
  • Hardware acceleration: HA is a tricky subject with Java. Realistically, whether or not you get it is at the discretion of the JVM and the system you're using. To my knowledge, the only reliable way to accelerate a drawing context without using VolatileImage directly in Java2D is to use full-screen exclusive mode and remains the only way to do so on older systems, since FSEM reserves the drawing context for your application only. Newer systems shouldn' t have that issue but it still remains a difficult thing to guarantee on an end user.



As I mentioned in my post, I'm not _actually_ using a SwingTimer in my game; I just used a SwingTimer for the example. My actual update/render loop is based on RK4 integration and looks something like this: http://pastie.org/4546694


Things to check:

  • Check the value of the sun.java2d.opengl property. It will likely be null but it's still worth doing so.
  • Check your buffer capabilities and ensure it supports page flipping and has multiple buffers. Grab both the front and back buffer capabilities and ensure they check out as well. E.g.

    • [source lang="java"]// check
      System.out.println("Page flipping: " + strategy.getCapabilities().isPageFlipping());
      System.out.println("Multiple Buffers: " + strategy.getCapabilities().isMultiBufferAvailable());
      System.out.println("FSEM Required: " + strategy.getCapabilities().isFullScreenRequired());
      System.out.println("Backbuffer accelerated: " + strategy.getCapabilities().getBackBufferCapabilities().isAccelerated());
      System.out.println("Frontbuffer accelerated: " + strategy.getCapabilities().getFrontBufferCapabilities().isAccelerated());[/source]
    • As an example, the above on my system returns:

      • True
      • True
      • False
      • True
      • True



Things I would try:

  • Lose the JScrollPane.
  • Ensure your context is accelerated by checking the above. If FSEM returns true then you must enable fullscreen exclusive mode to be accelerated. If any of the others return abnormal results then, unfortunately, there's not a lot you can do to fix that manually other than playing Jeopardy! with various graphics configurations. If sun.java2d.opengl returns false (instead of null) then I would try passing that as a VM argument with the value of true and see if that improves anything.
  • Ditch the timer. Use a while(true) loop and stagger your player updates yourself (use nanoTime()) and don't forget an event listener so you can kill your app. If you do not experience tearing then your problem is with Swing's EDT. I assure you that is a battle you will lose horribly so I would suggest reverting to a more traditional game-loop and moving on.


For what it's worth, this is why a lot of people prefer to use LWJGL or Slick2D over Java2D. You can still use simple draw commands without needing to battle AWT/Swing at the same time.

Edit: Spacing.
[/quote]

I hadn't suspected the JScrollPane... embarrassingly enough, that's there because it just worked when I was prototyping and I stuck with it. :o I'll get rid of it.

Again, I'm _not_ using a SwingTimer in my actual game code. So that's not the issue.

Good tip on the graphics capabilities and the opengl property, though. I will check those and post back in a minute.
Okay, here's the result:

Page flipping: false
Multiple Buffers: false
FSEM Required: false
Backbuffer accelerated: true
Frontbuffer accelerated: true
sun.java2d.opengl: null
So, in that case I'll remove the JScrollPane and enable sun.java2d.opengl and see if that makes a difference.

Thanks for your help! I will report my progress in a little while.
Okay, I removed the JScrollPane and enabled sun.java2d.opengl (with the VM arg -Dsun.java2d.opengl=true). Still no love.

I maybe just have to suck it up and try lwjgl. I was hoping that I wouldn't have to bring in 3rd party libs, but it looks like I'll have to at this point.

Thanks again!

Cheers,
-Thok
Okay, here's the result:
Page flipping: false
Multiple Buffers: false
FSEM Required: false
Backbuffer accelerated: true
Frontbuffer accelerated: true
sun.java2d.opengl: null[/quote]
Ah. The lack of page-flipping means you're likely to experience tearing. Not a whole lot you can do about that except try FSEM. Usually a faulty driver/low graphics card issue.

For the Swing timer thing: I apologize, I must have missed where you stated that.

For the JScrollPane issue: in most cases that indicates an initialization error somewhere that the addition of a non-custom component (whose default initialization helped kick Swing into gear, usually by auto-requesting a repaint) solved for you. No big deal.

Okay, here's the result:
Page flipping: false
Multiple Buffers: false
FSEM Required: false
Backbuffer accelerated: true
Frontbuffer accelerated: true
sun.java2d.opengl: null

Ah. The lack of page-flipping means you're likely to experience tearing. Not a whole lot you can do about that except try FSEM. Usually a faulty driver/low graphics card issue.
[/quote]

Interesting point. I actually have tried switching to FSEM; the only difference I saw here with the buffer capabilities was that page flipping was enabled. Multiple buffers was still false.

Given that, it looks like it's just not worth screwing with anymore. I'll take your advice and try out something like LWJGL.


For the Swing timer thing: I apologize, I must have missed where you stated that.
[/quote]

No worries. I probably just didn't make that fact prominent enough.

Thanks,
-Thok

This topic is closed to new replies.

Advertisement