vtome 0.3.5

Show images and video on a specific monitor — or a specific quadrilateral of one — with royalty-free codecs and no FFmpeg. GPU presentation with perspective-correct corner pinning, optional windowing, and a frame pipeline that stays in YUV until the shader.
Documentation
// Draw one frame into an arbitrary convex quadrilateral.
//
// The whole trick is in the fragment stage: instead of moving vertices to the
// quad's corners and hoping the texture coordinates interpolate sensibly, this
// covers the entire target with one triangle and asks, for each pixel, "which
// texel of the picture belongs here?" — by pushing the pixel back through the
// quad's inverse homography.
//
// The divide by w below is the perspective correction. Doing it per pixel is
// what a two-triangle mesh cannot do, and its absence is the crease that shows
// up down the diagonal of every keystoned projection.

struct Uniforms {
    // Rows of the inverse homography: target pixels -> the unit square.
    inverse0: vec4<f32>,
    inverse1: vec4<f32>,
    inverse2: vec4<f32>,
    // Rows of the YUV -> RGB matrix. The w component is the offset, which
    // carries both the limited-range pedestal and chroma's neutral point.
    color0: vec4<f32>,
    color1: vec4<f32>,
    color2: vec4<f32>,
    target_size: vec2<f32>,
    opacity: f32,
    mode: u32,
}

const MODE_RGBA: u32 = 0u;
const MODE_PLANAR: u32 = 1u;
const MODE_BIPLANAR: u32 = 2u;

@group(0) @binding(0) var<uniform> settings: Uniforms;
@group(0) @binding(1) var plane0: texture_2d<f32>;
@group(0) @binding(2) var plane1: texture_2d<f32>;
@group(0) @binding(3) var plane2: texture_2d<f32>;
@group(0) @binding(4) var texture_sampler: sampler;

// One oversized triangle rather than two triangles making a quad: fewer
// vertices, no shared edge, and no vertex buffer to bind.
@vertex
fn vertex(@builtin(vertex_index) index: u32) -> @builtin(position) vec4<f32> {
    let x = f32(i32(index) / 2) * 4.0 - 1.0;
    let y = f32(i32(index) & 1) * 4.0 - 1.0;

    return vec4<f32>(x, y, 0.0, 1.0);
}

@fragment
fn fragment(@builtin(position) position: vec4<f32>) -> @location(0) vec4<f32> {
    // The pixel's centre, in the same coordinates the quad was given in.
    let pixel = vec3<f32>(position.x, position.y, 1.0);

    let mapped = vec3<f32>(
        dot(settings.inverse0.xyz, pixel),
        dot(settings.inverse1.xyz, pixel),
        dot(settings.inverse2.xyz, pixel),
    );

    // Behind the horizon of the projective map: there is no texel there.
    let visible_w = step(0.00001, mapped.z);
    let coordinates = mapped.xy / max(mapped.z, 0.00001);

    // Outside the picture is transparent rather than clamped, so a quad smaller
    // than the target leaves the rest of the surface alone. Computed as a
    // factor rather than a `discard` so that sampling stays in uniform control
    // flow — a discarded fragment must not change what its neighbours may do.
    let inside = step(0.0, coordinates.x)
        * step(coordinates.x, 1.0)
        * step(0.0, coordinates.y)
        * step(coordinates.y, 1.0)
        * visible_w;

    var color = vec3<f32>(0.0);
    var alpha = 1.0;

    if settings.mode == MODE_RGBA {
        let texel = textureSample(plane0, texture_sampler, coordinates);
        color = texel.rgb;
        alpha = texel.a;
    } else {
        var yuv: vec3<f32>;

        if settings.mode == MODE_PLANAR {
            yuv = vec3<f32>(
                textureSample(plane0, texture_sampler, coordinates).r,
                textureSample(plane1, texture_sampler, coordinates).r,
                textureSample(plane2, texture_sampler, coordinates).r,
            );
        } else {
            // NV12 and friends: chroma is interleaved in one two-channel plane.
            let chroma = textureSample(plane1, texture_sampler, coordinates).rg;
            yuv = vec3<f32>(
                textureSample(plane0, texture_sampler, coordinates).r,
                chroma.r,
                chroma.g,
            );
        }

        let sample = vec4<f32>(yuv, 1.0);
        color = vec3<f32>(
            dot(settings.color0, sample),
            dot(settings.color1, sample),
            dot(settings.color2, sample),
        );
    }

    return vec4<f32>(clamp(color, vec3<f32>(0.0), vec3<f32>(1.0)), alpha * settings.opacity * inside);
}