A focused native-wgpu example that ports ShadeCore’s NDI output concept into the Junkpile renderer family.
The application renders one authoritative wgpu texture, previews it in a native window, reads frames back through a bounded three-slot GPU pipeline, converts RGBA into BGRA, and publishes a progressive NDI source from a dedicated worker thread.
Authoritative wgpu texture
├── native window preview
└── three-slot GPU readback
└── reusable CPU BGRA buffers
└── bounded two-frame worker queue
└── NDI source
└── NDI Video Monitor / Studio Monitor
The render thread never waits for an unbounded NDI queue. If GPU readback, CPU buffer packing, or the sender worker cannot keep up, the corresponding drop counter increases while the native preview continues.
ndigrafton-ndi 0.9.0ShadeCore’s OpenGL readback normally starts at the bottom-left, so ShadeCore defaults to vertical flipping. wgpu texture readback is already represented as top-left in this example, so vertical flip defaults to off. The option remains available for testing or unusual downstream workflows.
This project deliberately uses the same Rust NDI integration as the supplied ShadeCore codebase.
You need:
grafton-ndi, because the Rust crate builds against the installed SDK.Common macOS SDK locations include:
/Library/NDI SDK for macOS
/Library/NDI SDK for Apple
/Library/NDI 6 SDK
If your SDK is elsewhere:
export NDI_SDK_DIR="/path/to/your/NDI SDK"
Having the official monitor installed confirms the receiver/runtime side, but the SDK headers can still be a separate installation requirement.
The NDI library is installed as libndi.dylib and uses the install name @rpath/libndi.dylib. Revision 0.1.1 detects the active SDK, adds its lib/macOS directory to the executable runtime search paths, and also includes application-relative rpaths for future bundling. This prevents the project from compiling successfully and then failing at launch with:
Library not loaded: @rpath/libndi.dylib
After changing NDI_SDK_DIR or installing a different SDK, clean the application crate before rebuilding:
cd src-tauri
cargo clean
cd ..
npm run dev
For a one-session diagnostic workaround, the equivalent shell setting is:
export DYLD_LIBRARY_PATH="$NDI_SDK_DIR/lib/macOS${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
npm run dev
cd 32-wgpu-ndi-sender
npm install
npm run dev
The normal dev and build commands enable the ndi feature.
A renderer-only diagnostic build is also available:
npm run dev:no-ndi
That build intentionally refuses to start an NDI sender, but it lets the rest of the application compile without the optional NDI dependency.
1920 × 1080, 60 fps, and source name Junkpile 32.Junkpile 32.The receiver can display a machine-qualified name such as:
Computer Name (Junkpile 32)
The native renderer and the NDI monitor should show the same animation.
| Control | Purpose |
|---|---|
| Source name | Name advertised to NDI receivers |
| Groups | Optional comma-separated NDI groups |
| Resolution | 720p, 1080p, 1440p, or 4K |
| Frame rate | 24, 30, or 60 FPS |
| NDI clocks video | Lets the NDI sender clock outgoing video |
| Vertical flip | Reverses row order before BGRA publication |
| Start NDI | Applies the settings and creates the source |
| Stop NDI | Destroys the source and closes the worker |
| Reset metrics | Clears the pipeline counters |
Settings are locked while the sender is active. Stop NDI before changing the source format.
The controls window separates pressure at each stage:
| Metric | Meaning |
|---|---|
| Capture requests | Frames requested by the NDI cadence clock |
| GPU readbacks | Frames successfully copied back from wgpu |
| NDI frames sent | Frames handed to the NDI SDK |
| GPU drops | All readback slots were busy |
| CPU pool drops | No reusable BGRA buffer was available |
| Worker drops | The bounded two-frame NDI queue was full |
| Worker pending | Frames waiting for the NDI worker |
| CPU buffers free | Reusable full-frame buffers currently available |
| Readback slots busy | GPU buffers currently mapping |
| Raw frame bandwidth | Uncompressed RGBA/BGRA traffic before NDI compression |
A smooth monitor image with zero drops means the local pipeline is healthy. Receiver-side network or monitor issues can still occur independently.
Start with 1080p60. Then test:
720p60
1080p60
1440p60
4K30
4K60
4K60 is intentionally available rather than artificially blocked, but it has a much larger readback and network cost. Use the real-time counters to decide whether the hardware and network can sustain it.
Official NDI documentation notes that frame widths should be divisible by two; this example validates that requirement before reconfiguring the wgpu target.
Confirm the NDI SDK is installed and set NDI_SDK_DIR if necessary. The official monitor alone may provide the runtime without the SDK headers used at build time.
Enable Vertical flip. It is off by default because the wgpu path is already top-left.
Lower resolution or frame rate and identify the stage:
32-wgpu-ndi-sender/
├── config/ndi.json
├── src/
│ ├── index.html
│ ├── app.js
│ └── styles.css
└── src-tauri/
└── src/
├── main.rs
├── renderer.rs
├── ndi.rs
├── preview.rs
├── frame.rs
├── source.wgsl
└── preview.wgsl
This is an NDI sender example. It does not receive NDI streams, send audio, or provide NDI HX encoding. Receiver-side NDI belongs in a later Junkpile input example.