chroma_scope_wgpu is a decoded-frame video-scope library implemented with Rust, wgpu, and WebAssembly. It renders waveform, RGB/YRGB parade, vectorscope, CIE 1931 xy, and histogram views while keeping accepted scale, graticule, reference-line, resize, and snapshot behavior.
The library boundary starts after decoding: the host supplies decoded RGBA/BGRA or NV12 planes; Rust validates the frame, owns analysis and GPU resources, and renders the requested scopes. File decoding, WebRTC transport, and application layout remain host responsibilities.
License: this repository uses the PolyForm Noncommercial License 1.0.0. Commercial use is not permitted. Because this restriction is not OSI-compatible, the project is accurately described as source-available, not OSI open source.
The browser package has two independent entry points:
| Entry | Purpose | Owns |
|---|---|---|
chroma-scope-wgpu-web |
UI-free renderer | Decoded-frame validation, persistent WASM input memory, Rust/wgpu rendering, static geometry and optional label canvases |
chroma-scope-wgpu-web/workbench |
Optional official UI | English/Chinese module panels, automatic 4K/mobile density, analysis-rate controls, responsive layout, settings popovers, reset actions and reference-line gestures |
The workbench uses the public core adapter. It does not contain a second analysis or renderer implementation. The runnable examples are:
/canvas-only.html— application-owned canvases with no module-window UI./— the official responsive workbench and local video test harness.
- Rust 1.96.1 or newer
wasm32-unknown-unknowntargetwasm-pack0.15 or newer- Node.js 24 and npm 11, or compatible releases
- A browser and GPU that support WebGPU
Install the Rust target once:
rustup target add wasm32-unknown-unknownBrowser WebGPU is exposed only in a secure context. Loopback addresses such as http://127.0.0.1 and http://localhost receive a development exception, but an ordinary LAN URL such as http://192.168.x.x does not.
For phones, tablets, development boards, and other LAN clients, serve the page over HTTPS. The certificate must contain the host name or IP address in its Subject Alternative Name and must be trusted by the client. A public deployment should use a normal trusted certificate. This library intentionally has no WebGL or CPU-rendering fallback; when navigator.gpu or a suitable adapter is unavailable, initialization fails explicitly.
Build the Rust crate and WASM package directly:
cargo build --release
wasm-pack build --target web --release --out-dir web/pkgBuild the release WASM package, install locked web dependencies, type-check, and create web/dist/:
./scripts/build-web.shGenerated artifacts:
web/pkg/— browser-targeted WASM bindingsweb/dist/— deployable Vite outputtarget/— native and Rust build output
All three are generated locally and excluded from version control.
Loopback HTTP is sufficient for development on the same computer:
npm --prefix web run devOpen http://127.0.0.1:5173/.
For HTTPS and LAN access, provide your own certificate and private key:
VITE_HTTPS_CERT=/absolute/path/dev.crt \
VITE_HTTPS_KEY=/absolute/path/dev.key \
npm --prefix web run dev:https -- --port 5174Open https://<server-host>:5174/. Certificate files belong outside the repository; web/.certs/ and the local override config are ignored.
Supply one application-owned canvas per enabled scope. The core caches static graticules and redraws them only when size or display options invalidate the cache.
import { createWgpuScopeRenderer } from "chroma-scope-wgpu-web";
import type {
AnalysisConfig,
ScopeName,
ScopeRenderTarget,
VideoScopeFrame
} from "chroma-scope-wgpu-web";
const targetScopes: ScopeName[] = ["waveform", "parade", "vectorscope", "cie", "histogram"];
const targets = Object.fromEntries(targetScopes.map((scope) => {
const canvas = document.querySelector<HTMLCanvasElement>(`canvas[data-scope="${scope}"]`)!;
return [scope, {
canvas,
width: canvas.clientWidth,
height: canvas.clientHeight,
devicePixelRatio: window.devicePixelRatio
}];
})) as Record<ScopeName, ScopeRenderTarget>;
const renderer = await createWgpuScopeRenderer({ targets });
const frame: VideoScopeFrame = {
width: 1920,
height: 1080,
timestampUs: 0,
format: "nv12",
range: "limited",
color: { primaries: "rec709", transfer: "bt709", matrix: "bt709" },
planes: [
{ data: lumaBytes, strideBytes: 1920 },
{ data: interleavedChromaBytes, strideBytes: 1920 }
]
};
const analysis: AnalysisConfig = {
scopes: ["waveform", "parade", "vectorscope"],
signalDensityWidth: frame.width,
signalDensityHeight: 192,
vectorscopeDensityWidth: 768,
vectorscopeDensityHeight: 768,
cieDensityWidth: 512,
cieDensityHeight: 512,
histogramBins: 256,
waveformChannels: ["y"],
paradeChannels: ["y", "r", "g", "b"]
};
await renderer.submitFrame(frame, analysis, "half");Display and lifecycle operations are independent from frame submission:
renderer.updateDisplayOptions({
waveform: { scaleStyle: "10bit", signalRange: "data", mode: "rgb" },
vectorscope: { targetMode: "75+100", zoom2x: false }
});
renderer.resize("waveform", 960, 540, window.devicePixelRatio);
const snapshot = await renderer.requestSnapshot();
const statistics = renderer.stats();
await renderer.waitForIdle();
renderer.dispose();submitFrame() accepts decoded rgba8, bgra8, and nv12 frames in the realtime WebGPU path. Unsupported formats and invalid strides, offsets, or metadata fail explicitly. See docs/API.md for the complete frame, analysis, display, timing, and error contracts.
The typed adapter manages a persistent three-slot WASM input ring. Schedulers that integrate directly with the generated binding may reserve and fill a slot themselves:
import init, { WasmRenderer } from "./web/pkg/chroma_scope_wgpu.js";
await init();
const wasm = await WasmRenderer.create(dynamicCanvases, staticCanvases);
wasm.configure(config);
wasm.reserveInput(0, lumaBytes.length + chromaBytes.length);
const input = wasm.inputView(0); // reacquire after every reserveInput call
input.set(lumaBytes, 0);
input.set(chromaBytes, lumaBytes.length);
wasm.submitNv12(
input.subarray(0, lumaBytes.length),
input.subarray(lumaBytes.length),
1920, 1080, 1920, 1920, 0, 0, 0,
"half", "limited", "bt709"
);Do not retain inputView() across a later reserveInput() that may grow WASM memory. Keep no more than three submitted leases in flight and call waitForIdle() before destroying externally owned storage.
.
├── src/ Rust analysis, geometry, GPU resources and WASM binding
│ ├── geometry/ Static scope geometry and label layouts
│ └── gpu/ wgpu pipelines, resources, submissions and timing
├── shaders/ WGSL compute, render, line and composite shaders
├── web/
│ ├── src/ Typed core adapter, optional workbench and examples
│ └── tests/ Browser-adapter unit tests
├── tests/ Rust contracts, fixtures, golden data and browser tests
├── scripts/ Build, verification and optional A/B tooling
├── reports/ Machine-readable accepted verification evidence
└── docs/ API and verification protocols
Source code lives in src/, shaders/, and web/src/. Generated output, certificates, Playwright artifacts, and the local CodeGraph database are ignored. .codegraph/.gitignore is retained so contributors can create a local index without committing machine-specific data.
Run all self-contained checks available from a clean clone:
./scripts/verify-core.shThis runs Rust formatting, Clippy, native tests, WASM target checks, the release web build, TypeScript checks, unit tests, browser contracts, and script tests.
The historical TypeScript/WebGPU implementation and large video/NV12 fixtures are intentionally not duplicated in this repository. To reproduce cross-backend visual, decoded-input, performance, and stability gates, point the harness at a reference checkout:
export CHROMA_SCOPE_REFERENCE_ROOT=/absolute/path/to/chroma_scope
./scripts/verify.shRK3588 A/B verification additionally requires ADB_SERIAL, BOARD_HOST, VITE_HTTPS_CERT, and VITE_HTTPS_KEY. Exact protocols and the accepted reports are documented in docs/verification.md.
Licensed under the PolyForm Noncommercial License 1.0.0. You may use, study, modify, and distribute the software only for permitted noncommercial purposes under those terms. Contact the licensor separately for commercial licensing.