Blazing-fast Stable Retro fork with native vectorization and preprocessing
Project description
stable-retro-turbo is a performance-focused fork of Stable Retro that accelerates rollouts by moving vectorization and preprocessing into native code. By batching environment stepping and preprocessing natively, it avoids Python overhead, wrapper dispatch, and redundant memory copies, delivering substantially higher rollout throughput, especially with many parallel environments.
Turbo-Only Features
Compared with upstream Stable Retro's single-environment RetroEnv API,
stable-retro-turbo adds a training-oriented fast path:
- ⚡ RetroVecEnv:
RetroVecEnvprovides a GymnasiumVectorEnvfast path for many emulator lanes at once. - 🧪 Fused native preprocessing: RL image transforms, frame handling, and reward clipping can run in the native rollout path.
- 📦 Observation ownership modes: Callers can choose copy-safe observations or faster view-based observations for benchmarks.
- 🔎 Native info filtering: Step metadata can be reduced before it crosses the Python boundary.
- 🛤️ Multi-state training support: Lanes can use fixed, sampled, and tracked start states for curricula or task-conditioned agents.
- 🔁 Lane-local masked reset: Finished lanes retain their terminal state until the caller explicitly resets the selected lanes.
- 🎛️ Rollout stochasticity controls: Sticky actions and random no-op starts are available in the native path.
- 🧩 Explicit ROM paths: Benchmarks and integrations can run without relying on an imported ROM lookup.
- 🍄 Expanded Mario saved states: Mario Level 1 and Level 2 starts ship with the package.
Install
uv venv --python 3.14
uv pip install stable-retro-turbo
Quick platform player
For a local editable install, run:
uv tool install . -e
Then launch a representative installed game for a platform:
stable-retro-turbo play genesis
stable-retro-turbo play nes
stable-retro-turbo play SuperMarioBros-Nes-v0
stable-retro-turbo play all
To see the raw RGB game and the exact PPO-style preprocessed observation at
the same time, add --show-obs:
stable-retro-turbo play SuperMarioBros-Nes-v0 --show-obs
The second window tiles the four grayscale 84x84 frames that make up the observation, after the standard top crop and area resize. It samples every fourth frame while the RGB game continues rendering at its normal 60 FPS.
all opens one game per platform, sequentially. Use
stable-retro-turbo play --list to see the current ROM-backed platform choices.
For Atari games, the player presses FIRE for one frame after the initial reset
and every later episode reset so games such as Breakout start automatically.
Use
import stable_retro as retro # Uses the upstream-compatible import name.
# RetroVecEnv is the stable-retro-turbo native vector interface.
env = retro.RetroVecEnv(
# stable-retro params (all stable-retro params can still be used)
"SuperMarioBros-Nes-v0", # Stable Retro game integration.
state="Level1-1", # Saved game state to load in each lane.
# stable-retro-turbo specific params
num_envs=32, # Number of emulator lanes stepped together.
num_threads=16, # Native worker threads for those lanes.
render_mode="rgb_array", # Return frame arrays instead of opening a window.
obs_crop=(32, 0, 0, 0), # Crop 32 pixels from the top before resizing.
obs_crop_mode="mask", # Hide the HUD while preserving full-frame geometry.
obs_crop_fill=0, # Pixel value used by mask crop regions.
obs_resize=(84, 84), # Resize observations natively for RL input.
obs_resize_algorithm="area", # Area resize is a good downsampling default.
obs_grayscale=True, # Convert RGB frames to grayscale natively.
obs_layout="chw", # Return channel-first tensors for PyTorch.
obs_copy="safe_view", # Avoid extra copies while keeping observations safe.
frame_skip=4, # Repeat each action for 4 emulator frames.
frame_stack=4, # Stack the last 4 processed frames.
maxpool_last_two=True, # Max-pool recent frames to reduce flicker.
noop_reset_max=0, # Disable random no-op starts for this example.
sticky_action_prob=0.0, # Disable sticky actions for deterministic stepping.
info_filter="terminal", # Only return full info payloads at episode end.
)
obs, infos = env.reset(seed=123)
obs, rewards, terminations, truncations, infos = env.step(env.action_space.sample())
env.close() # Release native emulator resources.
Atari
Atari support is included in the standard install through the packaged Stella
libretro core and the same RetroEnv / RetroVecEnv API used by other systems:
pip install stable-retro-turbo
import stable_retro as retro
env = retro.RetroVecEnv(
"Breakout-Atari2600-v0",
state="Start",
num_envs=32,
num_threads=16,
obs_resize=(84, 84),
obs_grayscale=True,
obs_resize_algorithm="area",
obs_layout="chw",
frame_skip=4,
frame_stack=4,
maxpool_last_two=True,
noop_reset_max=0,
info_filter="none",
)
obs, infos = env.reset(seed=123)
obs, rewards, terminations, truncations, infos = env.step(env.action_space.sample())
done = terminations | truncations
if done.any():
obs, infos = env.reset(options={"reset_mask": done})
env.close()
Import Atari ROMs into Stable Retro's data tree before constructing the env.
No separate Atari runtime or ROM registry is used. RetroVecEnv preserves Stable
Retro .state files, scenario rewards, button-mask actions, native preprocessing,
and lane-local masked reset under AutoresetMode.DISABLED.
RetroVecEnv Parameters
retro.RetroVecEnv(
game,
state=retro.State.DEFAULT,
scenario=None,
info=None,
use_restricted_actions=retro.Actions.FILTERED,
record=False,
players=1,
inttype=retro.data.Integrations.STABLE,
obs_type=retro.Observations.IMAGE,
render_mode="human",
*,
num_envs=1, # number of emulator lanes in the vector env
num_threads=None, # native worker threads; defaults to num_envs
rom_path=None, # explicit ROM path for direct-ROM or external use
obs_resize=None, # native resize target as (width, height)
obs_crop=None, # native crop before resize
obs_crop_mode="remove", # "remove" crops pixels; "mask" fills them
obs_crop_fill=0, # fill value for obs_crop_mode="mask"
obs_grayscale=False, # convert image observations to grayscale
obs_resize_algorithm="nearest", # "nearest", "bilinear", or "area"
obs_layout="hwc", # observation layout: "hwc" or "chw"
obs_copy="copy", # "copy", "safe_view", or "unsafe_view"
frame_skip=1, # repeat each action for this many frames
frame_stack=1, # stack this many processed frames
maxpool_last_two=False, # max-pool the last two skipped frames
noop_reset_max=0, # random no-op frames after reset
use_fire_reset=True, # press FIRE once after Atari resets
sticky_action_prob=0.0, # chance to repeat the previous lane action
reward_clip=False, # clip rewards in the native path
info_filter="all", # "all", "terminal", "none", or mode/keys mapping
)
Inherited fields keep their upstream Stable Retro meaning. The native vector
path currently supports players=1, image observations, and no movie
recording. state also accepts turbo-only multi-state forms: a sequence with
one state per env lane, or a {state_name: weight} mapping sampled on reset.
| Turbo parameter | Default | What it controls |
|---|---|---|
num_envs |
1 |
Number of emulator lanes in the vector environment. |
num_threads |
None (num_envs) |
Native worker threads; when omitted, uses one worker per env lane. |
rom_path |
None |
Explicit ROM path for direct-ROM tests or external integrations. |
obs_resize |
None |
Native resize target as (width, height). |
obs_crop |
None |
Native crop before resize in the vector fast path. |
obs_crop_mode |
"remove" |
Crop behavior: "remove" keeps the historical geometry-changing crop, while "mask" preserves the full observation canvas and fills the cropped regions before resize, layout conversion, and frame stacking. |
obs_crop_fill |
0 |
Scalar uint8 fill value for crop masking; RGB observations fill every channel with the same value. |
obs_grayscale |
False |
Convert image observations to grayscale natively. |
obs_resize_algorithm |
"nearest" |
Resize algorithm: "nearest", "bilinear", or "area". |
obs_layout |
"hwc" |
Observation layout: "hwc" or "chw". |
obs_copy |
"copy" |
Observation ownership mode: "copy", "safe_view", or benchmark-only "unsafe_view". |
frame_skip |
1 |
Repeat each action for this many emulator frames. |
frame_stack |
1 |
Stack this many processed frames in each returned observation. |
maxpool_last_two |
False |
Max-pool the last two skipped frames before preprocessing. |
noop_reset_max |
0 |
Apply up to this many random no-op frames after reset. |
use_fire_reset |
True |
Press and release FIRE for one native frame after each full-episode Atari reset when the action is available. |
sticky_action_prob |
0.0 |
Probability of repeating the previous lane action instead of the requested action. |
reward_clip |
False |
Clip native vector rewards to [-1, 1] or explicit bounds. |
info_filter |
"all" |
Info payload filter: "all", "terminal", "none", or {"mode": ..., "keys": (...)}. |
Use crop masking when you want to hide HUDs or other static screen regions
during initial training without changing the spatial observation contract. For
example, obs_crop=(32, 0, 0, 0), obs_crop_mode="mask" hides the top 32 pixels
before resize and frame stacking, while keeping the same observation geometry as
the full-canvas path for later fine-tuning on unmasked observations.
Gymnasium VectorEnv
RetroVecEnv is specific to stable-retro-turbo and implements the
Gymnasium vector API directly. It does not subclass SB3 VecEnv, and
stable-baselines3 is not a runtime dependency for the native vector path.
The native env permanently uses Gymnasium disabled autoreset semantics:
obs, infos = env.reset(seed=seed)
obs, rewards, terminations, truncations, infos = env.step(actions)
When a lane terminates, the returned observation and info are the genuine terminal transition. Reset only the finished lanes before stepping again:
import stable_retro
env = stable_retro.RetroVecEnv(
"SuperMarioBros-Nes-v0",
num_envs=16,
)
obs, infos = env.reset(seed=list(range(16)))
obs, rewards, terminations, truncations, infos = env.step(actions)
done = terminations | truncations
if done.any():
obs, reset_infos = env.reset(options={"reset_mask": done})
Terminal observations and infos are returned directly from
step(). A terminated lane cannot be stepped again until it is selected by a
masked reset. Unselected lanes retain their emulator state, RNG stream,
observation/frame stack, and sticky-action history. Reset infos are columnar and
only mark selected lanes as present.
reset_mask must be a NumPy bool array with shape (num_envs,) and at least
one selected lane. A scalar reset seed expands to seed + lane_index; a seed
sequence must contain exactly one integer or None per lane. During a masked
reset, unselected seed entries are ignored. For environments constructed with a
start-state catalog, start_indices is an int32 array of the same shape:
mask = np.array([False, True, False, True], dtype=np.bool_)
starts = np.array([-1, 1, -1, 0], dtype=np.int32)
obs, reset_infos = env.reset(
seed=[None, 101, None, 303],
options={"reset_mask": mask, "start_indices": starts},
)
Nonnegative values select that catalog entry atomically; -1 keeps the
configured fixed/per-lane/weighted policy. Values and seeds for unselected lanes
are ignored. active_state_indices() and active_states() change only for
selected lanes.
Commands
uv run python scripts/benchmark_vec_env.py --list-profiles # show saved benchmark profiles
uv run python scripts/benchmark_vec_env.py --profile supermario-level1-1 --dry-run # print resolved env benchmark config
uv run python scripts/benchmark_vec_env.py --profile supermario-level1-1 # run native/classic rollout benchmark
uv run python scripts/benchmark_vec_env.py --profile atari-breakout # benchmark Atari manual reset
make benchmark-local BENCHMARK_ARGS=--dry-run GAME=MegaMan PLATFORM=Nes STATE=Level1
make benchmark GAME=SuperMarioBros PLATFORM=Nes STATE=Level1-1
uv run pytest tests/test_python/test_vec_env.py # run focused RetroVecEnv tests
uv run --with build python -m build # build source and wheel artifacts
Benchmarks
The benchmark profile file is
scripts/benchmark_vec_env.json. The default
user-facing profile is supermario-level1-1, which uses a real
SuperMarioBros-Nes-v0 / Level1-1 saved state with PPO-style preprocessing:
crop (32,0,0,0), resize 84x84, grayscale, frame skip 4, frame stack 4,
two-frame max-pool, 32 envs, and 16 native threads.
Use real saved states for user-facing RetroVecEnv throughput numbers.
State.NONE is reserved for explicit direct-ROM hot-path diagnostics and
requires --allow-state-none. The atari-breakout profile measures the real
Stella Start saved-state contract through RetroVecEnv.
make benchmark refreshes the local extension through setup.py build_ext --inplace
only when native/build inputs changed, then runs the same benchmark entrypoint
as benchmark-local. Set GAME and PLATFORM to compose a Stable Retro id
such as MegaMan-Nes-v0, or pass a full id through GAME and omit PLATFORM.
Set STATE, BENCHMARK_PROFILE, BENCHMARK_SECONDS, and BENCHMARK_ARGS to
override the profile defaults.
Reference Modal Results
All rows were measured on Modal CPU with cpu_request=16.0,
memory_mb=16384, os_cpu_count=32, affinity_cpu_count=32,
machine=x86_64, platform Linux-4.19.0-gvisor-x86_64-with-glibc2.36, and
Python 3.14.6. Each env-throughput row uses SuperMarioBros-Nes-v0 /
Level1-1, a real saved state, crop (32,0,0,0), resize 84x84, grayscale,
frame skip 4, frame stack 4, two-frame max-pool, sampled actions, and
three samples.
The stable-retro-turbo==1.0.0.post22 rows install the published PyPI wheel
and mount only the benchmark harness/profile JSON. Package runtime:
/usr/local/lib/python3.14/site-packages/stable_retro/__init__.py; extension
/usr/local/lib/python3.14/site-packages/stable_retro/_retro.cpython-314-x86_64-linux-gnu.so.
The upstream rows remotely build Farama stable-retro from
ec7a62718a1f99f34bf5e5d5c57255c9a53df507 (main) and use the classic
RetroEnv path with benchmark-side preprocessing.
Modal runs: full env benchmark
ap-sNnRpf48gi4umUyabfrOA9;
16-env env-only fairness benchmark
ap-vQu3BlHG2uEeyykTbjK9pV.
| Source / backend | Shape | Samples steps/s | Mean | Std | Best | Speedup vs subproc / async | Artifact |
|---|---|---|---|---|---|---|---|
post22 native fused RetroVecEnv |
16 envs |
9403.0, 9278.3, 9612.2 |
9431.2 |
168.7 |
9612.2 |
10.13x / 11.68x |
post22 16-env |
upstream Gymnasium AsyncVectorEnv / classic RetroEnv |
16 envs |
807.3, 810.9, 804.2 |
807.5 |
3.4 |
810.9 |
0.87x / 1.00x |
upstream async |
Notes
- The import package is
stable_retro;retroremains as a compatibility shim. RetroVecEnvis the turbo-specific Gymnasium vector environment.- Atari uses the packaged Stella core through
RetroEnvandRetroVecEnv. RetroVectorEnvis not exposed;RetroVecEnvis the public native vector API.- Source builds, CI, and release wheels target Python
3.14; the repo-local deterministic release helper publishes onecp314wheel for each supported release platform. Building from source also requires CMake, a C/C++ compiler, and platform core build dependencies. - ROMs are not included. Import ROMs and read game/core docs through upstream Stable Retro unless the work is specifically about this turbo layer.
active_state_indices()returns a read-onlyint32NumPy view for task-conditioned training; copy it when you need a stable snapshot.- Third-party emulator cores carry their own licenses; see
LICENSES.md.
Architecture
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stable_retro_turbo-1.0.1.post30-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.
File metadata
- Download URL: stable_retro_turbo-1.0.1.post30-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
- Upload date:
- Size: 91.6 MB
- Tags: CPython 3.14, manylinux: glibc 2.27+ x86-64, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d0be7b8174e2e81a20c67c267184bd56d499570558f6fa70b6dce56101e4aa04
|
|
| MD5 |
d1626fd2f3d1d017591cead90e6f3600
|
|
| BLAKE2b-256 |
b58119932150616035c578746817ed73246d1b030fa316376d1662f0df07fd50
|
Provenance
The following attestation bundles were made for stable_retro_turbo-1.0.1.post30-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl:
Publisher:
release.yml on tsilva/stable-retro-turbo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stable_retro_turbo-1.0.1.post30-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl -
Subject digest:
d0be7b8174e2e81a20c67c267184bd56d499570558f6fa70b6dce56101e4aa04 - Sigstore transparency entry: 2169223411
- Sigstore integration time:
-
Permalink:
tsilva/stable-retro-turbo@edaa1f72aab24eee36be60cb02708296bf4e6560 -
Branch / Tag:
refs/tags/v1.0.1.post30 - Owner: https://github.com/tsilva
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@edaa1f72aab24eee36be60cb02708296bf4e6560 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stable_retro_turbo-1.0.1.post30-cp314-cp314-macosx_14_0_arm64.whl.
File metadata
- Download URL: stable_retro_turbo-1.0.1.post30-cp314-cp314-macosx_14_0_arm64.whl
- Upload date:
- Size: 85.9 MB
- Tags: CPython 3.14, macOS 14.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4624ef6775091ed1c8e1733b2f589a6321438c598c53608cdd4ead7faf13ba7e
|
|
| MD5 |
e5779435b9a117c013e466a70bcfe5d8
|
|
| BLAKE2b-256 |
459a13a097e04fd70b70cbeeb858639020c2e8abafe58c2df3bd0cfd47c56792
|
Provenance
The following attestation bundles were made for stable_retro_turbo-1.0.1.post30-cp314-cp314-macosx_14_0_arm64.whl:
Publisher:
release.yml on tsilva/stable-retro-turbo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stable_retro_turbo-1.0.1.post30-cp314-cp314-macosx_14_0_arm64.whl -
Subject digest:
4624ef6775091ed1c8e1733b2f589a6321438c598c53608cdd4ead7faf13ba7e - Sigstore transparency entry: 2169223510
- Sigstore integration time:
-
Permalink:
tsilva/stable-retro-turbo@edaa1f72aab24eee36be60cb02708296bf4e6560 -
Branch / Tag:
refs/tags/v1.0.1.post30 - Owner: https://github.com/tsilva
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@edaa1f72aab24eee36be60cb02708296bf4e6560 -
Trigger Event:
push
-
Statement type: