Folder: feedback-tauri-v2-ws-template
Collection: Tauri v2 Essentials
Renderer: Raw WebGL 1 / GLSL ES 1.0
Window model: Two Tauri v2 WebView windows connected by an embedded loopback WebSocket relay
Example 09 combines the persistent GPU-feedback lesson from Example 08 with a two-window control architecture. It demonstrates how to:
ws://127.0.0.1:2727;getUserMedia(), the MediaStream, the hidden video element, camera texture uploads, WebGL programs, and framebuffer state in one renderer WebView;The example remains deliberately focused. It is not a recorder, compositor, media router, authenticated network service, or native camera framework.
A normal WebGL pass is stateless. A feedback renderer becomes temporal by making the previous output one of the next frame’s inputs.
Current webcam texture ───────────────┐
▼
Previous feedback texture → Simulation program → Next feedback texture
│
▼
Display program
│
▼
Visible canvas
The two-window version adds another concern: user controls and renderer-owned resources live in separate WebViews.
Controls WebView
parameters + camera intent + pause state
│
│ JSON over ws://127.0.0.1:2727
▼
Embedded Rust relay
│
▼
Canvas WebView
camera + video decoder + WebGL + brush + FBO history
A MediaStream, WebGL texture, or framebuffer cannot be serialized and sent through this relay. The protocol therefore carries only commands, numeric state, and diagnostics.
MediaStream and camera track;HTMLVideoElement;canvas WebView fullscreen state.Operating-system camera
↓
getUserMedia in canvas WebView
↓
hidden HTMLVideoElement
↓
new decoded frame detected
↓
gl.texImage2D(..., video)
↓
u_webcam sampler2D
The camera stream never crosses the WebSocket. Stopping the camera replaces its texture with a one-pixel dark placeholder, while the simulation and brush remain operational.
WebGL cannot safely sample from a texture while that same texture is attached as the active render destination. The renderer therefore uses two physical targets:
Frame N
Read target B → write target A → display A → swap roles
Frame N + 1
Read target A → write target B → display B → swap roles
The simulation program stores state. The display program applies palette mapping and tone mapping without feeding that presentation color back into the simulation.
Pointer on renderer canvas
↓
normalized canvas UV
↓
u_mouse + u_mouseDown + u_brushSize
↓
localized simulation-state injection
Reaction-Diffusion mode injects the V activator. The other modes inject a time-varying color.
| Mode | Stored-state behavior | Camera behavior |
|---|---|---|
| Echo Trail | Previous RGB is multiplied by Decay | Current camera RGB is added every frame |
| Fluid Smear | Previous state is advected through curl noise | Camera edges increase flow and camera RGB is injected |
| Reaction-Diffusion | Red and green store Gray-Scott U and V chemicals | Camera luminance injects V and removes U |
| Thermal | A nine-tap neighborhood diffuses stored energy | Camera luminance becomes a continuous heat source |
| Mirror Echo | Previous state contracts through a six-sector symmetry fold | Camera is sampled through the same kaleidoscopic fold |
| Glitch Memory | Previous RGB is block-shifted with chromatic offsets | Camera is injected with matching aberration |
Changing modes clears both physical feedback targets because the simulation branches interpret stored channels differently. Reaction-Diffusion initializes with U = 1; image modes initialize near black.
The renderer prefers floating-point state because several modes can accumulate values outside the normalized byte range. WebGL 1 support differs across systems, so the app performs an actual framebuffer-completeness test:
Try RGBA + FLOAT target
↓ complete? yes → use floating-point feedback
↓ no
Use RGBA8 unsigned-byte feedback
The selected format is shown in both the controls diagnostics and renderer HUD. RGBA8 remains functional, but subtle gradients and chemical state can quantize more visibly.
state-sync
Complete parameter and paused state.
param
One coalesced parameter update.
action
set-mode, set-paused, clear-feedback, reset-feedback, or fullscreen.
camera-command
enumerate, start, or stop, with an optional deviceId.
request-state
Ask an existing renderer to replay readiness and telemetry.
canvas-ready / request-state
Renderer is ready for state restoration.
runtime
Renderer ready, camera idle, running, paused, or error state.
diagnostic
One camera, shader, FBO, GPU, resolution, or FPS field.
error-log
Camera, WebGL, or Tauri error details.
camera-devices
Device IDs and labels reported by the canvas WebView.
camera-status
Requesting, running, stopped, ended, or error.
paused-state
Authoritative pause state, including canvas keyboard actions.
renderer-state
Complete parameters after a canvas-side reset or mode shortcut.
Slider input is collected in a Map and flushed through requestAnimationFrame(). Repeated motion of one slider replaces its pending value rather than generating unbounded socket traffic.
The controls window is the authority for user-facing state. When the canvas reconnects, controls resend:
The renderer avoids restarting an already-running camera when the same device is requested after a controls-only reconnect.
Persistent feedback history is GPU-local state. It cannot survive a canvas reload or WebGL context loss. A new renderer correctly begins with newly initialized framebuffer targets rather than pretending that prior texture history was restored.
src/controls.htmlDefines the Junkpile control shell, connection pills, camera controls, six mode buttons, simulation parameters, palette controls, brush size, renderer diagnostics, and action buttons.
src/controls.jsContains:
src/canvas.htmlDefines the full-window renderer canvas, camera-status notice, renderer HUD, fatal error layer, and hidden video element.
src/canvas.jsContains:
src/styles.cssProvides a shared visual language for both windows. The controls shell, panel, and child layout use explicit min-height: 0 and panel-local overflow-y: auto, so scrolling works immediately on first launch without resizing the window.
src-tauri/src/main.rs starts embedded WebSocket listeners on:
127.0.0.1:2727
[::1]:2727
Each connection identifies itself as controls or canvas. The relay broadcasts each text message to all other connected clients.
The toggle_canvas_fullscreen command retrieves the configured renderer through:
app.get_webview_window("canvas")
src-tauri/capabilities/main-capability.json grants core:default to both configured WebView labels.
src-tauri/Cargo.toml includes a standalone workspace boundary:
[workspace]
resolver = "2"
This prevents Cargo from walking upward into an unrelated parent workspace.
src-tauri/Info.plist includes NSCameraUsageDescription for the system permission prompt.
src-tauri/entitlements.plist includes the camera entitlement referenced by tauri.conf.json.
feedback-tauri-v2-ws-template/
├── README.md
├── package.json
├── package-lock.json
├── src/
│ ├── controls.html
│ ├── controls.js
│ ├── canvas.html
│ ├── canvas.js
│ └── styles.css
└── src-tauri/
├── build.rs
├── Cargo.toml
├── Cargo.lock
├── tauri.conf.json
├── Info.plist
├── entitlements.plist
├── capabilities/
│ └── main-capability.json
├── icons/
└── src/
└── main.rs
cd feedback-tauri-v2-ws-template
npm install
npm run dev
Both windows open automatically. Camera permission belongs to the renderer window because that WebView calls getUserMedia().
npm run build
Tauri writes platform bundles below:
src-tauri/target/release/bundle/
Unsigned or unnotarized macOS applications may be blocked by Gatekeeper when moved to another computer.
Shortcuts are available in both windows unless focus is inside a form control.
| Key | Action |
|---|---|
C |
Start or stop camera |
Space |
Pause or resume simulation |
X |
Clear feedback state |
R |
Restore defaults and reset simulation time |
F |
Toggle canvas fullscreen |
1–6 |
Select feedback mode |
Canvas-side pause, reset, and mode shortcuts report authoritative state back to the controls window.
The controls window displays telemetry generated by the renderer:
Render FPS and upload FPS are intentionally separate. A 60 Hz renderer can repeatedly display a 30 fps camera, but the camera texture should only be uploaded when the video decoder produces a new frame.
app.withGlobalTauri exposes window.__TAURI__.core.invoke() to vanilla JavaScript.controls and canvas are explicitly named in the capability file.get_webview_window().NotReadableError.2 to limit framebuffer memory and shader cost.