Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pygame Clock inaccurate #304

Open
illume opened this issue Jul 12, 2016 · 18 comments
Open

Pygame Clock inaccurate #304

illume opened this issue Jul 12, 2016 · 18 comments
Labels
bug hard A hard challenge to solve time pygame.time

Comments

@illume
Copy link
Member

illume commented Jul 12, 2016

Originally reported by: Don Polettone (Bitbucket: DonPolettone, GitHub: DonPolettone)


Hi all,

this is something that's been making me mad for months, if not years, and I hope someone can finally fix this:

The clock seems not doing well in pygame; animation stutters / jitters. First I thought I was doing something wrong, but when I digged deeper and broke it down to a test as simple as can be, I recognised it must be an issue with pygame itself.

Try THIS out:

#!python

import sys
import pygame
pygame.init()

RES = (640, 480)
FPS = 60
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)

display = pygame.display.set_mode(RES, pygame.FULLSCREEN)
clock = pygame.time.Clock()

bg_img = pygame.Surface(RES)
bg_img.fill(WHITE)
for y in (229, 290):
    for x in range(40, RES[0], 40):
        bg_img.set_at((x, y), BLUE)

def motion_test():

    rect = pygame.rect.Rect((0, 240), (40, 40))

    while True:

        display.blit(bg_img, (0, 0))
        
        for e in pygame.event.get():
            if (e.type == pygame.QUIT or
                e.type == pygame.KEYDOWN and e.key == pygame.K_ESCAPE):
                pygame.quit()
                sys.exit()
        
        rect.x += 1
        if rect.x >= RES[0]:
            rect.x = 0
        
        pygame.draw.rect(display, BLUE, rect)
        
        pygame.display.flip()
        clock.tick(FPS)

if __name__ == "__main__":
    motion_test()


Every few seconds, the rect stutters a bit. In a pixelated game, this looks awful. Especially when scrolling over pixely tilemaps. I tried it out on both Mac and Win - it is a general issue.

Can someone help/test/agree/decline/fix this ??


Related Docs: https://www.pygame.org/docs/ref/time.html#pygame.time.Clock

@illume
Copy link
Member Author

illume commented Oct 2, 2016

Original comment by Christopher Jones (Bitbucket: shortcipher, GitHub: shortcipher):


I get those same jitters with your script on 64-bit Windows 7. Similar issues with my pygame script are driving me crazy too. Can't find any fix that works by Googling.
Have you thought of raising the issue on stackoverflow?
Chris Jones

@illume
Copy link
Member Author

illume commented Oct 2, 2016

Original comment by Christopher Jones (Bitbucket: shortcipher, GitHub: shortcipher):


I've just found the answer! The pygame.display.flip() doco says "If your display mode is using the flags pygame.HWSURFACE and pygame.DOUBLEBUF, this will wait for a vertical retrace and swap the surfaces". This should stop the display juddering. But putting those flags in pygame.display.set_mode() does nothing on my system, which defaults to using the windib driver. After changing the driver to directx, those flags are honoured, and the rectangle moves smoothly. Add these lines at the start of your script:-

#!python

import os
os.environ['SDL_VIDEODRIVER'] = 'directx'

Add the set_mode flags:-

#!python

display = pygame.display.set_mode(RES, pygame.FULLSCREEN | pygame.HWSURFACE | pygame.DOUBLEBUF)

Remove this line:-

#!python

        clock.tick(FPS)

or change it to:-

#!python

        clock.tick(0)

@illume
Copy link
Member Author

illume commented Oct 13, 2016

Original comment by Don Polettone (Bitbucket: DonPolettone, GitHub: DonPolettone):


Hi Christopher,

thanks a lot for the very interesting hint!

I've tried out both versions:

If I do the changes and remove the line, the program runs extremely fast
(no time delay at all I think).

If I do the changes and amend the line as described below, the program
runs much too fast (around 120 FPS or even more).

(Win 10 Home 64bit)

Additionally, this work-around would not solve the issue, because I do
not know what hardware my games will be running on, and so I would never
know when V-TRACE would happen. And as I use an FPS-based, fix
time-step, the game would run on different speeds, depending on the
hardware.

I got soooo tired of this clock issue recently, so I coded my own
python-based clock for Win, Mac and Linux now. To make the clock work on
Win, I had to mess around with the Windows API a bit to tighten up the
OS' time resolution. Some lines of python-C synthax were necessary to
achieve this, and thanks to some very helpful people on stackoverflow, I
got it working now.

My own clock is pretty much as accurate as Pyglet's, but has much less
code. But don't get me wrong, compared to the guy that writes the pyglet
engine I am nothing but a small, helpless worm. I tried to "extract" the
clock from pyglet first and use Pygame for all the rest, but it was a
mess, and I did not like it (mixing 2 engines). After realising this, I
gave it one more try and started to mess around coding my own clock
(again). It was really Windows' time granularity of about 16 ms that
prevented me from achieving this goal, but now I finally got it.

I still believe that
fixing Pygame's clock (for Win users) should be considered.

@illume
Copy link
Member Author

illume commented Oct 13, 2016

Original comment by Christopher Jones (Bitbucket: shortcipher, GitHub: shortcipher):


So my fix evidently doesn't work on all systems: on both of my PCs (Windows
7 and Windows 10) it syncs the frame rate to the monitor's screen refresh
rate. If your clock manages to achieve 60 fps by setting 16.666 ms between
display flips, won't your game still jitter on monitors with refresh rate
not a multiple of 60 fps? Any chance of posting your motion test example
with your clock code included?

@illume
Copy link
Member Author

illume commented Oct 15, 2016

Original comment by Christopher Jones (Bitbucket: shortcipher, GitHub: shortcipher):


With your clock module, the motion test still jitters on my system. Raising
the process priority didn't help. The VSYNC tester at
https://www.vsynctester.com/index.html indicated that my monitor was
running at 59.940, not 60 fps, but initializing Clock() with this value
didn't help either.

@illume
Copy link
Member Author

illume commented Oct 20, 2016

Original comment by Don Polettone (Bitbucket: DonPolettone, GitHub: DonPolettone):


Hi again,

What exactly do you mean with "it jitters"? Is it like when limitting
pygame to 60 FPS (ca. one "hickup" per second)? Or does it continously
jitter around with no pattern?

I did lots of testings on both my machines (fast desktop and rather slow
notebook, both Win10 64bit Home). The results were most interesting. I
got some code running in which I can switch the clock that's used during
execution, and it runs this same rect test, limitting each clock to 60
FPS. The 3 clocks are pygame's, pyglet's, and mine.

The pyglet clock behaves much like my clock, on both systems: The rect
sometimes jitters for a tiny fraction of a second, but mostly it moves
smoothly. With pygame, the rect like stops/jitters in a clear interval
of like once per second. I really think the pygame clock does not work
properly (at least on Windows (10(64bit))).

So I am stuck with either pyglet's clock or mine, but both seem to have
the same issue. Sometimes, with no clear time interval, there's
something happening which produces a slight jittering. At first I
thought it must be the OS interrupting the process in some way, draining
ressources. So I raised the process priority with psutil, but still, the
jittering sometimes happens (it's the same). Then I thought it could be
python's garbage collection that causes the issue, so I made my clock
turn it off before the timing starts and switch it on again afterwards,
but that did not help either.

I have included some code in my clock to see whether there's any frame
where the clock would have no wait time at all, and throw an error if
this happens. It does not happen when the jittering occurs, I got no
error. So my clock has plenty of time to wait each frame (about 14-15ms
per frame), and it works fine.

Also I think the problem is not related to the display refresh rate,
because if it would be, you'd notice some logical interval at which the
jittering happens. If I run the VSYNC test it gives me about 59.998 on
my desktop and 60.05 on the notebook, and the program behaves quite
equally on both systems.

Is there some way to monitor what exactly happens in Windows while my
program is running?

And what is your suggestion? How can I code an NES-like program with
python using a fix time step (60 FPS, PAL) which runs 100% accurate? Is
this even possible?

@illume
Copy link
Member Author

illume commented Oct 21, 2016

Original comment by Christopher Jones (Bitbucket: shortcipher, GitHub: shortcipher):


Movement is mostly smooth, but with occasional jitters with no apparent pattern. Interestingly, if I just change the driver to directx with

#!python

os.environ['SDL_VIDEODRIVER'] = 'directx'

I get tearing of the blue rectangle, implying that pygame.display.flip() is writing to the frame buffer at the same time as it is being read out to the display. This doesn't happen with the default windib driver, so there must be some double-buffering and/or locking to prevent it. So although your clock accurately controls the frequency of the pygame.display.flip() calls, asynchronous processing inside this function may result in the actual frame buffer update happening slightly earlier or later on some frames. I still believe, therefore, that a 100% accurate solution must entail sychronisation to vertical retrace. I'm surprised that the fix I provided with pygame.HWSURFACE, pygame.DOUBLEBUF, and the directx driver didn't work for you. Could you check with

#!python

print(hex(display.get_flags() & 0xFFFFFFFF))

that the display flags have been accepted - should be 0xc0000001.

@illume
Copy link
Member Author

illume commented Oct 21, 2016

Original comment by Don Polettone (Bitbucket: DonPolettone, GitHub: DonPolettone):


Hey again!

I don't believe it... it worked?! It gave me 0xc0000001L (don't know
what the L is for), but also a 100% smooth movement!

I think I must have done something wrong in the 1st attempt, sorry.
You're awesome. Thank you.

My only concern: How can we check the refresh rate of any connected
monitor? I googled a bit but I think with pygame itself that's not
possible?

But it's awesome! That's the 1st time I see real smooth motion in
pygame.

Thanks man

@illume
Copy link
Member Author

illume commented Oct 22, 2016

Original comment by Christopher Jones (Bitbucket: shortcipher, GitHub: shortcipher):


Glad it's working now!
You could get the nominal refresh rate outside of pygame by

#!python

import win32api
fps = win32api.EnumDisplaySettings().DisplayFrequency

but on my system that gives 59 not 60 fps.
Would be better to get the actual frequency using clock.get_fps(), where clock = pygame.time.Clock() and you have a clock.tick() in your game loop: that will give the fps computed by averaging the last ten calls to Clock.tick().

@illume
Copy link
Member Author

illume commented Oct 23, 2016

Original comment by Don Polettone (Bitbucket: DonPolettone, GitHub: DonPolettone):


Did many testings and once again, new issues popped up:

I did a blitting benchmark which stops the time it needs to blit 1000
sprites per frame and blits that time onto the screen.

It seems that using the HWSURFACE + DOUBLEBUF flags results in a much
higher blitting time, and it's much more inconsistent as well.
Especially when dealing with per pixel alpha images.

Using the ordinary "display = pygame.display.set_mode(RES,
pygame.FULLSCREEN)", it takes me 1-2 milsec to blit 1000 .png images
loaded with either convert() OR convert_alpha(). If I use convert() and
set_colorkey(), I get 2-6 milsecs (inconsistent).

If I use a HWSURFACE + DOUBLEBUF, the exact same code takes 35 - 1215
(!) milsecs for convert_alpha() png's, and 2-6 milsecs for convert()
only / convert() + set_colorkey() png's. That's way off limits.

(I did all these benchmarks using the directx SDL vid driver.)

Alas, using .jpg's with a colorkey doesn't work either because pygame
changes the image's colours in a weird way if they're loaded from disk.

Conclusion:

If I don't use VSYNC I can blit as much per pixel transparent png's as I
wish in 1-2 ms. That's enough for a NES-ish 2D bullet hell game. But
it's not accurate (jittering problem).

If I use VSYNC, I get smooth animation, which is a must-have for such a
game, but the blitting times are way too high and inconsistent, which is
a complete showstopper.

It seems my journey goes on... dammit! Have you experienced the same
issues with VSYNC?

This stuff is really making me mad again.... I think I should really
consider trying another framework for game dev purposes (else than
pygame).

@illume
Copy link
Member Author

illume commented Oct 23, 2016

Original comment by Christopher Jones (Bitbucket: shortcipher, GitHub: shortcipher):


Actually, you can get smooth movement with just the DOUBLEBUF flag and not the HWSURFACE flag. You could try benchmarking your sprite blits without HWSURFACE to see if it makes a difference.
I am just blitting one 1920 x 1200 full-screen image, and that is fitting OK with the 60 fps frame rate.

@illume
Copy link
Member Author

illume commented Oct 24, 2016

Original comment by Christopher Jones (Bitbucket: shortcipher, GitHub: shortcipher):


I have reproduced your excessive blit timings for png images with alpha transparency. Removing HWSURFACE didn't help. A workaround which gives acceptable blit timings is to blit the images on to a non-transparent background image and then blit that to the display, along these lines:-

#!python

bg = bg_img.copy()
bg.blit(alphapng, (50, 50))
display.blit(bg, (0, 0))

@illume
Copy link
Member Author

illume commented Oct 24, 2016

Original comment by Don Polettone (Bitbucket: DonPolettone, GitHub: DonPolettone):


Yes, it seems that's the only solution on Windows to get smooth motion in Pygame (so far).

I have found out that it's the pygame.DOUBLEBUF flag that sets VSYNC to True, HWSURFACE is not needed. But it only works if you set the directx driver first, so that really did the trick.

But still, it's all very quirky on windows with pygame.. I would prefer a non-jittering, non-VSYNC, and fast standard setup for pygame on windows without having to care about display flags and all that stuff.

Is there no way to get smooth motion using the std windib driver?

@MyreMylar
Copy link
Contributor

I was tinkering with this today and observed the same thing as described above - it seems the only way to get completely smooth screen scrolling/movement in pygame 1.9 is to trick the system into enabling vsynch to control the framerate. All the other combinations I've tried using the pygame clock you get a slight hitching every second or so.

However, this hacky method is no longer available in pygame 2 as the:

os.environ['SDL_VIDEODRIVER'] = 'directx'

...line is no longer available in SDL2 (the only available videodriver option on windows is now 'windows' see here: https://wiki.libsdl.org/FAQUsingSDL).

So in pygame 2 we now have the worst of both worlds, the same, seemingly pygame clock caused hitching, and the hacky way to bypass it by enabling vsynch is no longer available.

@robertpfeiffer
Copy link
Contributor

@MyreMylar Did you disable the clock and use vsync only?

@MyreMylar
Copy link
Contributor

@robertpfeiffer To get smooth scrolling in pygame 1 on windows?

Sort of, from memory you need to stop limiting the frame rate with clock.tick(60) or whatever and let the v sync take care of doing that instead - but you don't need to remove the clock entirely. You can still run clock.tick() with no FPS limit argument and calculate the current framerate.

@robertpfeiffer
Copy link
Contributor

robertpfeiffer commented Oct 31, 2019

Oh, yeah. Clock.tick(60) should not wait at all when vsync is on and the vsync frame rate is 50 Hz. But then the game only runs at 50 Hz instead of 60. On the other hand, if the screen refresh rate is 50 Hz and you have clock.tick(30) in there somewhere, you might get additional stutter from clock.tick. I think modern game engines solve this by only locking the frame rate at 144fps, and parameterising the update step with the elapsed time, or by having the game logic that needs a fixed time step run at 30/60FPS in its own thread with separate threads for UI/input, game logic, rendering, and networking.

We could enable vsync again, but we'd need to keep it turned off by default for backwards compatibility. (Edit: Compatibility with old pygame games, not old hardware or SDL versions I mean)

@MyreMylar
Copy link
Contributor

MyreMylar commented Oct 31, 2019 via email

@MyreMylar MyreMylar added hard A hard challenge to solve and removed 1.9.1 major labels May 16, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug hard A hard challenge to solve time pygame.time
Projects
None yet
Development

No branches or pull requests

3 participants