Skip to content
Merged
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: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"

Expand All @@ -43,10 +43,10 @@ jobs:
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: "3.12"

Expand All @@ -72,10 +72,10 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

Expand Down
32 changes: 13 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,8 @@ software timestamps alone.
- OpenGL stimulus rendering (`Texture`, `Shapes`, fixation, etc.).
- GLFW-first window/input handling via `Screen` for tighter display control.
- Backend-aware input handling through `ResponseHandler`.
- Multiple text paths:
- `Text` (system fonts via FreeType + HarfBuzz; polished default),
- `GLText` (OpenGL bitmap glyphs),
- `GLTextSDF` (distance-field text),
- `GLSystemText` (backward-compatible explicit name for `Text`).
- `Text` for text rendering: system fonts via FreeType + HarfBuzz, drawn as
OpenGL quads.
- Psychophysics helpers (`make_gabor`, gratings, normalization, dithering).
- Audio playback utility (`Audio`) backed by `tachyaudio`.
- Optional Wooting analog-keyboard integration (`tachypy[wooting]`): on-screen
Expand All @@ -69,10 +66,10 @@ Install base package:
pip install tachypy
```

The base install includes GLFW for display/input, PyOpenGL, Pillow text
support, pyserial for serial/trigger workflows, and TachyAudio for audio playback.
Pygame support has been removed; GLFW is the supported display/input backend.
TachyAudio is currently published as a beta release; TachyPy requires `tachyaudio>=0.2.0b2`, which includes the Windows wheel fix. If your pip resolver refuses pre-releases, pass `--pre` explicitly.
The base install includes GLFW, PyOpenGL, FreeType, HarfBuzz, pyserial, and
TachyAudio. Pygame is no longer supported. TachyAudio is currently beta;
TachyPy requires `tachyaudio>=0.2.0b2`. If pip refuses pre-releases, pass
`--pre` explicitly.

Editable install for development:

Expand All @@ -87,7 +84,6 @@ Optional extras:
```bash
pip install -e ".[test]" # pytest
pip install -e ".[wooting]" # Wooting analog-keyboard integration
# Pillow, FreeType, HarfBuzz, GLFW, and audio are included in the base install
```

### Wooting analog-keyboard integration
Expand Down Expand Up @@ -162,7 +158,7 @@ python clock_timer_demo.py
Use `Esc` to quit, click `START`/`STOP`/`RESET`, or use `Space` and `R`.
For development, use `tachypy-clock-demo --windowed` or `python clock_timer_demo.py --windowed`.

Choose a font for demo text rendering with GLFW `Text`/`GLSystemText`:
Choose a font for demo text rendering with GLFW `Text`:

```bash
TACHYPY_FONT="Avenir Next, Helvetica, Arial" python example_tachypy.py
Expand All @@ -183,13 +179,11 @@ TACHYPY_FONT="Avenir Next, Helvetica, Arial" python example_tachypy.py

## Text Rendering Notes

