AI-assisted project. This codebase was created with Claude (Anthropic), directed and reviewed by a human author. The pipeline is built and verified end to end on real hardware — a real Arena 7.27 slice map, a live NDI sender, a real GPU, both canvases confirmed by pixel readback. Nothing has ever run on an actual LED wall, and the warp lattice is built on a hand-authored fixture because no genuinely warped Advanced Output was available. See Status.
Recreate a physical LED rig on the screens you actually have, and play the real show onto it.
UnMapper reads a Resolume Arena Advanced Output — the same slice map driving the real wall — receives Resolume's outputs over NDI, and composites them onto a virtual reconstruction of the rig. That reconstruction can be driven out to directly connected displays, so a bank of monitors stands in for the LED, or rendered as a 3D previz of the set.
Two views of one stage, and they are not alternatives:
- Emulation — the whole rig recreated flat, one canvas pixel per LED. Each connected display shows a cropped region, so a grid of monitors becomes the wall. Pixel-exact: the crop is sampled nearest, so a region that is not the monitor's own size looks blocky rather than being quietly interpolated into something plausible.
- Previz — the same panels at their positions in 3D, through a camera.
Both are fed by the same slice map and the same live frames, so what you see in one is what the other is showing.
Early. The pipeline below is built and verified end to end on real hardware — a real Arena 7.27 file, a live NDI sender, a real GPU:
| Piece | State |
|---|---|
| Resolume Advanced Output import | Built. Tested against 4 real files. |
| NDI discovery / receive / send | Built. Verified against a live NDI 6.3.2 sender at 50 fps. |
| Emulation canvas (wgpu) | Built. Verified by pixel readback. |
| 3D previz camera (wgpu) | Built. Verified by pixel readback. |
| Corner-pinned slice sampling | Built (projective). |
| Warp lattices | Built — a warped slice is drawn through its lattice, one quad per cell. See the caveat below. |
| Stage file (XML) | Built. Lossless round trip, hand-editable. |
| Desktop GUI | Built. Live NDI in the viewport, drag-to-place, save/load. |
| Output to connected displays | Built. One window per monitor, each a pixel-exact crop. |
| 2D backdrop mockup | Built. Viewport only — never reaches an output. |
| 3D set model (glTF/GLB) | Built. Node transforms baked, depth-tested with the panels. |
| NDI output | Built. Publishes a canvas region, or the previz camera, as an NDI source. |
| Previz to an output | Built. A window or an NDI source showing the camera view. |
| CLI | Built — import, bind, check, render, sources. |
| Syphon / Spout publishing | Not built yet — use NDI output instead. |
| Non-planar panels | Built — a panel surface can be flat, an arc, or a measured lattice. Previz only. |
Nothing has been run on a real LED wall or in a venue.
UnMapper imports one panel per slice, and a slice routinely covers a whole run of tiles — a curved upstage wall, a wrapped column, a folded corner. Drawing that as one flat rectangle is exactly the thing previz is supposed to fix, so a panel carries a surface: flat, an arc of a given sweep, or an explicit lattice of measured points.
Surfaces shape the previz view only. The emulation canvas stays flat and pixel-exact whatever the surface says, because one canvas pixel is one LED and an output crops that canvas to a monitor standing in for a piece of the rig.
An operator warps a slice in Resolume when the surface it feeds is not a flat rectangle: the content is pre-distorted so it lands straight on a curved wall. The LED processor still reads a plain rectangle out of the raster, so showing that rectangle shows the pre-distorted image — what goes down the wire, and not what an audience ever sees. UnMapper reads through the lattice instead, so the panel shows what the wall will show.
The honest limit: every Advanced Output available while this was written had an
untouched lattice on every slice. Where the control points live and what order
they come in are pinned against real Arena 7.27 files, because an untouched
lattice is a regular grid and that is checkable. What a warped export actually
looks like is not — the warped test fixture was authored by hand and says so in
its header. A Point Mode other than PM_LINEAR, and a homography that does
more than restate the output rect, are both reported and not applied.
The GUI is the intended way in. Import an Advanced Output, pick an NDI source for each Resolume output, drag the panels into the shape of your rig, and save.
cargo run -p unmapper-guiIt also opens a stage directly:
cargo run -p unmapper-gui -- rig.unmapper.xmlEverything it does is available headlessly too:
cargo run -p unmapper-app -- sourcescargo run -p unmapper-app -- import "AdvancedOutput.xml" -o rig.unmapper.xmlcargo run -p unmapper-app -- bind rig.unmapper.xml --source 0 --ndi "STUDIO (Arena - Screen 1)"cargo run -p unmapper-app -- render rig.unmapper.xml -o wall.pngcargo run -p unmapper-app -- render rig.unmapper.xml --previz -o previz.png --size 1280x720Naming these is most of the battle, and mixing two of them up is the difference between a correct wall and a plausible-looking wrong one.
| Space | Units | Holds |
|---|---|---|
| Composition | px | Resolume's composition raster. A slice's input quad. |
| Screen raster | px | One Resolume output. A slice's output quad. |
| Virtual raster | px | UnMapper's emulation canvas — the whole rig, flat. A panel's layout. |
| Stage | metres | The physical set, Y up. A panel's placement. |
| Display | px | A connected monitor. An output's region crops the canvas into one. |
The one thing to get right: a slice's pixels live in two places, and which
one to sample depends entirely on what the sender is sending. If Resolume sends
its whole composition, sample the slice's input quad. If it sends one feed per
screen — the usual show configuration — that feed already has the slicing
applied, so sample the output quad. SourceSpace records which.
Two ways to describe the set, and they are not alternatives — an operator with both should not have to choose.
A 2D mockup — a render, plan or photo of the display surface, sitting behind the panels on the emulation canvas so they can be dragged onto the places they occupy in it. It is an editing aid and never content: the viewport draws it, and the canvas that outputs crop from does not. Fade it with the opacity slider so panels stay readable over a busy render.
A 3D set model — glTF or GLB, as exported by Blender, Cinema 4D, SketchUp or
anything else. Only geometry is read; materials, cameras and animation are
ignored, because this is context for judging where the walls sit, not a render.
Nested node transforms are baked at load, so a truss rotated inside a rig group
arrives where the file says it is. CAD is usually in millimetres — there is a
one-click mm→m button for exactly that.
<Geometry>
<Backdrop path="art/set-render.png" opacity="0.6">
<Rect x="0" y="0" width="1920" height="1080"/>
</Backdrop>
<Model path="cad/set.glb" scale="0.001">
<Translation x="0" y="0" z="0"/>
<Rotation x="0" y="0" z="0" w="1"/>
</Model>
</Geometry>A source does not have to be NDI. Two offline kinds exist so a rig can be laid out and checked with no Resolume, no network and often no venue:
- Test pattern — a grid with four differently-coloured corners and a centre
cross, sized to whatever the slice map says the screen is. Because the corners
differ, a slice that is flipped, rotated or sampling the wrong region is
obvious at a glance rather than plausible-looking. It is a geometry aid, not
a colour reference —
test-cardin this fleet does that properly. - Still — any image, for laying a rig out against real artwork.
<Source id="src-9001" name="LED Processor 1" enabled="true">
<TestPattern/>
<ScreenRaster screen="9001"/>
<Expected width="1920" height="1080"/>
</Source>The canvas is rendered once per frame at full resolution; every output then blits the region it stands in for. Ten monitors cost one render and ten blits, and none of them can disagree about which frame they are showing.
Add outputs in the GUI's Outputs panel, or by hand:
<Output id="out-left" name="Wall Left monitor" enabled="true">
<Display index="0" fullscreen="true"/>
<Emulation x="0" y="0" width="960" height="1080"/>
<Size width="960" height="1080"/>
</Output>New outputs are created windowed, not fullscreen — a fullscreen window that opens on the wrong monitor is unpleasant to get rid of. Tick the box once it looks right. Closing an output window disables that output rather than quitting.
A stage saves as XML you can read, diff and hand-edit:
<UnMapperStage version="1" name="two-panel-wall">
<VirtualRaster width="1920" height="1080"/>
<Sources>
<Source id="src-9001" name="LED Processor 1" enabled="true">
<Ndi name="STUDIO (Arena - Screen 1)"/>
<ScreenRaster screen="9001"/>
<Expected width="1920" height="1080"/>
</Source>
</Sources>
<Panels>
<Panel id="panel-9001-9101" name="Wall Left" enabled="true">
<Pixels width="960" height="1080"/>
<Layout x="0" y="0" width="960" height="1080"/>
<Placement>
<Translation x="-1.248" y="1.404" z="0"/>
<Rotation x="0" y="0" z="0" w="1"/>
<Size width="2.496" height="2.808"/>
</Placement>
</Panel>
</Panels>
<Bindings>
<Binding panel="panel-9001-9101" source="src-9001" slice="9101">
<SourceQuad>
<v x="0" y="0"/><v x="960" y="0"/><v x="960" y="1080"/><v x="0" y="1080"/>
</SourceQuad>
</Binding>
</Bindings>
</UnMapperStage>Quads are four <v x= y=> corners in Resolume's own order, so anyone who has
read an Advanced Output recognises them. The round trip is exact — saving a
stage twice gives byte-identical files.
crates/
unmapper-core domain model — spaces, panels, bindings, the show
unmapper-resolume Advanced Output reader
unmapper-stagefile the stage XML format
unmapper-ndi NDI, loaded at run time rather than linked
unmapper-render wgpu — emulation canvas and previz camera
unmapper-gui the desktop application
unmapper-app the `unmapper` CLI
diag vendored fleet diagnostics
The NDI runtime is loaded at run time, never linked. That is a licensing requirement, not a build convenience: the NDI licence permits redistribution only if the licence you ship under forbids reverse-engineering the SDK, and UnMapper is MIT, which grants exactly that right. Loading at run time means no NDI code is distributed and only the flat C ABI is named.
A machine with no runtime still builds and runs — NDI sources are simply unavailable, with the download URL in the error. Install the NDI Tools or SDK to enable them.
There is no CI for this repo yet, so this is the release:
scripts/release-local.shIt runs the tests and clippy, builds a universal macOS binary, and wraps the GUI
in a double-clickable UnMapper.app with the CLI beside it. --fast builds this
machine's architecture only.
macOS-only for now. Windows and Linux builds are possible with cargo-xwin and
cargo-zigbuild, but nothing here has ever been run on either, so shipping
binaries for them would imply a confidence that does not exist.
The bundle is unsigned, so macOS quarantines it on first open — right-click →
Open, or xattr -dr com.apple.quarantine dist-release/UnMapper.app.
This project is built on other people's work — see ATTRIBUTIONS.md.
MIT. See LICENSE.
NDI® is a registered trademark of Vizrt NDI AB. This project is not affiliated with Vizrt, Resolume, or any LED manufacturer.