Skip to content

tetrotibo/MLX-Python-Guide-to-the-Galaxy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

69 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

MLX Python guide for 42 school projects β€” pixel buffer rendering, mlx_loop, input handling, common errors, and diagnostics.

This guide was written alongside A-Maze-ing, a school project by tdarscot and tdhenain at 42 Belgium.

About

This guide exists because the MLX Python binding has almost no documentation and many students hit the same walls. Everything here is based on my original research and experimentation done during the development of A-Maze-ing.

▢️ A-Maze-ing demo: https://www.youtube.com/watch?v=TnMn02CwirY

The goal is not to hand you ready-made solutions - it is to save you the days of searching that should not be necessary just to open a window and draw a pixel. This is the guide that would have been useful as I started developing the visualizer for A-Maze-ing: enough to get unstuck, not enough to skip the discovery. The progressive understanding of how MLX works, and the moment where everything falls into place, is yours to experience.

MLX has more to offer than what is covered here. Bitmap fonts, sprites, elaborate UI and other techniques are left for you to find. This guide serves as a bridge, from completely lost to having a solid foundation you can build your own project on.

Installation

Platform Status
Linux AMD64 🟒 Fully supported - tested on Ubuntu 22.04 AMD64
Mac Silicon 🟒 Working via Docker --platform linux/amd64 - tested on Ventura 13.4 (Mac Studio M2)
Mac Intel 🟑 Should work - not tested
Windows 🟑 WSL2 with WSLg may work - not tested

Mac Silicon: Docker setup (skip if on other platform)

Step 1 β€” Install XQuartz

brew install --cask xquartz

Or download from xquartz.org. After installation, log out and back in.

Step 2 β€” Enable network connections in XQuartz

Open XQuartz β†’ Settings β†’ Security β†’ check "Allow connections from network clients". Log out and back in for the setting to take effect.

Step 3 β€” Open XQuartz and allow local connections

Open XQuartz first, then in your terminal (Mac, outside Docker):

xhost + localhost

Run this every time before starting the container.

Step 4 β€” Start the container with a shell attached

docker run -it --platform linux/amd64 --name mlxguide -e DISPLAY=host.docker.internal:0 -v /tmp/.X11-unix:/tmp/.X11-unix ubuntu:22.04 bash

This starts the container and drops you directly into a shell inside it. All following steps run inside this shell.

Installation steps (all platforms)

Step 1 β€” Install Python and pip

⏱️ Can take up to a minute to complete, be patient!

apt-get update && apt-get install -y python3 python3-pip

Step 2 β€” Install git

apt-get install -y git

Step 3 β€” Clone the repository

git clone https://github.com/tetrotibo/MLX-Python-Guide-to-the-Galaxy.git mlxguide
cd mlxguide

Step 4 β€” Install X11 and Vulkan dependencies

apt-get install -y libx11-dev libxext-dev libxcb-keysyms1 libvulkan1

Step 5 β€” Install the MLX wheel

pip3 install 00_install/mlx-2.2-py3-none-any.whl

Step 6 β€” Verify

python3 01_modules/M01_init.py

A window should open on your Mac. Press ESC or click the X button to close it. If it opens, everything is working.

Troubleshooting

munmap_chunk(): invalid pointer / Aborted MLX crashed before connecting to the display. Check that:

  • XQuartz is open on your Mac before starting the container
  • You ran xhost + localhost in a Mac terminal before starting the container
  • The DISPLAY variable is set correctly β€” run echo $DISPLAY inside the container, it should return host.docker.internal:0

libmlx.so: No such file or directory The container is running as arm64 instead of amd64. Make sure your docker run command includes --platform linux/amd64.

Window doesn't appear but no error Run echo $DISPLAY inside the container. If it returns empty, the environment variable was not passed in. Stop the container and restart it with the full docker run command above.

pip3: command not found Run Step 1 again β€” python3-pip may not have installed correctly.

Repo Structure

Folder Contents
00_install/ MLX wheel
01_modules/ Module files (M01–M06)
02_common_errors/ Error demonstrations (E01–E07)
03_diagnostics/ Diagnostic scripts (D01–D03)
04_broken_mlx_functions/ Broken function demos (B01)
05_template/ Blank starter template

Modules

File Topic
M01 - init MLX init, window creation, clean shutdown
M02 - image buffer Image buffer, write_pixel(), write_rect()
M03 - tile grid Tile coordinates, inset tiles, wall bitmasks
M04 - draw order Draw order, compositor pattern, UI split
M05 - text mlx_string_put(), color conversion, draw order
M06 - interactive Input handling, game loop, deferred pattern
Screenshots

M02 β€” Image buffer
Six solid-color rectangles drawn with write_rect() β€” the foundation of every visual in this guide. M02

M03 β€” Tile grid
A full tile grid with inset rendering and wall bitmasks. The two blue tiles mark entry and exit. M03

M04 β€” Draw order
The compositor pattern in action: background, path, pattern, and UI panel each drawn in the correct order. M04

