fal.wma) to stream the video over WebRTC.
The clip above is a recording of this app on one H100. The caption shows each commands message as the client sent it.
The example shows the CommandWorld interaction profile:
- The world starts when the session starts. The first video frame tells the client that the world is ready.
- The client sends the commands that are held and the commands that were tapped.
- An optional
setupmessage chooses the seed image. Aresetmessage restarts the world.
What Is a World Model
A world model generates video that responds to what you do. A video model turns a prompt into a clip of fixed length. A world model produces the next frames one chunk at a time, and each chunk depends on the frames before it and on the latest actions from the user, such as movement and camera keys. Because the user’s input shapes every new frame, the output works like a game: you can walk around, look around, and see the world change in response. That loop has two consequences for how you serve it:- Frames must arrive in real time. The app streams the generated frames to the client as a video track, not as a finished file. This is why the app streams over WebRTC.
- Each session holds state. The model keeps the world it has already generated, so a session belongs to one user and runs on one GPU for as long as it lasts.
Key Facts
Prerequisites
- A fal account with permission to deploy and call the app. The browser client connects through fal’s WMA service at
wma.fal.run. It needs a fal API key on your server, or a signed-in dashboard session for the playground. - Access to the fal WMA TURN servers for the account that owns the app (the
Xinfal.run/X/app). Request it from fal before you rely on the app. Without it, a session connects only when the runner is directly reachable, which fails on many runners and networks. See If the Session Does Not Start. - Python 3.12 on your computer. The app image uses Python 3.12, and the Python versions must match for deployment.
fal1.82.1 and authentication. Run these commands:
- An H100 machine type. On one H100 the app uses about 14 GB of GPU memory.
/data:
- The upstream code from SkyworkAI/Matrix-Game at commit
c7a88bf. Only theMatrix-Game-2folder is checked out. - The weights from
Skywork/Matrix-Game-2.0at revision08c42d1, about 12 GB for the files that the app needs.
The App
View the complete source code on GitHub. This page shows the parts that use the WMA SDK.The Contract
The contract declares one video track and theCommandWorld profile.
The Commands message holds the eight semantic commands.
The contract describes the app to the playground and to clients. The handlers do the real work.
The values "/active" and "/activated" are JSON Pointers into the commands message.
The Control Handlers
The app registers one handler for each message type. The handlers run on the session event loop.Billing in the Generation Loop
The generation thread bills each frame after it queues the frame. If billing is already final, the session is closing and the thread stops.The App Class
The class sets the runner, the pinned requirements, and a longstartup_timeout for the weight download.
The keep_alive value keeps a runner up for 300 seconds after its last session, and fal bills that idle time. The value min_concurrency=0 scales the app to zero, so the first session waits for a cold start. The max_concurrency value is the most runners, and max_multiplexing is the most sessions on one runner. Both are example limits. Adjust them for your workload and cost. See Scale your application.
The Session Backend
Thecreate_backend method accepts one session at a time. It then starts the generation thread and the WebRTC peer.
The lines that build the ICE servers (the RunnerIceConfig call) are boilerplate. Copy them as they are.
torch and the upstream code inside Engine.load().
How the Contract Maps to Controls
The app declares eight semantic commands. The client does not send physical key names. The browser, a gamepad, or an agent can map any input to these commands.
Each
commands message reports the complete held state in active. It reports newly tapped commands in activated.
fal.wma.KeyState merges both lists. It keeps a tap for one generation step and a held command until the client releases it.
The generation thread calls KeyState.sample() once for each chunk.
A new tap wins over an older held command in the same conflict group. If the client holds both commands of a group, KeyState keeps both, and the app cancels them.
The model works in chunks. One chunk is three latent frames, which decode to 12 video frames. The first chunk decodes to 9 frames.
All frames of a chunk use the same keyboard and mouse values.
This is how the upstream inference_streaming.py script conditions the model. The app replaces its terminal prompt with the sampled state.
The other messages follow the CommandWorld profile:
Run and Deploy
Run these commands from the root of the cloned repository. Usefal run as the development loop. It starts the app on an ephemeral runner.
Cost and First Start
You pay per second for the runner, including setup time and the idlekeep_alive time. See Pricing. The billable units in the app matter only if you list the app on the fal Marketplace. Callers then pay for these units at a price that the fal team sets.
The first session starts a runner, and that start is slow. The runner installs the requirements, then downloads and loads about 12 GB of weights. In our tests, this took 5 to 20 minutes, so the app sets startup_timeout to 1800 seconds. You can follow the progress in the runner logs in the dashboard.
Later starts are faster because the weights stay on the /data volume across runners. To skip the cold start, set min_concurrency = 1 to keep one runner warm. You then pay for that runner while it is idle. See cold starts.
Clean Up
To stop paying for the app, scale it to zero withfal apps scale <app-name> --min-concurrency 0. To remove it, run fal apps delete <app-name>.
Try It
Open the deployed app in the fal dashboard. In the Playground tab, select the/start-session endpoint.
- Select Connect to start a session. The first frame shows that the world is ready.
- Hold a movement or look control, or its keyboard key, to move. Release it to stop.
- Use the Restart world control to reset the world.
Browser Client
For your own page, connect with the experimental WMA browser client. There is no Python or Node client today. The deploy output shows the app ID asyour-account/<app-name>. The name comes from the class name, so MatrixGameApp becomes matrix-game-app.
Install the client and the server proxy in your web application. Set up the proxy route as the browser client page describes.
onData callback receives the raw JSON string. Parse it before you use it.
Enable the controls after the video element fires playing. The blur handler releases all held keys when the tab loses focus.
A session can end on its own. The stream_exhausted message closes the session when the world reaches its length limit. In that case, open a new session. To reconnect, wrap fal.realtime.open(...) and the ended flag in a function.
The try and catch around await session.ready handle a failed start.
A failure reaches both onError and onState. The ended flag makes endSession run once.
Server Messages
If the Session Does Not Start
Use Your Own Model
The model is theEngine class. It has three parts.
load()loads the weights once for each runner.new_world(scene)starts a world from a seed image.step(world, keys)returns a chunk ofuint8frames with the shape[T, H, W, 3].
- A generation thread calls
stepin a loop. It reads the sampled keys and checks for a restart before each chunk. - The thread puts each frame in a bounded queue, with the
pushmethod. The method blocks while the queue is full. make_video_queue_trackplays the queue as the video track, andAiortcPeersends it to the client.
Billing
This section applies if you list the app on the fal Marketplace. For your own private app, you pay only for runner time, and the units do not change the cost. The app callssession.add_billable_units(1) for each frame that it queues for playback. One unit is one frame.
Some frames never play. The app drops the queued frames when a reset or setup message restarts the world, and some frames remain queued when the session ends.
Before billing is final, a deferred cleanup calls session.cap_billable_units(total - undelivered). The argument is the new total, so the undelivered frames are not billed.
The WMA guide describes this billing pattern. fal reports the total when the session closes.
The fal team configures the price per unit when it lists the app.
Limits
- Each runner serves one session at a time. With
max_concurrency = 2, the example serves at most two sessions at once. - A world lasts at most 1437 video frames, about 57 seconds at playback speed. The app then sends
stream_exhaustedand closes the session. Useresetto start again before that point. - The app plays video at 25 frames per second, the rate that upstream states for real-time generation. In our tests on one H100, generation ran at about 19 to 23 fps at 640x352, which is slower than playback, so motion can look bursty. The first frame arrived 1 to 4 seconds after we connected.
- A key change takes effect at the next chunk. The delay is the time to generate a chunk plus the frames that wait in the queue.
- Upstream notes that an upward camera movement can cause brief rendering glitches, such as black frames.
- The example supports the
universalmodel only. The upstreamtemplerunmodel uses different weights and a different command set.