Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# TachyPy
[![CI](https://github.com/Charestlab/tachypy/actions/workflows/ci.yml/badge.svg)](https://github.com/Charestlab/tachypy/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/pypi/v/tachypy.svg)](https://pypi.org/project/tachypy/)
[![Python versions](https://img.shields.io/pypi/pyversions/tachypy.svg)](https://pypi.org/project/tachypy/)
[![Docs Status](https://readthedocs.org/projects/tachypy/badge/?version=latest)](https://tachypy.readthedocs.io/en/latest/?badge=latest)
[![License](https://img.shields.io/github/license/Charestlab/tachypy.svg)](https://github.com/Charestlab/tachypy/blob/main/LICENSE)

TachyPy is a psychophysics engine for Python focused on precise visual timing with
OpenGL rendering, a GLFW-first display/input backend, and experiment-friendly
Expand Down Expand Up @@ -53,7 +57,8 @@ software timestamps alone.
- Psychophysics helpers (`make_gabor`, gratings, normalization, dithering).
- Audio playback utility (`Audio`) backed by `tachyaudio`.
- Optional Wooting analog-keyboard integration (`tachypy[wooting]`): on-screen
pressure feedback and `WOOTING_ACQUISITION` straight from `tachypy`.
pressure feedback, analog scrollbar interaction, and
`WOOTING_ACQUISITION` straight from `tachypy`.
- Test suite for core logic and regressions.

## Installation
Expand Down Expand Up @@ -88,7 +93,8 @@ pip install -e ".[wooting]" # Wooting analog-keyboard integration
### Wooting analog-keyboard integration

`pip install "tachypy[wooting]"` adds support for Wooting analog keyboards
(pressure acquisition, logging, and on-screen visual feedback):
(pressure acquisition, logging, visual feedback, and analog scrollbar
interaction):

```python
from tachypy import Screen, WOOTING_ACQUISITION
Expand Down Expand Up @@ -183,7 +189,7 @@ TACHYPY_FONT="Avenir Next, Helvetica, Arial" python example_tachypy.py
- `GLSystemText` supports system font selection by family name, fallback list
(e.g. `"Avenir Next, Helvetica, Arial"`), or direct font file path.
- For production instruction text, prefer `Text`.
- The old texture-backed constructor is backbenched as `tachypy.text.LegacyText`.
- The old texture-backed constructor is retained as `tachypy.text.LegacyText`.

## API Naming

Expand Down
6 changes: 6 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ Core modules
.. automodule:: tachypy.scrollbar
:members:

Scrollbar interaction
---------------------

.. automodule:: tachypy.scrollbar_interaction
:members:

.. automodule:: tachypy.psychophysics
:members:

Expand Down
76 changes: 76 additions & 0 deletions docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,82 @@ OpenGL context creation.
Warmup flip timestamps are cleared after the warmup sequence, so the first
experiment flip starts with a clean ``Screen`` timing state.

.. _refresh-rates-vsync:

Refresh rates and VSync
------------------------

Two independent things happen whenever a frame is drawn:

- **Presentation timing** — whether a buffer swap is synced to the display.
Handled entirely by VSync: when on, the OS/GPU blocks ``flip()`` until the
real vertical blank, regardless of what TachyPy knows.
- **Loop scheduling** — how often the interaction loops (``interact_slider``,
``wait_light_press_visual``) bother calling ``flip()`` at all, via
``get_render_interval()``.

VSync only blocks ``flip()`` while events are being pumped on some platforms
(see ``poll_events()``) — every TachyPy interaction loop already does this.

``get_render_interval()`` only reads the ``vsync`` flag to pick which rate to
use — the monitor's highest rate at the current resolution when on,
``desired_refresh_rate`` when off — never to observe VSync itself: the
vertical-blank signal is only observable by calling ``flip()``, which
blocks, so using it here would mean blocking on every scheduling check
instead of just when a frame is actually due. A wrong estimate only affects
how often frames are submitted, never presentation correctness.

``desired_refresh_rate`` feeds both, but not consistently:

.. list-table::
:header-rows: 1

* - Value
- ``tick()`` (manual pacing, ``vsync=False``)
- Interaction loops
* - ``None`` (default)
- Monitor's max rate at current resolution, or 60 Hz if unknown
- Same
* - ``0``
- No rate limit — ``flip()`` runs flat out
- Same as ``None`` (still rate-limited)
* - positive, e.g. ``120``
- Paces to that rate
- Same
* - negative, e.g. ``-1``
- No rate limit, same as ``0``
- Raises ``ValueError``

Known rough edge, not deliberate design — don't rely on ``0``/negative
behavior being stable across the two paths.

This normally costs nothing: the monitor's rate is detected correctly, so
both paths use it directly. TachyPy paces to the *highest* rate at the
current resolution rather than whatever the current mode reports, since
adaptive-refresh displays (e.g. ProMotion) can otherwise get stuck at a
transient idle rate. The 60 Hz fallback only applies when GLFW can't report
any rate at all — and guesses low rather than high, since an over-eager
schedule can stall input polling (same thread) more than a slow one.

Screen initialization warnings
-------------------------------

``Screen`` can print up to three ``[TachyPy WARNING]`` diagnostics to
stderr at construction, each firing at most once:

``screen_number`` out of range
Falls back to monitor 0, listing every detected monitor's name and max
refresh rate.

Requested rate exceeds the display
``desired_refresh_rate`` exceeds the monitor's highest rate at the
current resolution.

Display rate unknown
GLFW couldn't report any rate, so TachyPy guesses 60 Hz. Suppressed when
``vsync=False`` with ``desired_refresh_rate`` set, since that's used
instead.

Input/event handling
--------------------

Expand Down
6 changes: 0 additions & 6 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,6 @@ Use ``Esc`` to quit, click ``START``/``STOP``/``RESET``, or use ``Space`` and

tachypy-clock-demo --windowed

Run the default demo:

.. code-block:: bash

python example_tachypy.py

Notes
-----

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ abstractions for display/input, and helper utilities for experiment workflows.
timing_validation
text_rendering
audio
scrollbar
wooting
examples
contributing
Expand Down
88 changes: 88 additions & 0 deletions docs/scrollbar.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
Scrollbar widget
================

TachyPy's :class:`~tachypy.scrollbar.Scrollbar` is a customizable visual
widget for selecting a continuous value, ``0``–``100`` by default. It is
independent of the input device: the same widget can be controlled with the
mouse, an analog keyboard, or another custom interaction loop.

Normal mouse use
----------------

Create the scrollbar with the display dimensions, draw it every frame, and
pass the current mouse position to ``handle_mouse``. The value is selected when
a mouse button is released:

.. code-block:: python

from tachypy import ResponseHandler, Screen, Scrollbar

screen = Screen(fullscreen=False)
responses = ResponseHandler(screen=screen)
scrollbar = Scrollbar(
screen_width=screen.width,
screen_height=screen.height,
position_y=screen.height / 2,
half_bar_length=350,
num_marks=11,
text_left="0",
text_right="100",
content_scale=screen.content_scale,
)

value = None
while value is None:
responses.get_events()
if responses.should_quit():
break

scrollbar.handle_mouse(*responses.get_mouse_position())
screen.fill((128, 128, 128))
scrollbar.draw()
screen.flip()

for click in responses.get_mouse_clicks():
if click["type"] == "mouseup":
value = scrollbar.get_value()

screen.close()

The widget's value can also be controlled directly:

.. code-block:: python

scrollbar.set_value(50) # choose an initial/current value
current = scrollbar.get_value()

Customization
-------------

The constructor keeps the appearance and geometry of the scrollbar explicit.
Common options include:

* ``half_bar_length``, ``bar_thickness`` and ``bar_color`` for the main bar;
* ``num_marks``, ``mark_thickness`` and ``mark_color`` for tick marks;
* ``text_left``, ``text_right``, ``font_name``, ``font_size`` and
``text_color`` for endpoint labels;
* ``half_end_height``, ``end_thickness`` and ``end_color`` for the endpoints;
* ``limit_mouse`` to require the cursor to stay near the bar's horizontal line;
* ``content_scale=screen.content_scale`` for sharp labels on Retina/HiDPI
displays.

For the complete constructor reference, see the
:class:`~tachypy.scrollbar.Scrollbar` API documentation.

Analog keyboard interaction
----------------------------

Analog keyboard controls are documented with the Wooting integration because
they include key roles, pressure-to-speed mapping, confirmation safety, Wooting
key validation, and keyboard/mouse modes. The interaction layer keeps this
widget unchanged and simply drives its existing ``set_value``/``draw`` API:

.. seealso::

:doc:`wooting`

The generic, keyboard-agnostic API is documented in
:mod:`tachypy.scrollbar_interaction`.
22 changes: 22 additions & 0 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,25 @@ Add new tests
- Mock GLFW/OpenGL interactions when asserting non-rendering behavior.
- Add regression tests for every bug fix before release.
- Mock ``tachyaudio.OutputStream`` in CI and keep hardware audio tests separate.

Manual diagnostics
-------------------

Some checks need a real display and human judgment, so they live in
``tests/`` as plain scripts rather than ``pytest`` cases — not collected by
the automated suite, and not installed as a console command, since they're
for TachyPy developers, not experiment authors.

``tests/manual_fps_probe.py``
Measures the actual achieved ``flip()`` rate under VSync, with no
drawable content. Useful for sanity-checking VSync/pacing behavior on a
new machine, after a GLFW/platform update, or when investigating a
suspiciously low frame rate before suspecting your own drawables or
hardware polling. Reports a tight, unpaced loop and a paced measurement
using the same ``LoopPacer`` as ``interact_slider`` and
``wait_light_press_visual``.

.. code-block:: bash

python tests/manual_fps_probe.py # fullscreen
python tests/manual_fps_probe.py --windowed # windowed, for development
2 changes: 1 addition & 1 deletion docs/text_rendering.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Recommended usage
- Use ``Text`` for high-quality instruction screens and overlays.
- Use ``GLSystemText`` only when you want the explicit historical class name.
- Use ``GLTextSDF`` when scalable text quality matters and shaping is simple.
- The old Pillow texture-backed constructor is backbenched as
- The old Pillow texture-backed constructor is retained as
``tachypy.text.LegacyText`` for compatibility.

HiDPI and Retina displays
Expand Down
Loading
Loading