Text + MXL logo pattern

This commit is contained in:
Dmitry Sergeev
2026-09-13 16:33:50 +03:00
parent 94ff41a124
commit 97dec92b8b
15 changed files with 999 additions and 23 deletions
+101
View File
@@ -0,0 +1,101 @@
// DVD-style bouncing logo (image texture, e.g. assets/mxl.png) on studio black,
// Rec.709, 10-bit Y'CbCr, packed as v210 (4:2:2).
// One work-item per 16-byte block = 6 pixels (6 Y + 3 Cb + 3 Cr).
// Stateless bounce: logo position is a pure function of the frame tick
// (two triangle waves), so no velocity/position state is carried between frames.
//
// Sub-pixel motion: the logo plane is bilinearly resampled at the fractional
// position, so edges glide continuously instead of stepping whole pixels.
// The blend is built from mix() chains, which reproduce texel values exactly
// in solid regions (mix of equal values is exact in f32).
//
// Logo plane (binding 2, read-only), one u32 per logo pixel:
// logo[0] = logo_w | logo_h << 16
// logo[1 + dy*logo_w + dx] = opaque<<30 | Cr<<20 | Cb<<10 | Y
struct Params {
width: u32,
height: u32,
frame: u32, // animation tick (0,1,2,... per generated frame), NOT the raw grain index
_pad0: u32,
};
@group(0) @binding(0) var<storage, read_write> out: array<u32>;
@group(0) @binding(1) var<uniform> params: Params;
@group(0) @binding(2) var<storage, read> logo: array<u32>;
// 0 -> 1 -> 0 triangle wave; input is elapsed bounce cycles.
fn tri01(x: f32) -> f32 {
let f = fract(x);
return select(2.0 * f, 2.0 - 2.0 * f, f > 0.5);
}
// Deviation of a logo texel from the background (64/512/512); zero when the
// texel is transparent or out of the plane. u32 wraparound in the callers
// makes every lower/upper bound case fail the range test, so out-of-bounds
// needs no special handling.
fn tap(lw: u32, lh: u32, dx: u32, dy: u32) -> vec3<f32> {
if (dx < lw && dy < lh) {
let s = logo[1u + dy * lw + dx];
if ((s >> 30u) & 1u == 1u) {
return vec3<f32>(f32(s & 0x3FFu) - 64.0,
f32((s >> 10u) & 0x3FFu) - 512.0,
f32((s >> 20u) & 0x3FFu) - 512.0);
}
}
return vec3<f32>(0.0, 0.0, 0.0);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let block = gid.x;
let total = (params.width * params.height) / 6u;
if (block >= total) {
return;
}
let x = (block * 6u) % params.width;
let py = (block * 6u) / params.width;
// Logo geometry comes from the plane header.
let hdr = logo[0];
let lw = hdr & 0xFFFFu;
let lh = hdr >> 16u;
// Slow glide: ~8.6 px/frame horizontal, ~6.5 px/frame vertical at 25 fps.
// Near-irrational speed ratio (~sqrt(2)) covers the whole frame and
// rarely repeats the path, like the DVD logo.
let t = f32(params.frame) / 25.0;
let lx = tri01(t * 0.061) * max(f32(params.width) - f32(lw), 0.0);
let ly = tri01(t * 0.088) * max(f32(params.height) - f32(lh), 0.0);
let bx = u32(lx); // integer part of the position
let by = u32(ly);
let fx = lx - f32(bx); // fractional part, [0,1)
let fy = ly - f32(by);
var y: array<u32, 6>;
var cb: array<u32, 6>;
var cr: array<u32, 6>;
for (var i = 0u; i < 6u; i++) {
// Screen pixel (x+i, py) covers logo-plane footprint [m-f, m+1-f) x
// [n-f, n+1-f): texel m-1 gets weight fx, texel m gets 1-fx (same
// vertically). Chroma is blended the same way and packed at even
// columns, matching v210 4:2:2 co-siting.
let m = (x + i) - bx;
let n = py - by;
let top = mix(tap(lw, lh, m, n - 1u), tap(lw, lh, m - 1u, n - 1u), fx);
let bot = mix(tap(lw, lh, m, n), tap(lw, lh, m - 1u, n), fx);
let d = mix(bot, top, fy);
y[i] = u32(64.0 + d.x + 0.5);
cb[i] = u32(512.0 + d.y + 0.5);
cr[i] = u32(512.0 + d.z + 0.5);
}
// v210 word layout, chroma co-sited with luma samples 0/2/4:
// w0 = Cb0|Y0<<10|Cr0<<20; w1 = Y1|Cb2<<10|Y2<<20;
// w2 = Cr2|Y3<<10|Cb4<<20; w3 = Y4|Cr4<<10|Y5<<20
out[block * 4u + 0u] = (cb[0] & 0x3FFu) | ((y[0] & 0x3FFu) << 10u) | ((cr[0] & 0x3FFu) << 20u);
out[block * 4u + 1u] = (y[1] & 0x3FFu) | ((cb[2] & 0x3FFu) << 10u) | ((y[2] & 0x3FFu) << 20u);
out[block * 4u + 2u] = (cr[2] & 0x3FFu) | ((y[3] & 0x3FFu) << 10u) | ((cb[4] & 0x3FFu) << 20u);
out[block * 4u + 3u] = (y[4] & 0x3FFu) | ((cr[4] & 0x3FFu) << 10u) | ((y[5] & 0x3FFu) << 20u);
}
+75
View File
@@ -0,0 +1,75 @@
// 75% SMPTE color bars + moving square, Rec.709, 10-bit Y'CbCr, packed as v210 (4:2:2).
// One work-item per 16-byte block = 6 pixels (6 Y + 3 Cb + 3 Cr).
struct Params {
width: u32,
height: u32,
frame: u32, // animation tick (0,1,2,... per generated frame), NOT the raw grain index
_pad0: u32,
};
@group(0) @binding(0) var<storage, read_write> out: array<u32>;
@group(0) @binding(1) var<uniform> params: Params;
fn bar_index(px: u32) -> u32 {
return min((px * 7u) / params.width, 6u);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let block = gid.x;
let total = (params.width * params.height) / 6u;
if (block >= total) {
return;
}
let x = (block * 6u) % params.width;
let py = (block * 6u) / params.width;
// Bar order: 75% white, yellow, cyan, green, magenta, red, blue
let y_tab = array<u32, 7>(721u, 674u, 581u, 534u, 251u, 204u, 111u);
let cb_tab = array<u32, 7>(512u, 176u, 589u, 253u, 771u, 435u, 848u);
let cr_tab = array<u32, 7>(512u, 543u, 176u, 207u, 817u, 848u, 481u);
// Moving square: horizontal oscillation around screen center.
// frame is a small tick; converting the huge raw grain index here
// would destroy f32 precision and freeze the motion.
let centerX = f32(params.width) / 2.0;
let centerY = f32(params.height) / 2.0;
let fps = 25.0;
let t = f32(params.frame) / fps;
const squareSize = 150u;
let half = f32(squareSize) / 2.0;
let offsetPixels = sin(t * 0.5) * (centerX - half);
let sq_x_min = centerX - half + offsetPixels;
let sq_x_max = centerX + half + offsetPixels;
let sq_y_min = centerY - half;
let sq_y_max = centerY + half;
let py_f = f32(py);
var y: array<u32, 6>;
var cb: array<u32, 6>;
var cr: array<u32, 6>;
for (var i = 0u; i < 6u; i++) {
let px = f32(x + i);
y[i] = 64u + (u32(px) * 876u) / params.width;
cb[i] = 512u;
cr[i] = 512u;
if (px >= sq_x_min && px < sq_x_max && py_f >= sq_y_min && py_f < sq_y_max) {
// inverted bar: luma mirrored across studio range (64..940),
// chroma mirrored across neutral 512
y[i] = 1004u - y[i];
cb[i] = 1024u - cb[i];
cr[i] = 1024u - cr[i];
}
}
// v210 word layout, chroma co-sited with luma samples 0/2/4:
// w0 = Cb0|Y0<<10|Cr0<<20; w1 = Y1|Cb2<<10|Y2<<20;
// w2 = Cr2|Y3<<10|Cb4<<20; w3 = Y4|Cr4<<10|Y5<<20
out[block * 4u + 0u] = (cb[0] & 0x3FFu) | ((y[0] & 0x3FFu) << 10u) | ((cr[0] & 0x3FFu) << 20u);
out[block * 4u + 1u] = (y[1] & 0x3FFu) | ((cb[2] & 0x3FFu) << 10u) | ((y[2] & 0x3FFu) << 20u);
out[block * 4u + 2u] = (cr[2] & 0x3FFu) | ((y[3] & 0x3FFu) << 10u) | ((cb[4] & 0x3FFu) << 20u);
out[block * 4u + 3u] = (y[4] & 0x3FFu) | ((cr[4] & 0x3FFu) << 10u) | ((y[5] & 0x3FFu) << 20u);
}