From c3bcee6223467c459856956aa721eff53728922b Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Wed, 16 Sep 2026 00:09:16 +0300 Subject: [PATCH] SMPTE RP219-1 pattern signal --- cmd/mxl-pattern/main.go | 14 ++- internal/generator/text.go | 4 +- .../{smpteBars.wgsl => colorBars.wgsl} | 0 kernels/static/colorBars.wgsl | 50 ++++++++ kernels/static/smpteBars.wgsl | 108 ++++++++++++++++-- 5 files changed, 158 insertions(+), 18 deletions(-) rename kernels/dynamic/{smpteBars.wgsl => colorBars.wgsl} (100%) create mode 100644 kernels/static/colorBars.wgsl diff --git a/cmd/mxl-pattern/main.go b/cmd/mxl-pattern/main.go index de6a2b6..08c22be 100644 --- a/cmd/mxl-pattern/main.go +++ b/cmd/mxl-pattern/main.go @@ -192,16 +192,22 @@ type pattern struct { var patterns = map[string]pattern{ "bars": { name: "bars", - description: "SMPTE 75% color bars", - kernelPath: "kernels/static/smpteBars.wgsl", + description: "75% color bars", + kernelPath: "kernels/static/colorBars.wgsl", motion: false, }, "bars-move": { name: "bars-move", - description: "SMPTE 75% color bars with moving square", - kernelPath: "kernels/dynamic/smpteBars.wgsl", + description: "75% color bars with moving square", + kernelPath: "kernels/dynamic/colorBars.wgsl", motion: true, }, + "smpte": { + name: "smpte", + description: "SMPTE RP-219 Color Bar Signal", + kernelPath: "kernels/static/smpteBars.wgsl", + motion: false, + }, "gray-bars": { name: "gray-bars", description: "13-step grayscale bars (Y 64..940)", diff --git a/internal/generator/text.go b/internal/generator/text.go index ae8ebd7..015ae34 100644 --- a/internal/generator/text.go +++ b/internal/generator/text.go @@ -45,8 +45,8 @@ func LoadFace(path string, size float64) (font.Face, error) { return face, nil } -// NewTextOverlay rasterizes text (white, anti-aliased) onto a black backing -// box, centered horizontally. Box width and x are multiples of 6 so the box +// NewTextOverlay rasterizes text (white, anti-aliased) onto a black backing box. +// Box width and x are multiples of 6 so the box // covers whole v210 blocks and the per-frame copy is word-aligned. func NewTextOverlay(text string, frameW, frameH, posX, posY int, face font.Face) (*TextOverlay, error) { if text == "" { diff --git a/kernels/dynamic/smpteBars.wgsl b/kernels/dynamic/colorBars.wgsl similarity index 100% rename from kernels/dynamic/smpteBars.wgsl rename to kernels/dynamic/colorBars.wgsl diff --git a/kernels/static/colorBars.wgsl b/kernels/static/colorBars.wgsl new file mode 100644 index 0000000..08cdfac --- /dev/null +++ b/kernels/static/colorBars.wgsl @@ -0,0 +1,50 @@ +// 75% SMPTE color bars, 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, + _pad0: u32, +}; + +@group(0) @binding(0) var out: array; +@group(0) @binding(1) var 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) { + let block = gid.x; + let total = (params.width * params.height) / 6u; + if (block >= total) { + return; + } + + let x = (block * 6u) % params.width; + + // Bar order: 75% white, yellow, cyan, green, magenta, red, blue + let y_tab = array(721u, 674u, 581u, 534u, 251u, 204u, 111u); + let cb_tab = array(512u, 176u, 589u, 253u, 771u, 435u, 848u); + let cr_tab = array(512u, 543u, 176u, 207u, 817u, 848u, 481u); + + var y: array; + var cb: array; + var cr: array; + for (var i = 0u; i < 6u; i++) { + let b = bar_index(x + i); + y[i] = y_tab[b]; + cb[i] = cb_tab[b]; + cr[i] = cr_tab[b]; + } + + // 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); +} diff --git a/kernels/static/smpteBars.wgsl b/kernels/static/smpteBars.wgsl index 08cdfac..d9742c1 100644 --- a/kernels/static/smpteBars.wgsl +++ b/kernels/static/smpteBars.wgsl @@ -1,5 +1,5 @@ -// 75% SMPTE color bars, 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). +// SMPTE RP 219-1 Color Bar Pattern Generator +// 10-bit Y'CbCr packed as v210 (4:2:2). Compliant with SMPTE RP 219-1:2014 struct Params { width: u32, @@ -11,8 +11,8 @@ struct Params { @group(0) @binding(0) var out: array; @group(0) @binding(1) var params: Params; -fn bar_index(px: u32) -> u32 { - return min((px * 7u) / params.width, 6u); +fn bar_index(px: u32, bars_75_width: u32) -> u32 { + return min((px * 7u) / bars_75_width, 6u); } @compute @workgroup_size(64) @@ -24,25 +24,109 @@ fn main(@builtin(global_invocation_id) gid: vec3) { } let x = (block * 6u) % params.width; + let y_px = (block * 6u) / params.width; - // Bar order: 75% white, yellow, cyan, green, magenta, red, blue + // 75% Color Table: white, yellow, cyan, green, magenta, red, blue let y_tab = array(721u, 674u, 581u, 534u, 251u, 204u, 111u); let cb_tab = array(512u, 176u, 589u, 253u, 771u, 435u, 848u); let cr_tab = array(512u, 543u, 176u, 207u, 817u, 848u, 481u); + // Layout calculations mapping the 4:3 area centered in a 16:9 canvas + let bars_width: u32 = (params.height / 3u) * 4u; + let bars_start: u32 = (params.width - bars_width) / 2u; + let bars_end: u32 = bars_width + bars_start; + let one_bar_width: u32 = bars_width / 7u; + + // Strict vertical boundary calculations based on 12th parts + let unit_h: u32 = params.height / 12u; + let bound_s1: u32 = unit_h * 7u; // Row 630 on 1080p + let bound_s2: u32 = bound_s1 + unit_h; // Row 720 on 1080p + let bound_s3: u32 = bound_s2 + unit_h; // Row 810 on 1080p + + // Section 3 Linear Y-Ramp boundaries + let ramp_start = bars_start + one_bar_width; + let ramp_end = bars_end; + let ramp_width = ramp_end - ramp_start; + var y: array; var cb: array; var cr: array; + for (var i = 0u; i < 6u; i++) { - let b = bar_index(x + i); - y[i] = y_tab[b]; - cb[i] = cb_tab[b]; - cr[i] = cr_tab[b]; + let px_x = x + i; + + // SECTION 1 (Top 7/12): Main 75% Color Bars + if (y_px < bound_s1) { + if (px_x >= bars_start && px_x < bars_end) { + let b = bar_index(px_x - bars_start, bars_width); + y[i] = y_tab[b]; + cb[i] = cb_tab[b]; + cr[i] = cr_tab[b]; + } else { + // 40% Gray Flanks + y[i] = 414u; cb[i] = 512u; cr[i] = 512u; + } + } + // SECTION 2 (Next 1/12): 100% Cyan, -I Signal, 75% White, 100% Blue + else if (y_px >= bound_s1 && y_px < bound_s2) { + if (px_x < bars_start) { + y[i] = 754u; cb[i] = 615u; cr[i] = 64u; // 100% Cyan + } else if (px_x >= bars_start && px_x < ramp_start) { + y[i] = 244u; cb[i] = 612u; cr[i] = 395u; // -I Signal + } else if (px_x >= bars_end) { + y[i] = 127u; cb[i] = 960u; cr[i] = 471u; // 100% Blue + } else { + y[i] = 721u; cb[i] = 512u; cr[i] = 512u; // 75% White + } + } + // SECTION 3 (Next 1/12): 100% Yellow, +Q Signal, Linear Y-Ramp, 75% Red + else if (y_px >= bound_s2 && y_px < bound_s3) { + if (px_x < bars_start) { + y[i] = 877u; cb[i] = 64u; cr[i] = 553u; // 100% Yellow + } else if (px_x >= bars_start && px_x < ramp_start) { + y[i] = 141u; cb[i] = 697u; cr[i] = 606u; // +Q Signal + } else if (px_x >= bars_end) { + y[i] = 250u; cb[i] = 409u; cr[i] = 960u; // 75% Red + } else { + // Perfect linear scaling from Digital Black (64u) to Digital White (940u) + let clamped_x = clamp(px_x, ramp_start, ramp_end); + y[i] = 64u + ((clamped_x - ramp_start) * (940u - 64u)) / ramp_width; + cb[i] = 512u; + cr[i] = 512u; + } + } + // SECTION 4 (Bottom 3/12): Pluge Panel Calibration Blocks + else { + if (px_x >= bars_start && px_x < bars_end) { + let b = (px_x - bars_start) / one_bar_width; + + if (b == 0u || b == 2u || b >= 4u) { + y[i] = 64u; cb[i] = 512u; cr[i] = 512u; // 0% Black + } else if (b == 1u) { + y[i] = 940u; cb[i] = 512u; cr[i] = 512u; // 100% White Calibration + } else if (b == 3u) { + // PLUGE Sub-bar generation (Split into 3 absolute equal fragments) + let sub_bar_offset = (px_x - bars_start) % one_bar_width; + let sub_b = (sub_bar_offset * 3u) / one_bar_width; + + cb[i] = 512u; + cr[i] = 512u; + if (sub_b == 0u) { + y[i] = 46u; // -2% Sub-Black + } else if (sub_b == 1u) { + y[i] = 64u; // 0% Black + } else { + y[i] = 82u; // +2% Above-Black + } + } + } else { + // 15% Gray Flanks + y[i] = 195u; cb[i] = 512u; cr[i] = 512u; + } + } } - // 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 + // Packing into standard v210 10-bit macroblock layout words 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);