M05 β€” Text and color constants
All C_ color constants rendered as swatches with their hex values and names, drawn with mlx_string_put(). M05

M06 β€” Interactive
The full interactive loop: arrow key movement, SPACE to regenerate, and a live UI panel with keybinds. M06

Common Errors

File Error
E01 - bounds write_rect() overflow - silent corruption vs crash
E02 - no loop Missing mlx_loop() - window closes immediately
E03 - mask key press Wrong mask on key hook - keys never fire
E04 - x button Missing close handler - X button has no effect
E05 - sync loop mlx_loop() is single-threaded and synchronous
E06 - text overwrite mlx_string_put() erased by mlx_put_image_to_window()
E07 - str color mlx_string_put() expects 0xBBGGRR not 0xRRGGBB

Diagnostics

File Topic
D01 - memory leak mlx_new_image() without mlx_destroy_image()
D02 - frame rate Measuring effective FPS via mlx_loop_hook()
D03 - key repeat X11/XQuartz key autorepeat behavior

Broken MLX Functions

File Topic
B01 - pixel put 01 mlx_pixel_put() called before mlx_loop() - black
B01 - pixel put 02 mlx_pixel_put() called from key handler - black
B01 - pixel put 03 mlx_pixel_put() with explicit flush - black

Conventions

Tile coordinates

Tile indices are always converted to pixel positions via:

def tile_px(tile_col): return (tile_col + BORDER) * TILE_SIZE
def tile_py(tile_row): return (tile_row + BORDER) * TILE_SIZE

Inset tiles

draw_tile() fills only the interior of a tile, leaving WALL_SIZE pixels on each edge as a gap. Without this, two adjacent same-color tiles bleed into a solid block with no visible separation at open passages.

Wall rendering

All modules use the 4-walls-per-cell approach: each cell owns and draws all 4 of its wall strips, trimmed at both ends by WALL_SIZE to avoid corner overlap. This leaves a WALL_SIZE x WALL_SIZE junction gap at every corner - almost invisible at WALL_SIZE = 1, visible at WALL_SIZE = 2+.

A cleaner alternative (west + north walls only) is documented in M03 but not used in the guide.

Draw order

MLX has no real layers - just one flat pixel buffer. The last write_rect() call to touch a pixel wins. The correct draw stack:

1. Background fill + floor tiles
2. Pattern tiles
3. Path tiles
4. Entry / exit tiles  - must come after path
5. Walls               - conventional, not required with inset tiles
6. UI background
7. UI content

Color Constants

All constants use 0xRRGGBB with a C_ prefix. write_rect() handles the BGR conversion internally.

Constant Role
C_BG Full-window background fill
C_FLOOR Default tile interior
C_WALL Wall strip color
C_ENTRY Entry tile
C_EXIT Exit tile
C_PATH Solution path tile
C_PATTERN Decorative highlight tile
C_UI_BG UI panel background
C_UI_ACTIVE UI panel highlighted element
C_UI_INACTIVE UI panel default element

Wall Bitmask Format

Used in M03 onwards to encode which walls are present on a cell:

bit 3 (0b1000) = West
bit 2 (0b0100) = South
bit 1 (0b0010) = East
bit 0 (0b0001) = North

Example: 0b1010 = West + East walls (horizontal corridor). Extracting a bit: (bitmask >> bit_position) & 1

Keyboard Events

Event X11/XQuartz number mlx_hook mask
EVENT_KEY_PRESS 2 1
EVENT_KEY_RELEASE 3 2
EVENT_WINDOW_CLOSE 33 0

Deferred pattern

Key handlers run synchronously inside mlx_loop() - there is no parallelism. While a handler executes, the loop is paused and no redraws happen. The deferred pattern lets you render visual feedback before work runs on the next frame:

# In key_press_handler:
pending_action = True
render_scene()     # feedback visible NOW, before work runs

# In game_loop - one frame later:
if pending_action:
    pending_action = False
    do_work()
    render_scene()

The freeze still happens - the pattern ensures feedback appears before it. See M06 and E05 for full demonstrations.

Thanks

amaazouz β€” for his curiosity about MLX and A-Maze-ing, which pushed me to document what I'd learned.

dloic β€” for his completely different approach to wall rendering that made me think harder about my own, and for being just as frustrated by the lack of MLX documentation - which is part of why this guide exists.

tdhenain β€” for his partnership throughout A-Maze-ing.

cgazen and kprist β€” for discovering the close window fix that saved everyone.

gwfranco β€” for his terminal-based take on A-Maze-ing.

gdupret β€” for his last-minute tips that always arrive exactly when they are needed.

mhummels, syalcin and aouassar β€” for their feedback on A-Maze-ing.

Also thanks to mhummels, gdupret, gwfranco, cgazen, kprist, and dloic for sharing their A-Maze-ing β€” seeing different approaches to the same project was invaluable.

About

A hands-on guide to the Python MLX binding for 42 school projects - modules, common errors, diagnostics, and a full function reference.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages