diff --git a/kivy/_clock.pxd b/kivy/_clock.pxd index 7408c49cb..6684d415f 100644 --- a/kivy/_clock.pxd +++ b/kivy/_clock.pxd @@ -29,6 +29,13 @@ cdef class ClockEvent(object): cpdef tick(self, double curtime) +cdef class FreeClockEvent(ClockEvent): + + cdef public int free + '''Whether this event was scheduled as a free event. + ''' + + cdef class CyClockBase(object): cdef public double _last_tick @@ -37,6 +44,16 @@ cdef class CyClockBase(object): frame. If more iterations occur, a warning is issued. ''' + cdef public double clock_resolution + '''If the remaining time until the event timeout is less than :attr:`clock_resolution`, + the clock will execute the callback even if it hasn't exactly timed out. + + If -1, the default, the resolution will be computed from config's ``maxfps``. + Otherwise, the provided value is used. Defaults to -1. + ''' + + cdef public double _max_fps + cdef public ClockEvent _root_event '''The first event in the chain. Can be None. ''' @@ -65,6 +82,7 @@ cdef class CyClockBase(object): cdef public object _lock_acquire cdef public object _lock_release + cpdef get_resolution(self) cpdef create_trigger(self, callback, timeout=*, interval=*) cpdef schedule_once(self, callback, timeout=*) cpdef schedule_interval(self, callback, timeout) @@ -72,3 +90,14 @@ cdef class CyClockBase(object): cpdef _release_references(self) cpdef _process_events(self) cpdef _process_events_before_frame(self) + cpdef get_min_timeout(self) + cpdef get_events(self) + + +cdef class CyClockBaseFree(CyClockBase): + + cpdef create_trigger_free(self, callback, timeout=*, interval=*) + cpdef schedule_once_free(self, callback, timeout=*) + cpdef schedule_interval_free(self, callback, timeout) + cpdef _process_free_events(self, double last_tick) + cpdef get_min_free_timeout(self) diff --git a/kivy/_clock.pyx b/kivy/_clock.pyx index 3dc499127..853ec8584 100644 --- a/kivy/_clock.pyx +++ b/kivy/_clock.pyx @@ -1,6 +1,10 @@ -__all__ = ('CyClockBase', 'ClockEvent') +__all__ = ('ClockEvent', 'CyClockBase', 'FreeClockEvent', 'CyClockBaseFree') + + +cdef extern from "float.h": + double DBL_MAX from kivy.weakmethod import WeakMethod from kivy.logger import Logger @@ -47,6 +51,7 @@ cdef class ClockEvent(object): clock._last_event.next = self self.prev = clock._last_event clock._last_event = self + self.clock.on_schedule(self) clock._lock_release() def __call__(self, *largs): @@ -65,6 +70,7 @@ cdef class ClockEvent(object): self.clock._last_event.next = self self.prev = self.clock._last_event self.clock._last_event = self + self.clock.on_schedule(self) self.clock._lock_release() return True self.clock._lock_release() @@ -102,7 +108,7 @@ cdef class ClockEvent(object): # cap is next, so we have reached the end of the list # because current one being processed is going to be the last event now if self.clock._cap_event is self.clock._next_event: - self.clock._cap_event = None + self.clock._cap_event = None else: self.clock._cap_event = self.prev # new cap @@ -137,10 +143,8 @@ cdef class ClockEvent(object): '''(internal method) Processes the event for the kivy thread. ''' cdef object callback, ret - # timeout happened ? (check also if we would miss from 5ms) this - # 5ms increase the accuracy if the timing of animation for - # example. - if curtime - self._last_dt < self.timeout - 0.005: + # timeout happened ? if less than resolution process it + if curtime - self._last_dt < self.timeout - self.clock.get_resolution(): return True # calculate current timediff for this event @@ -170,13 +174,29 @@ cdef class ClockEvent(object): return self.loop def __repr__(self): - return '' % self.get_callback() + return ''.format(self.timeout, self.get_callback()) + + +cdef class FreeClockEvent(ClockEvent): + '''The event returned by the ``Clock.XXX_free`` methods of + :class:`CyClockBaseFree`. It stores whether the event was scheduled as a + free event. + ''' + + def __init__(self, free, *largs, **kwargs): + self.free = free + ClockEvent.__init__(self, *largs, **kwargs) cdef class CyClockBase(object): '''The base clock object with event support. ''' + def __cinit__(self, **kwargs): + self.clock_resolution = -1 + self._max_fps = 60 + self.max_iteration = 10 + def __init__(self, **kwargs): super(CyClockBase, self).__init__(**kwargs) self._root_event = None @@ -187,7 +207,26 @@ cdef class CyClockBase(object): self._lock_acquire = self._lock.acquire self._lock_release = self._lock.release - self.max_iteration = 10 + cpdef get_resolution(self): + '''Returns the minimum resolution the clock has. It's a function of + :attr:`clock_resolution` and ``maxfps`` provided at the config. + ''' + cdef double resolution = self.clock_resolution + # timeout happened ? (check also if we would miss from 5ms) this + # 5ms increase the accuracy if the timing of animation for + # example. + if resolution < 0: + if self._max_fps: + resolution = 1 / (3. * self._max_fps) + else: + resolution = 0.0001 + return resolution + + def on_schedule(self, event): + '''Function that is called internally every time an event is triggered + for this clock. It takes the event as a parameter. + ''' + pass cpdef create_trigger(self, callback, timeout=0, interval=False): '''Create a Trigger event. Check module documentation for more @@ -337,10 +376,10 @@ cdef class CyClockBase(object): self._cap_event = self._last_event event = self._root_event - while not done: + while not done and event is not None: self._next_event = event.next done = self._cap_event is event or self._cap_event is None - '''We have to worry about this case: + '''Usage of _cap_event: We have to worry about this case: If in this iteration the cap event is canceled then at end of this iteration _cap_event will have shifted to current event (or to the @@ -361,7 +400,7 @@ cdef class CyClockBase(object): self._lock_acquire() event = self._next_event - self._cap_event = None + self._next_event = self._cap_event = None self._lock_release() cpdef _process_events_before_frame(self): @@ -390,7 +429,7 @@ cdef class CyClockBase(object): self._cap_event = self._last_event event = self._root_event - while not done: + while not done and event is not None: if event.timeout != -1: done = self._cap_event is event or self._cap_event is None event = event.next @@ -408,11 +447,29 @@ cdef class CyClockBase(object): else: self._lock_acquire() event = self._next_event - self._cap_event = None + self._next_event = self._cap_event = None self._lock_release() - def get_events(self): - '''Returns the list of :class:`ClockEvent` currently scheduled. + cpdef get_min_timeout(self): + '''Returns the remaining time since the start of the current frame + for the event with the smallest timeout. + ''' + cdef ClockEvent ev + cdef double val = DBL_MAX + self._lock_acquire() + ev = self._root_event + while ev is not None: + if ev.timeout <= 0: + val = 0 + break + val = min(val, ev.timeout + ev._last_dt) + ev = ev.next + self._lock_release() + + return val + + cpdef get_events(self): + '''Returns the list of :class:`ClockEvent` instances currently scheduled. ''' cdef list events = [] cdef ClockEvent ev @@ -424,3 +481,128 @@ cdef class CyClockBase(object): ev = ev.next self._lock_release() return events + + +cdef class CyClockBaseFree(CyClockBase): + '''A clock class that supports scheduling free events in addition to normal + events. + + Each of the :meth:`~CyClockBase.create_trigger`, + :meth:`~CyClockBase.schedule_once`, and :meth:`~CyClockBase.schedule_interval` + methods, which create a normal event, have a corresponding method + for creating a free event. + ''' + + cpdef create_trigger(self, callback, timeout=0, interval=False): + cdef FreeClockEvent event + if not callable(callback): + raise ValueError('callback must be a callable, got %s' % callback) + event = FreeClockEvent(False, self, interval, callback, timeout, 0) + event.release() + return event + + cpdef schedule_once(self, callback, timeout=0): + cdef FreeClockEvent event + if not callable(callback): + raise ValueError('callback must be a callable, got %s' % callback) + + event = FreeClockEvent( + False, self, False, callback, timeout, self._last_tick, None, True) + return event + + cpdef schedule_interval(self, callback, timeout): + cdef FreeClockEvent event + if not callable(callback): + raise ValueError('callback must be a callable, got %s' % callback) + + event = FreeClockEvent( + False, self, True, callback, timeout, self._last_tick, None, True) + return event + + cpdef create_trigger_free(self, callback, timeout=0, interval=False): + '''Similar to :meth:`~CyClockBase.create_trigger`, but instead creates + a free event. + ''' + cdef FreeClockEvent event + if not callable(callback): + raise ValueError('callback must be a callable, got %s' % callback) + event = FreeClockEvent(True, self, interval, callback, timeout, 0) + event.release() + return event + + cpdef schedule_once_free(self, callback, timeout=0): + '''Similar to :meth:`~CyClockBase.schedule_once`, but instead creates + a free event. + ''' + cdef FreeClockEvent event + if not callable(callback): + raise ValueError('callback must be a callable, got %s' % callback) + + event = FreeClockEvent( + True, self, False, callback, timeout, self._last_tick, None, True) + return event + + cpdef schedule_interval_free(self, callback, timeout): + '''Similar to :meth:`~CyClockBase.schedule_interval`, but instead creates + a free event. + ''' + cdef FreeClockEvent event + if not callable(callback): + raise ValueError('callback must be a callable, got %s' % callback) + + event = FreeClockEvent( + True, self, True, callback, timeout, self._last_tick, None, True) + return event + + cpdef _process_free_events(self, double last_tick): + cdef FreeClockEvent event + cdef int done = False + + self._lock_acquire() + if self._root_event is None: + self._lock_release() + return + + self._cap_event = self._last_event + event = self._root_event + while not done and event is not None: + if not event.free: + done = self._cap_event is event or self._cap_event is None + event = event.next + continue + + self._next_event = event.next + done = self._cap_event is event or self._cap_event is None + + self._lock_release() + + try: + event.tick(last_tick) + except: + raise + else: + self._lock_acquire() + event = self._next_event + + self._next_event = self._cap_event = None + self._lock_release() + + cpdef get_min_free_timeout(self): + '''Returns the remaining time since the start of the current frame + for the *free* event with the smallest timeout. + ''' + cdef FreeClockEvent ev + cdef double val = DBL_MAX + + self._lock_acquire() + ev = self._root_event + while ev is not None: + if ev.free: + if ev.timeout <= 0: + val = 0 + break + val = min(val, ev.timeout + ev._last_dt) + ev = ev.next + self._lock_release() + + return val diff --git a/kivy/clock.py b/kivy/clock.py index ba219e1d4..4a47bd7c3 100644 --- a/kivy/clock.py +++ b/kivy/clock.py @@ -217,9 +217,139 @@ As a a consequence, calling :meth:`CyClockBase.unschedule` with the original callback is now significantly slower and highly discouraged. Instead, the returned events should be used to cancel. As a tradeoff, all the other methods are now significantly faster than before. + +Advanced Clock Details +----------------------- + +The following section goes into the internal kivy clock details as well +as the various clock options. It is meant only for advanced users. + +Fundamentally, the Kivy clock attempts to execute any scheduled callback +rhythmically as determined by the specified fps (frame per second, see +``maxfps`` in :mod:`~kivy.config`). That is, ideally, given e.g. a desired fps +of 30, the clock will execute the callbacks at intervals of 1 / 30 seconds, or +every 33.33 ms. All the callbacks in a frame are given the same timestamp, +i.e. the ``dt`` passed to the callback are all the same and it's the difference +in time between the start of this and the previous frame. + +Because of inherent indeterminism, the frames do not actually occur exactly +at intervals of the fps and ``dt`` may be under or over the desired fps. +Also, once the timeout is "close enough" to the desired timeout, as determined +internally, Kivy will execute the callback in the current frame even when the +"actual time" has not elapsed the ``timeout`` amount. + +Kivy offers now, since ``1.9.2``, multiple clocks with different behaviors. + +Default Clock +^^^^^^^^^^^^^^ + +The default clock (``default``) behaves as described above. When a callback +with a timeout of zero or non-zero is scheduled, they are executed at the frame +that is near the timeout, which is a function of the fps. So a timeout of zero +would still result in a delay of one frame or about 1 / fps, typically a bit +less but sometimes more depending on the CPU usage of the other events +scheduled for that frame. + +In a test using a fps of 30, a callback with a timeout of 0, 0.001, and 0.05, +resulted in a mean callback delay of 0.02487, 0.02488, and 0.05011 seconds, +respectively. When tested with a fps of 600 the delay for 0.05 was similar, +except the standard deviation was reduced resulting in overall better accuracy. + +Interruptible Clock +^^^^^^^^^^^^^^^^^^^^ + +The default clock suffers from the quantization problem, as frames occur only +on intervals and any scheduled timeouts will not be able to occur during an +interval. For example, with the timeout of 0.05, while the mean was 0.05011, +its values ranged between 0.02548 - 0.07348 and a standard deviation of 0.002. +Also, there's the minimum timeout of about 0.02487. + +The interruptible clock (``interrupt``) will execute timeouts even during a +frame. So a timeout of zero will execute as quickly as possible and similarly +a non-zero timeout will be executed even during the interval. + +This clock, and all the clocks described after this have an option, +:attr:`ClockBaseInterruptBehavior.interupt_next_only`. When True, any of the +behavior new behavior will only apply to the callbacks with a timeout of +zero. Non-zero timeouts will behave like in the default clock. E.g. for this +clock when True, only zero timeouts will execute during the the interval. + +In a test using a fps of 30, a callback with a timeout of 0, 0.001, and 0.05, +resulted in a mean callback delay of 0.00013, 0.00013, and 0.04120 seconds, +respectively when :attr:`ClockBaseInterruptBehavior.interupt_next_only` was +False. Also, compared to the default clock the standard deviation was reduced. +When :attr:`ClockBaseInterruptBehavior.interupt_next_only` was True, the values +were 0.00010, 0.02414, and 0.05034, respectively. + +Free Clock +^^^^^^^^^^^ + +The interruptible clock may not be ideal for all cases because all the events +are executed during the intervals and events are not executed anymore +rhythmically as multiples of the fps. For example, there may not be any benefit +for the graphics to update in a sub-interval, so the additional accuracy +wastes CPU. + +The Free clock (``free_all``) solves this by having ``Clock.xxx_free`` versions +of all the Clock scheduling methods. By free, we mean the event is free from +the fps because it's not fps limited. E.g. +:meth:`CyClockBaseFree.create_trigger_free` corresponds to +:meth:`CyClockBase.create_trigger`. Only when an event scheduled using the +``Clock.xxx_free`` methods is present will the clock interrupt and execute +the events during the interval. So, if no ``free`` event is present the clock +behaves like the ``default`` clock, otherwise it behaves like the ``interrupt`` +clock. + +In a test using a fps of 30, a callback with a timeout of 0s, 0.001s, and +0.05s, resulted in a mean callback delay of 0.00012s, 0.00017s, and 0.04121s +seconds, respectively when it was a free event and 0.02403s, 0.02405s, and +0.04829s, respectively when it wasn't. + +Free Only Clock +^^^^^^^^^^^^^^^^^ + +The Free clock executes all events when a free event was scheduled. This +results in normal events also being execute in the middle of the interval +when a free event is scheduled. For example, above, when a free event was +absent, a normal event with a 0.001s timeout was delayed for 0.02405s. However, +if a free event happened to be also scheduled, the normal event was only +delayed 0.00014s, which may be undesirable. + +The Free only clock (``free_only``) solves it by only executing free events +during the interval and normal events are always executed like with the +default clock. For example, in the presence of a free event, a normal event +with a timeout of 0.001s still had a delay of 0.02406. So this clock, +treats free and normal events independently, with normal events always being +fps limited, but never the free events. + +Summary +^^^^^^^^ + +The kivy clock type to use can be set with the ``kivy_clock`` option the +:mod:`~kivy.config`. If ``KIVY_CLOCK`` is present in the environment it +overwrites the config selection. Its possible values are as follows: + +* When ``kivy_clock`` is ``default``, the normal clock, :class:`ClockBase`, + which limits callbacks to the maxfps quantization - is used. +* When ``kivy_clock`` is ``interrupt``, a interruptible clock, + :class:`ClockBaseInterrupt`, which doesn't limit any callbacks to the + maxfps - is used. Callbacks will be executed at any time. +* When ``kivy_clock`` is ``free_all``, a interruptible clock, + :class:`ClockBaseFreeInterruptAll`, which doesn't limit any callbacks to the + maxfps in the presence of free events, but in their absence it limits events + to the fps quantization interval - is used. +* When ``kivy_clock`` is ``free_only``, a interruptible clock, + :class:`ClockBaseFreeInterruptAll`, which treats free and normal events + independently; normal events are fps limited while free events are not - is + used. + ''' -__all__ = ('Clock', 'CyClockBase', 'ClockBase', 'ClockEvent', 'mainthread') +__all__ = ( + 'Clock', 'ClockEvent', 'FreeClockEvent', 'CyClockBase', 'CyClockBaseFree', + 'ClockBaseBehavior', 'ClockBaseInterruptBehavior', + 'ClockBaseInterruptFreeBehavior', 'ClockBase', 'ClockBaseInterrupt', + 'ClockBaseFreeInterruptAll', 'ClockBaseFreeInterruptOnly', 'mainthread') from sys import platform from os import environ @@ -228,10 +358,18 @@ from kivy.context import register_context from kivy.weakmethod import WeakMethod from kivy.config import Config from kivy.logger import Logger -from kivy.compat import clock as _default_time +from kivy.compat import clock as _default_time, PY2 import time from threading import Lock -from kivy._clock import CyClockBase, ClockEvent +from kivy._clock import CyClockBase, ClockEvent, FreeClockEvent, \ + CyClockBaseFree +try: + from multiprocessing import Event as MultiprocessingEvent +except ImportError: # https://bugs.python.org/issue3770 + from threading import Event as MultiprocessingEvent +from threading import Event as ThreadingEvent + +# some reading: http://gameprogrammingpatterns.com/game-loop.html def _get_sleep_obj(): @@ -317,8 +455,8 @@ except (OSError, ImportError, AttributeError): time.sleep(microseconds / 1000000.) -class ClockBase(CyClockBase): - '''A clock object with event support. +class ClockBaseBehavior(object): + '''The base of the kivy clock. ''' _dt = 0.0001 @@ -330,16 +468,25 @@ class ClockBase(CyClockBase): _rfps_counter = 0 _frames = 0 _frames_displayed = 0 - _max_fps = 30. - _sleep_obj = None + _events_duration = 0 + '''The measured time that it takes to process all the events etc, excepting + any sleep or waiting time. It is the average and is updated every 5 + seconds. + ''' + + _duration_count = 0 + _sleep_time = 0 + _duration_ts0 = 0 MIN_SLEEP = 0.005 + '''The minimum time to sleep. If the remaining time is less than this, + the event loop will continuo + ''' SLEEP_UNDERSHOOT = MIN_SLEEP - 0.001 - def __init__(self): - super(ClockBase, self).__init__() - self._sleep_obj = _get_sleep_obj() - self._start_tick = self._last_tick = self.time() + def __init__(self, **kwargs): + super(ClockBaseBehavior, self).__init__(**kwargs) + self._duration_ts0 = self._start_tick = self._last_tick = self.time() self._max_fps = float(Config.getint('graphics', 'maxfps')) @property @@ -369,7 +516,31 @@ class ClockBase(CyClockBase): def usleep(self, microseconds): '''Sleeps for the number of microseconds. ''' - _usleep(microseconds, self._sleep_obj) + pass + + def idle(self): + '''(internal) waits here until the next frame. + ''' + fps = self._max_fps + if fps > 0: + min_sleep = self.get_resolution() + undershoot = 4 / 5. * min_sleep + usleep = self.usleep + ready = self._check_ready + + done, sleeptime = ready(fps, min_sleep, undershoot) + while not done: + usleep(1000000 * sleeptime) + done, sleeptime = ready(fps, min_sleep, undershoot) + + current = self.time() + self._dt = current - self._last_tick + self._last_tick = current + return current + + def _check_ready(self, fps, min_sleep, undershoot): + sleeptime = 1 / fps - (self.time() - self._last_tick) + return sleeptime - undershoot <= min_sleep, sleeptime - undershoot def tick(self): '''Advance the clock to the next step. Must be called every frame. @@ -378,24 +549,22 @@ class ClockBase(CyClockBase): self._release_references() - # do we need to sleep ? - if self._max_fps > 0: - min_sleep = self.MIN_SLEEP - sleep_undershoot = self.SLEEP_UNDERSHOOT - fps = self._max_fps - usleep = self.usleep - - sleeptime = 1 / fps - (self.time() - self._last_tick) - while sleeptime - sleep_undershoot > min_sleep: - usleep(1000000 * (sleeptime - sleep_undershoot)) - sleeptime = 1 / fps - (self.time() - self._last_tick) + ts = self.time() + current = self.idle() # tick the current time - current = self.time() - self._dt = current - self._last_tick self._frames += 1 self._fps_counter += 1 - self._last_tick = current + + # compute how long the event processing takes + self._duration_count += 1 + self._sleep_time += current - ts + t_tot = current - self._duration_ts0 + if t_tot >= 1.: + self._events_duration = \ + (t_tot - self._sleep_time) / float(self._duration_count) + self._duration_ts0 = current + self._sleep_time = self._duration_count = 0 # calculate fps things if self._last_fps_tick is None: @@ -444,7 +613,165 @@ class ClockBase(CyClockBase): time = staticmethod(partial(_default_time)) -ClockBase.time.__doc__ = '''Proxy method for :func:`~kivy.compat.clock`. ''' +ClockBaseBehavior.time.__doc__ = '''Proxy method for :func:`~kivy.compat.clock`. ''' + + +class ClockBaseInterruptBehavior(ClockBaseBehavior): + '''A kivy clock which can be interrupted during a frame to execute events. + ''' + + interupt_next_only = False + _event = None + _get_min_timeout_func = None + + def __init__(self, interupt_next_only=False, **kwargs): + super(ClockBaseInterruptBehavior, self).__init__(**kwargs) + self._event = MultiprocessingEvent() if PY2 else ThreadingEvent() + self.interupt_next_only = interupt_next_only + self._get_min_timeout_func = self.get_min_timeout + + def usleep(self, microseconds): + self._event.clear() + self._event.wait(microseconds / 1000000.) + + def on_schedule(self, event): + fps = self._max_fps + if not fps: + return + + if not event.timeout or ( + not self.interupt_next_only and event.timeout + <= 1 / fps # remaining time + - (self.time() - self._last_tick) # elapsed time + + 4 / 5. * self.get_resolution()): # resolution fudge factor + self._event.set() + + def idle(self): + fps = self._max_fps + event = self._event + resolution = self.get_resolution() + if fps > 0: + done, sleeptime = self._check_ready( + fps, resolution, 4 / 5. * resolution) + if not done: + event.wait(sleeptime) + + current = self.time() + self._dt = current - self._last_tick + self._last_tick = current + event.clear() + # anything scheduled from now on, if scheduled for the upcoming frame + # will cause a timeout of the event on the next idle due to on_schedule + # `self._last_tick = current` must happen before clear, otherwise the + # on_schedule computation is wrong when exec between the clear and + # the `self._last_tick = current` bytecode. + return current + + def _check_ready(self, fps, min_sleep, undershoot): + if self._event.is_set(): + return True, 0 + + t = self._get_min_timeout_func() + if not t: + return True, 0 + + if not self.interupt_next_only: + curr_t = self.time() + sleeptime = min(1 / fps - (curr_t - self._last_tick), t - curr_t) + else: + sleeptime = 1 / fps - (self.time() - self._last_tick) + return sleeptime - undershoot <= min_sleep, sleeptime - undershoot + + +class ClockBaseInterruptFreeBehavior(ClockBaseInterruptBehavior): + '''A base class for the clock that interrupts the sleep interval for + free events. + ''' + + def __init__(self, **kwargs): + super(ClockBaseInterruptFreeBehavior, self).__init__(**kwargs) + self._get_min_timeout_func = self.get_min_free_timeout + + def on_schedule(self, event): + if not event.free: # only wake up for free events + return + # free events should use real time not frame time + event._last_dt = self.time() + return super(ClockBaseInterruptFreeBehavior, + self).on_schedule(event) + + +class ClockBase(ClockBaseBehavior, CyClockBase): + '''The ``default`` kivy clock. See module for details. + ''' + + _sleep_obj = None + + def __init__(self, **kwargs): + super(ClockBase, self).__init__(**kwargs) + self._sleep_obj = _get_sleep_obj() + + def usleep(self, microseconds): + _usleep(microseconds, self._sleep_obj) + + +class ClockBaseInterrupt(ClockBaseInterruptBehavior, CyClockBase): + '''The ``interrupt`` kivy clock. See module for details. + ''' + + pass + + +class ClockBaseFreeInterruptAll( + ClockBaseInterruptFreeBehavior, CyClockBaseFree): + '''The ``free_all`` kivy clock. See module for details. + ''' + + pass + + +class ClockBaseFreeInterruptOnly( + ClockBaseInterruptFreeBehavior, CyClockBaseFree): + '''The ``free_only`` kivy clock. See module for details. + ''' + + def idle(self): + fps = self._max_fps + if fps > 0: + event = self._event + min_sleep = self.get_resolution() + usleep = self.usleep + undershoot = 4 / 5. * min_sleep + min_t = self.get_min_free_timeout + interupt_next_only = self.interupt_next_only + + current = self.time() + sleeptime = 1 / fps - (current - self._last_tick) + while sleeptime - undershoot > min_sleep: + if event.is_set(): + do_free = True + else: + t = min_t() + if not t: + do_free = True + elif interupt_next_only: + do_free = False + else: + sleeptime = min(sleeptime, t - current) + do_free = sleeptime - undershoot <= min_sleep + + if do_free: + event.clear() + self._process_free_events(current) + else: + event.wait(sleeptime - undershoot) + current = self.time() + sleeptime = 1 / fps - (current - self._last_tick) + + self._dt = current - self._last_tick + self._last_tick = current + event.clear() # this needs to stay after _last_tick + return current def mainthread(func): @@ -461,6 +788,7 @@ def mainthread(func): print('The request succedded!', 'This callback is called in the main thread.') + self.req = UrlRequest(url='http://...', on_success=callback) .. versionadded:: 1.8.0 @@ -473,7 +801,18 @@ def mainthread(func): return delayed_func if 'KIVY_DOC_INCLUDE' in environ: - #: Instance of :class:`ClockBase`. + #: Instance of :class:`ClockBaseBehavior`. Clock = None else: - Clock = register_context('Clock', ClockBase) + _classes = {'default': ClockBase, 'interrupt': ClockBaseInterrupt, + 'free_all': ClockBaseFreeInterruptAll, + 'free_only': ClockBaseFreeInterruptOnly} + _clk = environ.get('KIVY_CLOCK', Config.get('kivy', 'kivy_clock')) + if _clk not in _classes: + raise Exception( + '{} is not a valid kivy clock. Valid clocks are {}'.format( + _clk, sorted(_classes.keys()))) + + Clock = register_context('Clock', _classes[_clk]) + '''The kivy Clock instance. See module documentation for details. + ''' diff --git a/kivy/config.py b/kivy/config.py index aaa47595d..3a72a1b79 100644 --- a/kivy/config.py +++ b/kivy/config.py @@ -190,6 +190,8 @@ Available configuration tokens :class:`~kivy.uix.behaviors.buttonbehavior.ButtonBehavior` to make sure they display their current visual state for the given time. + `kivy_clock`: one of `default`, `interrupt`, `free_all`, `free_only` + The clock type to use with kivy. See :mod:`kivy.clock`. :input: @@ -263,6 +265,7 @@ Available configuration tokens .. versionchanged:: 1.9.2 `min_state_time` has been added to the `graphics` section. + `kivy_clock` has been added to the kivy section .. versionchanged:: 1.9.0 `borderless` and `window_state` have been added to the graphics section. @@ -309,7 +312,7 @@ from weakref import ref _is_rpi = exists('/opt/vc/include/bcm_host.h') # Version number of current configuration format -KIVY_CONFIG_VERSION = 15 +KIVY_CONFIG_VERSION = 16 Config = None '''The default Kivy configuration object. This is a :class:`ConfigParser` @@ -799,6 +802,9 @@ if not environ.get('KIVY_DOC_INCLUDE'): elif version == 14: Config.setdefault('graphics', 'min_state_time', '.035') + elif version == 15: + Config.setdefault('kivy', 'kivy_clock', 'default') + # elif version == 1: # # add here the command for upgrading from configuration 0 to 1