- `Text` is the polished system-font renderer and is equivalent to `GLSystemText`.
- `GLText`/`GLTextSDF`/`GLSystemText` render text directly in OpenGL and are
backend-independent.
- `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 retained as `tachypy.text.LegacyText`.
- `Text` is the only renderer. It uses FreeType + HarfBuzz and draws OpenGL
quads.
- `font_name` accepts a family, comma-separated fallback list (for example,
`"Avenir Next, Helvetica, Arial"`), or direct font path. Missing fonts use a
built-in default with a warning; construction raises only if none resolve.

## API Naming

Expand Down Expand Up @@ -234,7 +228,7 @@ re-sync project webhooks from the RTD project settings.

- `screen.py`: display/context lifecycle and backend abstraction.
- `responses.py`: keyboard/mouse event handling and key-state queries.
- `text.py`, `gltext.py`, `gltext_sdf.py`, `glsystemtext.py`: text rendering.
- `text.py`: text rendering (`Text`).
- `textures.py`, `shapes.py`, `draggable.py`, `scrollbar.py`: visual primitives.
- `psychophysics.py`: stimulus generation and normalization utilities.
- `audio.py`: sound playback and timing helpers.
Expand Down
9 changes: 0 additions & 9 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,6 @@ Core modules
.. automodule:: tachypy.text
:members:

.. automodule:: tachypy.gltext
:members:

.. automodule:: tachypy.gltext_sdf
:members:

.. automodule:: tachypy.glsystemtext
:members:

.. automodule:: tachypy.scrollbar
:members:

Expand Down
15 changes: 6 additions & 9 deletions docs/backends.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ 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:
``desired_refresh_rate`` feeds both, but a scheduled interaction loop can't
represent "no rate limit" the way ``tick()`` can — it needs a positive
interval to pace ``render_due()`` against, so ``0``/negative falls back to a
real rate there instead, with a ``[TachyPy WARNING]``:

.. list-table::
:header-rows: 1
Expand All @@ -65,18 +68,12 @@ how often frames are submitted, never presentation correctness.
* - ``None`` (default)
- Monitor's max rate at current resolution, or 60 Hz if unknown
- Same
* - ``0``
* - ``0`` or negative, e.g. ``-1``
- No rate limit — ``flip()`` runs flat out
- Same as ``None`` (still rate-limited)
- Same as ``None``, and warns that it can't honor "no rate limit"
* - 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
Expand Down
9 changes: 4 additions & 5 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Install TachyPy

pip install tachypy

The base install includes GLFW for display/input, PyOpenGL, Pillow text
support, pyserial for serial trigger workflows, and TachyAudio for audio
playback. Pygame support has been removed; GLFW is the supported display/input
backend. TachyAudio is currently published as a beta release; TachyPy requires ``tachyaudio>=0.2.0b2``, which includes the Windows wheel fix. If your pip resolver refuses pre-releases, pass ``--pre`` explicitly.
The base install includes GLFW, PyOpenGL, FreeType, HarfBuzz, pyserial, and
TachyAudio. Pygame is no longer supported. TachyAudio is currently beta;
TachyPy requires ``tachyaudio>=0.2.0b2``. If pip refuses pre-releases, pass
``--pre`` explicitly.

For development:

Expand All @@ -28,7 +28,6 @@ Optional extras

pip install -e ".[test]" # pytest, coverage, lint tooling
pip install -e ".[wooting]" # Wooting analog-keyboard integration
# Pillow, FreeType, HarfBuzz, GLFW, and audio are included in the base install

See :doc:`wooting` for the Wooting analog-keyboard integration.

Expand Down
38 changes: 11 additions & 27 deletions docs/text_rendering.rst
Original file line number Diff line number Diff line change
@@ -1,35 +1,19 @@
Text Rendering
==============

TachyPy offers multiple text paths depending on precision and dependency needs.
TachyPy has one text class: ``Text``.

Text class
----------

``Text`` is TachyPy's polished system-font renderer. It is the friendly public
name for ``GLSystemText`` and uses FreeType + HarfBuzz when available, with an
OpenGL bitmap fallback.
``Text`` renders system TrueType/OpenType fonts with FreeType + HarfBuzz as
OpenGL quads. ``font_name`` accepts a family name, a comma-separated fallback
list (e.g. ``"Avenir Next, Helvetica, Arial"``), or a direct font-file path.

OpenGL text renderers
---------------------

- ``GLText``: bitmap glyph renderer in pure OpenGL.
- ``GLTextSDF``: signed-distance-field renderer for smoother scaling.
- ``GLSystemText``: explicit backward-compatible name for ``Text``.
You can pass a family name, comma-separated fallback list, or a direct
font-file path.

The OpenGL renderers are backend-independent and do not require
Pillow.

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 retained as
``tachypy.text.LegacyText`` for compatibility.
FreeType and HarfBuzz are required dependencies. If ``font_name`` doesn't
resolve, TachyPy tries built-in defaults (Helvetica, Arial, DejaVu Sans,
Liberation Sans, Noto Sans, Times New Roman) and warns; construction raises
only if none resolve.

HiDPI and Retina displays
--------------------------
Expand All @@ -49,7 +33,7 @@ resolution — otherwise text appears blurry.
Why the default is ``2.0``
~~~~~~~~~~~~~~~~~~~~~~~~~~

``Text``/``GLSystemText`` and ``Scrollbar`` default ``content_scale`` to
``Text`` and ``Scrollbar`` default ``content_scale`` to
``2.0`` (matching every Retina/HiDPI display) instead of ``1.0``, as a
safety net: neither class is linked to a ``Screen``, so a caller who
forgets this parameter can't be warned. At ``1.0`` that mistake renders
Expand All @@ -61,15 +45,15 @@ recommendation to skip it.
Performance: build once, update with ``set_text()``
-----------------------------------------------------

Constructing a ``Text``/``GLSystemText`` loads the font file and builds a
Constructing a ``Text`` loads the font file and builds a
fresh FreeType face + HarfBuzz font every time — roughly **2-3 ms before a
single glyph is rasterized**, regardless of ``content_scale``. That alone
can exceed a whole frame budget at high refresh rates (240 Hz = 4.17 ms),
so recreating a ``Text`` inside a per-frame or per-trial loop is a common
way to silently blow it.

Build the object **once** and update content with
:meth:`~tachypy.glsystemtext.GLSystemText.set_text` instead — it reuses the
:meth:`~tachypy.text.Text.set_text` instead — it reuses the
glyph cache, so only newly-seen glyphs are rasterized:

.. code-block:: python
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
numpy>=1.18.0
glfw>=2.7
pyserial>=3.5
Pillow>=10.0
PyOpenGL>=3.1.0
screeninfo>=0.6.0
freetype-py>=2.4
Expand Down
6 changes: 1 addition & 5 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = tachypy
version = 0.1.19
version = 0.2.0
author = Ian Charest, Mathias Salvas-Hebert and Frederic Gosselin
author_email = charest.ian@gmail.com
description = A package for psychophysics in Python, using GLFW and OpenGL.
Expand All @@ -24,7 +24,6 @@ package_dir =
install_requires =
glfw>=2.7
pyserial>=3.5
Pillow>=10.0
PyOpenGL>=3.1.0
numpy>=1.18.0
screeninfo>=0.6.0
Expand Down Expand Up @@ -64,9 +63,6 @@ omit =
*/tachypy/text.py
*/tachypy/textures.py
*/tachypy/shapes.py
*/tachypy/gltext.py
*/tachypy/gltext_sdf.py
*/tachypy/glsystemtext.py
*/tachypy/examples/*
*/tachypy/wooting/demos/*

Expand Down
4 changes: 0 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
install_requires=requires,
extras_require={
'test': ['pytest>=7.0', 'pytest-cov>=5.0', 'ruff>=0.6'],
'text': ['Pillow>=10.0'],
'system_text': ['freetype-py>=2.4', 'uharfbuzz>=0.39'],
'glfw': ['glfw>=2.7'],
'audio': [],
'wooting': ['tachywooting>=0.2.4'],
},
python_requires='>=3.10',
Expand Down
8 changes: 1 addition & 7 deletions src/tachypy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@
"ResponseHandler",
"Scrollbar",
"Text",
"GLText",
"GLTextSDF",
"GLSystemText",
"make_sine_grating",
"make_gaussian_envelope",
"make_gabor",
Expand Down Expand Up @@ -63,10 +60,7 @@
"center_rect_on_point": ("tachypy.shapes", "center_rect_on_point"),
"ResponseHandler": ("tachypy.responses", "ResponseHandler"),
"Scrollbar": ("tachypy.scrollbar", "Scrollbar"),
"Text": ("tachypy.glsystemtext", "GLSystemText"),
"GLText": ("tachypy.gltext", "GLText"),
"GLTextSDF": ("tachypy.gltext_sdf", "GLTextSDF"),
"GLSystemText": ("tachypy.glsystemtext", "GLSystemText"),
"Text": ("tachypy.text", "Text"),
"make_sine_grating": ("tachypy.psychophysics", "make_sine_grating"),
"make_gaussian_envelope": ("tachypy.psychophysics", "make_gaussian_envelope"),
"make_gabor": ("tachypy.psychophysics", "make_gabor"),
Expand Down
42 changes: 37 additions & 5 deletions src/tachypy/_warnings.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
"""Shared TachyPy diagnostic warning helper.

Deduplicates identical warnings (same context + message) so constructing many
``Text``/``GLSystemText`` objects with the same misconfiguration -- a common
pattern, since an experiment scene typically builds one label per UI element
-- doesn't flood stderr with repeats of the same root cause.
objects with the same misconfiguration doesn't flood stderr with repeats.
"""
import os
import sys
import traceback
from pathlib import Path

_warned: set = set()
_PACKAGE_DIR = Path(__file__).resolve().parent

# On by default; off if TACHYPY_DISABLE_WARNINGS is truthy, or via set_warnings_enabled().
_enabled = os.environ.get("TACHYPY_DISABLE_WARNINGS", "").strip().lower() not in (
"1", "true", "yes", "on",
)


def set_warnings_enabled(enabled: bool) -> None:
"""Turn TachyPy's ``[TachyPy WARNING]`` diagnostics on or off."""
global _enabled
_enabled = bool(enabled)


def warnings_enabled() -> bool:
"""Return whether TachyPy's diagnostic warnings are currently enabled."""
return _enabled


def caller_location() -> str:
"""Return 'file.py:line' for the nearest stack frame outside tachypy itself."""
for frame in reversed(traceback.extract_stack()[:-1]): # skip this frame itself
frame_path = Path(frame.filename).resolve()
if _PACKAGE_DIR not in frame_path.parents:
return f"{frame_path.name}:{frame.lineno}"
return "unknown location"


def warn_once(context: str, message: str) -> None:
"""Print a TachyPy-branded warning to stderr, once per unique (context, message)."""
"""Print a TachyPy-branded warning to stderr, once per unique (context, message).

Tagged with the caller's file:line (not part of the dedup key, so repeats collapse to one warning).
"""
if not _enabled:
return
key = (context, message)
if key in _warned:
return
_warned.add(key)
label = f"\n\t[TachyPy WARNING]: {context}"
label = f"\n\t[TachyPy WARNING]: {context} (from {caller_location()})"
if sys.stderr.isatty():
label = f"\033[1;31m{label}\033[0m"
print(f"{label}\n\t\t{message}", file=sys.stderr, end="\n\n")
Loading