English | 简体中文
This is an interactive container-loading game built on Genesis rigid-body physics. Use WASD and J/K/L to choose the position and orientation of each cargo box, then release it from above the target. Every placed box remains a dynamic rigid body, so a new impact can push, tilt, or topple the entire stack.
The default configuration uses a native Nyx GPU window, Genesis CPU physics,
and a fixed 60 Hz main loop.
- Automatically selects assets from
assets/box/whose three side lengths are all within0.3–0.6 m, then randomizes the box sequence. - Moves the target freely across the container floor with WASD and rotates it
by
90°around world X/Y/Z with J/K/L. - Resolves target height with a live OBB shape cast. Moving over an existing box places the preview on its surface instead of restricting the cursor to precomputed empty slots.
- Adds no artificial pose gap. After confirmation, Genesis simulates gravity, friction, collisions, sleeping, and contact-island wake-up.
- Supports a native Nyx GPU window, Genesis Viewer, headless auto-placement, and frame-time profiling.
- Run commands from the repository root.
- Use an existing Genesis Python environment that already contains
torch,trimesh, and the base Genesis dependencies.setup_nyx.shinstalls the overlay with--no-depsand does not install those large dependencies. - The setup script requires the
uvcommand-line tool. - Nyx mode requires an NVIDIA CUDA GPU and a Linux X11 or XWayland session.
- Use Genesis Viewer when Nyx or CUDA is unavailable, or
--no-vieweron a headless server.
Activate the project's existing Genesis environment and install the validated Nyx overlay once:
source /path/to/genesis-world/.venv/bin/activate
bash play/setup_nyx.shReplace /path/to/genesis-world with the actual local path to the Genesis
repository.
The script installs the following validated versions into the Git-ignored
play/.runtime/ directory without overwriting the original virtual
environment:
genesis-world==1.3.1quadrants==1.2.0numpy==2.3.5av==18.0.0gs-nyx==0.1.4gs-nyx-plugin==0.1.5
You can also select the interpreter explicitly without activating the environment first:
PYTHON_BIN=/path/to/genesis-world/.venv/bin/python \
bash play/setup_nyx.shIn that case, use the same interpreter to launch the game:
/path/to/genesis-world/.venv/bin/python play/box_tetris.pyOtherwise, launch from the repository root with the active environment:
python play/box_tetris.pyYou do not need to rerun setup_nyx.sh on subsequent launches.
--renderer auto is the default. It falls back to Genesis Viewer when the Nyx
package-version or CUDA checks fail. If Nyx has already been selected but
DISPLAY, XAUTHORITY, or libX11 is unavailable, the program reports an
input-window error instead of falling back automatically. Fix the graphical
session, or explicitly use --renderer genesis / --no-viewer.
You can select either interactive renderer directly:
python play/box_tetris.py --renderer nyx
python play/box_tetris.py --renderer genesisNyx uses 1280×800 Forward rendering, SMAA, no shadows, one camera, and a
native window. Frames stay on the CUDA device without cpu().numpy(), OpenCV,
or Python image copies. The current Nyx API cannot reliably report the native
title-bar close event, so exit with Esc or Ctrl-C in the launch terminal.
W/S: move the preview deeper into the container or toward the door.A/D: move the preview left or right.J/K/L: rotate90°around worldX/Y/Z.Space: confirm the glowing green outline and release the physical box from above it.Esc: exit the game.I: show or hide the full key guide in Genesis Viewer only.- Left mouse drag: orbit the Nyx camera around the loading area.
- Right mouse drag: pan the Nyx camera.
- Middle mouse vertical drag: dolly the Nyx camera in or out.
WASD, J/K/L, Space, and Esc have the same meaning in both interactive
renderers. The mouse orbit controller above is provided by
nyx_x11_input.py; Genesis Viewer uses its own native camera interaction.
Nyx camera gestures are active only inside the focused Nyx client area and do
not grab window-manager events, so the title bar can still move the window.
Keyboard and mouse commands are ignored until the Nyx window has focus. Edge
resize behavior depends on Nyx and the desktop window manager; the internal
render resolution remains 1280×800.
WASD controls a two-dimensional cursor over the entire container floor rather than jumping among filtered candidate slots. After every move or rotation, the game performs an exact downward OBB shape cast against the live stack. The preview sits on the floor when nothing is below it and moves to the first box surface when there is support underneath. Near a wall or neighboring axis-aligned box face, it snaps to an exact face-contact coordinate; continuing in the same direction moves away from the snap point.
Run python play/box_tetris.py --help for the generated command-line help.
| Option | Default | Purpose |
|---|---|---|
--renderer {auto,nyx,genesis} |
auto |
Select the interactive renderer. auto falls back from Nyx when package-version or CUDA checks fail. |
--backend {cpu,cuda} |
cpu |
Select the Genesis physics backend. CPU is usually faster for this single scene. |
--hibernation {auto,on,off} |
auto |
Control dynamic-body sleeping. Auto enables it on Genesis 1.2 and later. |
--no-viewer |
off | Disable both Nyx and Genesis Viewer for servers or automated tests. |
--max-boxes N |
72 |
Number of physical boxes to preallocate at startup. |
--max-collision-pairs N |
512 |
Genesis collision-pair capacity. Increase it after an overflow warning. |
--round-limit N |
unlimited | Play only N rounds while still preallocating --max-boxes entities. |
--seed N |
random | Fix the random box sequence for reproducible runs. |
--steps N |
unlimited | Stop after at most N main-loop steps. |
--auto-place |
off | Automatically confirm each default target for regression and stress tests. |
--profile |
off | Report p50/p95/p99 for full-frame, physics, rendering, and game-logic time. |
--profile-warmup-steps N |
60 |
Number of warm-up frames excluded from profiling. |
--profile-report-steps N |
300 |
Rolling profile-report interval. |
Example: use a reproducible sequence, Genesis Viewer, and 24 boxes:
python play/box_tetris.py \
--renderer genesis \
--max-boxes 24 \
--seed 7Focused-window X11 keyboard and mouse state
→ WASD / JKL command queue + Nyx orbit camera
→ AIMING / SETTLING game state machine
→ Genesis 1/60 s rigid-body step
→ batched visual-state update
→ native Nyx GPU window
Each round proceeds as follows:
- Rebuild current OBBs from the live stack poses in one batch and generate broad-phase AABBs. Refresh them after every move and rotation and once more before confirmation, without synchronizing on idle aiming frames.
- Move
0.05 mper WASD command and update an orthogonal orientation with J/K/L. Resolve the preview through an AABB broad phase followed by an exact 15-axis continuous-SAT vertical OBB shape cast. A tilted box therefore does not turn the highest corner of its AABB into a false horizontal surface. Scene topology remains unchanged at runtime. - On Space, move the precreated physical box
0.8 mabove the target, clear its velocity, and release it. - Genesis computes gravity, contacts, and friction. A new collision wakes connected old boxes, so the stack can move, tilt, or collapse as a whole.
- Begin the next round only after the current contact island sleeps or the full stack remains below the velocity thresholds.
There is no magnetic attachment, welding, or conversion to static geometry
after placement. Physics determines the final pose. Planning also introduces
no artificial gap: target face coordinates for the box, floor, wall, or
neighboring axis-aligned box are equal. The 1e-6 m geometry epsilon is used
only to classify projection overlap and is never added to the pose.
This lifecycle was inspired by the fixed-step, dynamic-stack, contact-island, and sleeping strategy used by the Dexterity Foresight Truck Loading Game. The browser reference first moves the current box to its support surface with a kinematic shape cast and then makes it dynamic. This project preserves the original requirement for a complete free fall and waits for physical stability before starting the next round.
- Genesis
1.3.1enables contact islands, hibernation, and native gravity. - Seventy-two off-camera physics slots settle and sleep before the first round; unused slots do not participate in continuous contact solving.
- Each cargo box has one analytic URDF
<box>collider. The original GLB is used only for textured visuals. - At
10 Hz, the stability gate first reads the current box's sleep flag. If it is still awake or the flag is unavailable, it reads full-stack velocity in one batch instead of synchronizing every physics step. - Nyx consumes Genesis visual state directly and renders Forward on CUDA.
- The green outline is one merged mesh per asset size; moving or hiding it requires a single batched pose update.
- Default placement candidates pass through a vectorized AABB broad phase, and only plausible OBB pairs enter batched NumPy SAT. No per-candidate physics query is required, even when placed boxes are tilted.
- The main loop uses an absolute-clock frame pacer, so a short stall does not create an unbounded catch-up loop.
- Genesis does not print an FPS record every physics step.
--profileemits periodic percentile summaries instead.
The following results are regression measurements from the development
machine, not a performance guarantee for every system. With an RTX 4090,
1280×800 output, 72 precreated cargo boxes, and real drops and collisions, a
Nyx stress run completed 40 placement rounds. After 60 warm-up frames, the
remaining 3,540 frames produced the results below. Full-frame time includes
the 60 Hz pacer wait, so 16.66 ms means the loop remained locked to the
target frame rate.
| Metric | p50 | p95 | p99 | max |
|---|---|---|---|---|
| Full frame | 16.66 ms | 16.66 ms | 16.66 ms | 16.67 ms |
| Active work | 1.87 ms | 2.99 ms | 5.02 ms | 12.93 ms |
| Genesis step | 0.47 ms | 0.86 ms | 1.09 ms | 2.31 ms |
| Nyx render | 1.28 ms | 1.91 ms | 2.20 ms | 3.43 ms |
The number of full frames above 110% of the frame budget was 0/3540.
Reproduce the Nyx test with:
python play/box_tetris.py \
--renderer nyx \
--max-boxes 72 \
--round-limit 40 \
--seed 11 \
--auto-place \
--steps 3600 \
--profileA separate complete headless run placed and stabilized all 72 boxes with the
same seed. Across 7,177 post-warm-up frames, active-work p99/max was
2.75/9.89 ms and Genesis-step p99/max was 2.17/3.32 ms, with no
collision-buffer overflow or abnormal exit.
The complete 72-box headless regression command is:
python play/box_tetris.py \
--renderer genesis \
--backend cpu \
--no-viewer \
--max-boxes 72 \
--max-collision-pairs 512 \
--seed 11 \
--auto-place \
--steps 10000 \
--profile \
--profile-report-steps 10000The usable loading volume is approximately 2.35 × 3.00 × 2.35 m. The door
is on the Y=0 side, and both the doorway and top remain open. At startup, the
game measures the real bounds of assets/box/*.glb and uses only assets whose
three side lengths all fall within 0.3–0.6 m:
card04_genesis.glbcard05_genesis.glbcard06_genesis.glbcube05_32cm_genesis.glbcube06_42cm_genesis.glbcube07_52cm_genesis.glb
The sequence is random; use --seed 7 for a reproducible order. Genesis cannot
add ordinary rigid bodies after scene build, so the game precreates and sleeps
72 physics slots behind the camera. Reduce capacity and the collision-pair
buffer when appropriate:
python play/box_tetris.py \
--max-boxes 24 \
--max-collision-pairs 256The default physics configuration uses 60 Hz, the Newton solver, sparse
CPU island solving, 30 main iterations, 20 line-search iterations, and a
512-pair collision capacity. Experimental no-slip post-processing is disabled
because it creates a dense buffer that grows quadratically with the number of
constraints and interferes with normal sleeping.
Use --backend cuda for A/B comparison, but CPU physics is usually faster for
this single interactive scene. Nyx rendering remains on the GPU either way.
python play/box_tetris.py \
--renderer genesis \
--backend cpu \
--no-viewer \
--max-boxes 2 \
--max-collision-pairs 256 \
--auto-place \
--steps 300 \
--profile--no-viewer disables both Nyx and Genesis Viewer. A headless invocation
without --steps executes only one main-loop step.
play/
├── box_tetris.py # Scene, physics, placement planner, and main loop
├── nyx_x11_input.py # X11 keyboard and mouse input for the Nyx window
├── preview_outline.obj # Green Nyx target outline
├── box_urdf/ # Textured visuals and analytic box colliders
├── requirements-nyx.txt # Validated Genesis/Nyx overlay versions
├── setup_nyx.sh # Installs the overlay into play/.runtime/
├── README.md # English documentation
└── README_CN.md # Chinese documentation
play/.runtime/, __pycache__/, and native-window configuration files are
generated locally, ignored by Git, and should not be committed.
If Python reports that torch, trimesh, dill, or another base module is
missing, the original Genesis environment is not active. Activate that
environment before running setup_nyx.sh. The script installs only the
validated overlay and does not install the large base dependencies.
If uv is missing, install it first or point UV_BIN=/path/to/uv to its
executable.
- No NVIDIA CUDA GPU: use
--renderer genesis. - No desktop window: use
--no-viewerfor automated runs. DISPLAY,XAUTHORITY, orlibX11error: run in an X11 or XWayland session. Native Wayland requires XWayland.- Nyx window is visible but the controls do not respond: click the window to focus it. Window discovery retries in the background and normally takes at most about one second.
The target must remain below the container ceiling and the game must be in
AIMING state. If the target is too high, the terminal prints a warning; move
it to a lower location. While the previous box is still moving, the game stays
in SETTLING and waits for the contact island to sleep or the full stack to
remain below the velocity thresholds. The terminal log reports the current
state and the result of each round.
- Confirm that the log reports Genesis
1.3.1with hibernation and contact islands enabled. - Prefer
--backend cpufor this single scene and Nyx for interactive rendering. - Check that rendering has not fallen back to software and close unrelated GPU-heavy applications.
- Reduce preallocation with
--max-boxes 24 --max-collision-pairs 256. - Add
--profileto separate Genesis-step, Nyx-render, and game-logic cost.
If Genesis reports a collision-pair buffer overflow, raise
--max-collision-pairs from 512 to 1024 or higher. Larger capacities use
more memory.
The current Nyx API cannot reliably report the native close event. Press
Esc, or return to the launch terminal and press Ctrl-C. Nyx keeps an
internal resolution of 1280×800; edge resizing depends on Nyx and the desktop
window manager.