Documentation/Source example
Example source
samples/vis/Shader - Vectorscope.js
Referenced by:vis.CreateAsync
include('docs/Flags.js');
window.DrawMode = 1;
// JSplitter vis sample: Vectorscope | HLSL shader pipeline.
// An open, pixel-shader approximation of the built-in phosphor vectorscope.
// No native compute shader or private GPU buffers are used.
//
// Pipeline:
// shader - stereo PCM -> round pixel hits + radial glow, accumulated
// with the built-in CPU formulas within the current PCM frame.
// feedback - previous image -> time-based persistence (300 ms).
// post - saved intensity -> DUI/CUI text color for display.
//
// The public shader interface exposes ps_4_0 image passes, not D3D11
// compute/structured buffers. Every source-shader pixel must search PCM
// samples, so this example bounds the search to 256 distributed stereo pairs
// from the native source's default 1024. At larger panel sizes this can be
// significantly more expensive than the built-in CPU or compute renderer.
// The feedback history is one 8-bit premultiplied image; unlike the native
// compute implementation it cannot separately retain full-precision core
// and glow intensities. Pause/new-source-revision behavior can also differ.
vis.UpdateIntervalMs = 16; // Default: 16 ms; allowed: unsigned integer 1..4294967295 ms.
// Built-in defaults for the visible shape: pointSize=1, scale=1,
// aspectScale=true, glowStrength=0.4, glowRadius=4, persistenceMs=300.
// These are constants inside the HLSL demonstration, not new API options.
const textColor = window.InstanceType === 1
? window.GetColourDUI(0) : window.GetColourCUI(0);
const backgroundColor = window.InstanceType === 1
? window.GetColourDUI(1) : window.GetColourCUI(1);
// Convert the theme's ARGB value into compile-time HLSL literals.
const colorComponent = shift => (((textColor >>> shift) & 255) / 255).toFixed(6);
const tint = `float3(${colorComponent(16)}, ${colorComponent(8)}, ${colorComponent(0)})`;
const themeAlpha = colorComponent(24);
// Source pass: texture t0 = left PCM, t1 = right PCM (paired R32F rows).
// Constants b0 = Draw width, height, effect time, available stereo pairs.
// CPU-style core and radial halo are combined into premultiplied coverage
// for a single PCM window; source-over blending accumulates the windows.
const sourceShader = `
Texture2D<float> LeftPCM : register(t0);
Texture2D<float> RightPCM : register(t1);
cbuffer VisConstants : register(b0) {
float2 viewportSize;
float time;
float pointCount;
};
struct PSInput {
float4 clipSpacePos : SV_POSITION;
float4 sceneSpacePos : SCENE_POS;
};
float4 main(PSInput input) : SV_Target {
float2 size = max(viewportSize, float2(1.0, 1.0));
float2 pixel = floor(input.sceneSpacePos.xy);
float2 center = size * 0.5;
// Corresponds to the built-in Vectorscope's default render settings.
const float pointSize = 1.0;
const float glowRadius = 4.0;
const float glowStrength = 0.4;
const float scale = 1.0;
float margin = max(1.0, pointSize * 0.5 + glowRadius + 1.0);
float2 radius = max(float2(0.0, 0.0), center - margin);
int splat = max(0, (int)ceil(pointSize * 0.5) - 1);
int count = max(0, (int)pointCount);
int plotted = min(count, 256); // Bounded per-pixel cost, not source.points.
float core = 0.0;
float glow = 0.0;
[loop]
for (int i = 0; i < plotted; ++i) {
int index = min(count - 1, (int)floor(
((float)i + 0.5) * (float)count / (float)plotted));
float l = LeftPCM.Load(int3(index, 0, 0)).r;
float r = RightPCM.Load(int3(index, 0, 0)).r;
float2 spot = center + 0.5 * float2(l - r, -(l + r)) * radius * scale;
float2 delta = pixel - floor(spot + 0.5);
if (abs(delta.x) <= splat && abs(delta.y) <= splat)
core += 0.55 * (1.0 - core);
float d2 = dot(delta, delta) / (glowRadius * glowRadius);
if (d2 < 1.0) {
float weight = 1.0 - d2;
float contribution = 0.22 * glowStrength * weight * weight;
glow += contribution * (1.0 - glow);
}
}
float coverage = saturate(core + glow * (1.0 - core));
return float4(coverage, coverage, coverage, coverage);
}`;
// Feedback pass: previous image (t0/s0), one step per Draw().
// frameStep is measured in 60-Hz frames; exp(-3*dt/300ms) matches the
// built-in's persistence equation. The tiny subtraction prevents 8-bit
// history from retaining nearly invisible pixels forever after quantization.
const feedbackShader = `
Texture2D Previous : register(t0);
SamplerState PreviousSampler : register(s0);
cbuffer ImageConstants : register(b0) {
float2 viewportSize;
float time;
float frameStep;
};
struct PSInput {
float4 clipSpacePos : SV_POSITION;
float4 sceneSpacePos : SCENE_POS;
float4 texelSpacePos : TEXEL_POS;
};
float4 main(PSInput input) : SV_Target {
float4 previous = Previous.SampleLevel(PreviousSampler,
input.texelSpacePos.xy, 0.0);
float fade = exp(-3.0 * (frameStep * (1000.0 / 60.0)) / 300.0);
// The native history bitmap is cleared to opaque black: alpha is always 1.
// Luminance (RGB), not alpha, carries the accumulated signal.
float coverage = max(0.0, previous.r * fade -
(0.5 / 255.0) * saturate(frameStep));
return float4(coverage, coverage, coverage, coverage);
}`;
// Post pass: tint the saved grayscale intensity without changing history.
// RGB is premultiplied by alpha, as required by the shader image pipeline.
const postShader = `
Texture2D History : register(t0);
SamplerState HistorySampler : register(s0);
cbuffer ImageConstants : register(b0) {
float2 viewportSize;
float time;
float frameStep;
};
struct PSInput {
float4 clipSpacePos : SV_POSITION;
float4 sceneSpacePos : SCENE_POS;
float4 texelSpacePos : TEXEL_POS;
};
float4 main(PSInput input) : SV_Target {
float4 previous = History.SampleLevel(HistorySampler,
input.texelSpacePos.xy, 0.0);
// Read history luminance, since the history bitmap is opaque.
float opacity = saturate(previous.r * ${themeAlpha});
float3 color = ${tint};
return float4(color * opacity, opacity);
}`;
// Native vectorscope source settings are all at their API defaults.
const source = {
leftChannel: 0, // Default: 0; also accepts a vis.AudioFormat channel name; "mix" is invalid.
rightChannel: 1, // Default: 1; also accepts a vis.AudioFormat channel name; "mix" is invalid.
durationMs: 40, // Default: 40; allowed: integer 10..500 ms.
points: 1024 // Default: 1024; allowed: integer 32..8192.
};
let vectorscope = null;
vis.CreateAsync(VisSourceType.Vectorscope, source, {
shader: sourceShader,
feedback: feedbackShader,
post: postShader,
stopOnPause: true // Default: false; allowed: true or false. Freeze the accumulated vectorscope image on pause.
})
.then(value => {
vectorscope = value;
window.Repaint();
})
.catch(error => console.log("Vectorscope shader pipeline failed: " + error));
function on_paint(gr) {
const w = window.Width, h = window.Height;
gr.FillSolidRect(0, 0, w, h, backgroundColor);
if (vectorscope && w > 0 && h > 0)
vectorscope.Draw(gr, 0, 0, w, h);
}
setInterval(() => window.Repaint(), 33);