WGPU is coming

This commit is contained in:
Dmitry Sergeev
2026-09-07 17:18:03 +03:00
commit f48195a8bf
19 changed files with 2230 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
package generator
// 8x16 1-bpp bitmap font baked from DejaVu Sans Mono 13px (public-domain
// style console glyphs). ASCII 32..127, one byte per row, LSB = leftmost pixel.
var fontAtlas8x16 = [96 * 16]byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x24, 0x24, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x48, 0x68, 0x28, 0xFE, 0x24, 0x24, 0x7F, 0x16, 0x12, 0x12, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x3C, 0x16, 0x02, 0x1C, 0x38, 0x40, 0x62, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x0E, 0x09, 0x09, 0x6E, 0x18, 0x76, 0xD0, 0xD0, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x06, 0x04, 0x0C, 0x9A, 0xD3, 0x63, 0x62, 0x5C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x10, 0x18, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x18, 0x10, 0x10, 0x00, 0x00, 0x00,
0x00, 0x0C, 0x08, 0x18, 0x18, 0x10, 0x10, 0x10, 0x10, 0x10, 0x18, 0x08, 0x0C, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x08, 0x4A, 0x1C, 0x1C, 0x4A, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x7F, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x08, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x60, 0x20, 0x30, 0x10, 0x10, 0x08, 0x08, 0x0C, 0x04, 0x06, 0x02, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x26, 0x62, 0x42, 0x5A, 0x42, 0x62, 0x26, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1E, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x7C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x22, 0x60, 0x20, 0x30, 0x18, 0x0C, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x22, 0x60, 0x20, 0x3C, 0x60, 0x60, 0x62, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x30, 0x38, 0x28, 0x24, 0x26, 0x22, 0x7E, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3E, 0x06, 0x06, 0x3E, 0x20, 0x60, 0x60, 0x22, 0x1C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x06, 0x02, 0x3E, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x60, 0x20, 0x30, 0x10, 0x18, 0x18, 0x08, 0x0C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x66, 0x62, 0x26, 0x3C, 0x66, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x26, 0x62, 0x62, 0x66, 0x7C, 0x60, 0x20, 0x1C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x08, 0x08, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x38, 0x06, 0x06, 0x38, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x1C, 0x70, 0x70, 0x1C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x60, 0x60, 0x30, 0x18, 0x08, 0x00, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x46, 0x42, 0xF3, 0xC9, 0xC9, 0xC9, 0xF3, 0x02, 0x06, 0x38, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x18, 0x3C, 0x34, 0x24, 0x66, 0x7E, 0x42, 0xC3, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3E, 0x62, 0x62, 0x62, 0x3E, 0x62, 0x42, 0x62, 0x3E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x38, 0x44, 0x06, 0x02, 0x02, 0x02, 0x06, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x1E, 0x22, 0x62, 0x42, 0x42, 0x42, 0x62, 0x22, 0x1E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x06, 0x06, 0x06, 0x7E, 0x06, 0x06, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x06, 0x06, 0x06, 0x7E, 0x06, 0x06, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x46, 0x02, 0x02, 0x72, 0x42, 0x42, 0x46, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x32, 0x1E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x42, 0x22, 0x12, 0x0E, 0x1E, 0x12, 0x32, 0x62, 0x42, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x67, 0x67, 0x67, 0x5F, 0x5B, 0x5B, 0x43, 0x43, 0x43, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x46, 0x46, 0x4E, 0x4A, 0x5A, 0x52, 0x72, 0x62, 0x62, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x66, 0x62, 0x42, 0x42, 0x42, 0x62, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3E, 0x66, 0x46, 0x46, 0x66, 0x3E, 0x06, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x66, 0x62, 0x42, 0x42, 0x42, 0x62, 0x66, 0x3C, 0x30, 0x20, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3E, 0x62, 0x62, 0x62, 0x3E, 0x32, 0x62, 0x42, 0xC2, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x3C, 0x06, 0x02, 0x06, 0x3C, 0x60, 0x40, 0x62, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x43, 0x42, 0x62, 0x26, 0x24, 0x24, 0x3C, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xC1, 0xC3, 0xC3, 0x5B, 0x5A, 0x5A, 0x76, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x42, 0x66, 0x3C, 0x18, 0x18, 0x3C, 0x24, 0x66, 0x43, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x43, 0x66, 0x24, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x60, 0x20, 0x30, 0x18, 0x08, 0x04, 0x06, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x38, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x38, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x08, 0x10, 0x10, 0x30, 0x20, 0x60, 0x00, 0x00,
0x00, 0x1C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1C, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x3C, 0x26, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00,
0x00, 0x00, 0x0C, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x22, 0x60, 0x7C, 0x62, 0x62, 0x7C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x02, 0x02, 0x02, 0x3E, 0x66, 0x46, 0x42, 0x46, 0x66, 0x3E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x44, 0x06, 0x06, 0x06, 0x44, 0x38, 0x00, 0x00, 0x00, 0x00,
0x00, 0x60, 0x60, 0x60, 0x60, 0x7C, 0x66, 0x62, 0x62, 0x62, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x66, 0x42, 0x7E, 0x02, 0x46, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x70, 0x18, 0x08, 0x08, 0x7E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x66, 0x62, 0x62, 0x62, 0x66, 0x7C, 0x60, 0x20, 0x1C, 0x00,
0x00, 0x02, 0x02, 0x02, 0x02, 0x3E, 0x66, 0x66, 0x62, 0x62, 0x62, 0x62, 0x00, 0x00, 0x00, 0x00,
0x00, 0x18, 0x00, 0x00, 0x00, 0x1C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x1C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x18, 0x0E, 0x00,
0x00, 0x06, 0x06, 0x06, 0x06, 0x66, 0x36, 0x1E, 0x1E, 0x36, 0x66, 0x46, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x70, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x66, 0x66, 0x62, 0x62, 0x62, 0x62, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x66, 0x42, 0x42, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x66, 0x46, 0x42, 0x46, 0x66, 0x3E, 0x02, 0x02, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x66, 0x62, 0x62, 0x62, 0x66, 0x7C, 0x60, 0x60, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x26, 0x06, 0x3C, 0x20, 0x22, 0x3C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x08, 0x08, 0x7E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x78, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x62, 0x62, 0x62, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x62, 0x26, 0x24, 0x34, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xC1, 0xC3, 0x5A, 0x5A, 0x7E, 0x66, 0x26, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x24, 0x18, 0x18, 0x3C, 0x24, 0x42, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x66, 0x24, 0x24, 0x3C, 0x18, 0x18, 0x18, 0x08, 0x06, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x20, 0x10, 0x18, 0x0C, 0x04, 0x7E, 0x00, 0x00, 0x00, 0x00,
0x00, 0x30, 0x18, 0x18, 0x18, 0x08, 0x0E, 0x08, 0x18, 0x18, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00,
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00,
0x00, 0x0E, 0x08, 0x18, 0x18, 0x18, 0x30, 0x18, 0x18, 0x18, 0x08, 0x08, 0x0E, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x7E, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00,
}
+6
View File
@@ -0,0 +1,6 @@
package generator
type FrameGenerator interface {
GenerateFrame(dest []byte, frameIndex int) error
Close() error
}
+302
View File
@@ -0,0 +1,302 @@
// OpenCL pattern gen
package generator
/*
#cgo linux LDFLAGS: -lOpenCL
#cgo windows LDFLAGS: -lOpenCL
#include <CL/cl.h>
#include <stdlib.h>
static cl_program build_cl_program(cl_context ctx, cl_device_id dev, const char* src, cl_int* err) {
cl_program prog = clCreateProgramWithSource(ctx, 1, &src, NULL, err);
if (*err != CL_SUCCESS) return NULL;
*err = clBuildProgram(prog, 1, &dev, NULL, NULL, NULL);
return prog;
}
static const char* get_build_log(cl_program prog, cl_device_id dev) {
static char buf[4096];
size_t n = 0;
if (clGetProgramBuildInfo(prog, dev, CL_PROGRAM_BUILD_LOG, sizeof(buf) - 1, buf, &n) != CL_SUCCESS) return "";
buf[n < sizeof(buf) - 1 ? n : sizeof(buf) - 1] = '\0';
return buf;
}
*/
import "C"
import (
"fmt"
"os"
"unsafe"
)
type OpenCLGenerator struct {
ctx C.cl_context
queue C.cl_command_queue
kernel C.cl_kernel
prog C.cl_program
dev C.cl_device_id
atlasBuf C.cl_mem
textBuf C.cl_mem
width int
height int
}
// TextConfig describes a burn-in text overlay rendered by the kernel
// with the baked 8x16 bitmap font. FG/BG are 10-bit {Y, Cb, Cr}.
type TextConfig struct {
Str string
X, Y int
Scale int
FG [3]uint32
BG [3]uint32
Boxed bool
}
func (g *OpenCLGenerator) setArg(idx int, size C.size_t, p unsafe.Pointer) error {
if err := C.clSetKernelArg(g.kernel, C.cl_uint(idx), size, p); err != C.CL_SUCCESS {
return fmt.Errorf("opencl: set arg %d failed: %d", idx, err)
}
return nil
}
// setTextArgs pushes the overlay arguments (kernel args 5..16) to the kernel.
// buf is the text byte buffer; a placeholder is allowed when len(cfg.Str) == 0,
// because the kernel checks text_len before dereferencing the text pointer.
func (g *OpenCLGenerator) setTextArgs(buf C.cl_mem, cfg TextConfig) error {
textLen := C.int(len(cfg.Str))
tx, ty := C.int(cfg.X), C.int(cfg.Y)
scale := C.int(cfg.Scale)
if scale < 1 {
scale = 1
}
hasBG := C.int(0)
if cfg.Boxed {
hasBG = 1
}
fgY, fgCb, fgCr := C.cl_uint(cfg.FG[0]), C.cl_uint(cfg.FG[1]), C.cl_uint(cfg.FG[2])
bgY, bgCb, bgCr := C.cl_uint(cfg.BG[0]), C.cl_uint(cfg.BG[1]), C.cl_uint(cfg.BG[2])
args := []struct {
idx int
p unsafe.Pointer
sz C.size_t
}{
{5, unsafe.Pointer(&buf), C.sizeof_cl_mem},
{6, unsafe.Pointer(&textLen), C.sizeof_int},
{7, unsafe.Pointer(&tx), C.sizeof_int},
{8, unsafe.Pointer(&ty), C.sizeof_int},
{9, unsafe.Pointer(&scale), C.sizeof_int},
{10, unsafe.Pointer(&hasBG), C.sizeof_int},
{11, unsafe.Pointer(&fgY), C.sizeof_cl_uint},
{12, unsafe.Pointer(&fgCb), C.sizeof_cl_uint},
{13, unsafe.Pointer(&fgCr), C.sizeof_cl_uint},
{14, unsafe.Pointer(&bgY), C.sizeof_cl_uint},
{15, unsafe.Pointer(&bgCb), C.sizeof_cl_uint},
{16, unsafe.Pointer(&bgCr), C.sizeof_cl_uint},
}
for _, a := range args {
if err := g.setArg(a.idx, a.sz, a.p); err != nil {
return err
}
}
return nil
}
// SetText installs (or, with an empty string, removes) the burn-in overlay.
func (g *OpenCLGenerator) SetText(cfg TextConfig) error {
if len(cfg.Str) > 0 && (cfg.Scale < 1 || cfg.Scale > 8) {
return fmt.Errorf("opencl: text scale must be 1..8, got %d", cfg.Scale)
}
if len(cfg.Str) > 1024 {
return fmt.Errorf("opencl: text too long: %d bytes", len(cfg.Str))
}
var err C.cl_int
buf := g.atlasBuf // placeholder: text_len = 0 keeps the kernel off it
if len(cfg.Str) > 0 {
cData := C.CBytes([]byte(cfg.Str))
defer C.free(cData)
buf = C.clCreateBuffer(g.ctx, C.CL_MEM_READ_ONLY|C.CL_MEM_COPY_HOST_PTR,
C.size_t(len(cfg.Str)), cData, &err)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: text buffer upload failed: %d", err)
}
}
if err := g.setTextArgs(buf, cfg); err != nil {
if len(cfg.Str) > 0 {
C.clReleaseMemObject(buf)
}
return err
}
if g.textBuf != nil && g.textBuf != buf {
C.clReleaseMemObject(g.textBuf)
}
if len(cfg.Str) > 0 {
g.textBuf = buf
} else {
g.textBuf = nil
}
return nil
}
func NewOpenCLGenerator(width, height uint, kernelPath string) (*OpenCLGenerator, error) {
var err C.cl_int
var platform C.cl_platform_id
if err = C.clGetPlatformIDs(1, &platform, nil); err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: platform not found: %d", err)
}
g := &OpenCLGenerator{width: int(width), height: int(height)}
if err = C.clGetDeviceIDs(platform, C.CL_DEVICE_TYPE_GPU, 1, &g.dev, nil); err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: no suitable gpu device found")
}
g.ctx = C.clCreateContext(nil, 1, &g.dev, nil, nil, &err)
if err != C.CL_SUCCESS {
return nil, fmt.Errorf("opencl: ctx creation error")
}
g.queue = C.clCreateCommandQueueWithProperties(g.ctx, g.dev, nil, &err)
if err != C.CL_SUCCESS {
g.queue = C.clCreateCommandQueue(g.ctx, g.dev, 0, &err)
}
src, errGo := os.ReadFile(kernelPath)
if errGo != nil {
g.Close()
return nil, fmt.Errorf("opencl: read kernel error: %w", errGo)
}
cSrc := C.CString(string(src))
defer C.free(unsafe.Pointer(cSrc))
g.prog = C.build_cl_program(g.ctx, g.dev, cSrc, &err)
if err != C.CL_SUCCESS {
msg := "clCreateProgramWithSource failed"
if g.prog != nil {
msg = C.GoString(C.get_build_log(g.prog, g.dev))
}
g.Close()
return nil, fmt.Errorf("opencl: compilation failed: %s", msg)
}
cKName := C.CString("generate_v210_pattern")
defer C.free(unsafe.Pointer(cKName))
g.kernel = C.clCreateKernel(g.prog, cKName, &err)
if err != C.CL_SUCCESS {
g.Close()
return nil, fmt.Errorf("opencl: kernel init failed")
}
g.atlasBuf = C.clCreateBuffer(g.ctx, C.CL_MEM_READ_ONLY|C.CL_MEM_COPY_HOST_PTR,
C.size_t(len(fontAtlas8x16)), unsafe.Pointer(&fontAtlas8x16[0]), &err)
if err != C.CL_SUCCESS {
g.Close()
return nil, fmt.Errorf("opencl: font atlas upload failed: %d", err)
}
if err := g.setArg(4, C.sizeof_cl_mem, unsafe.Pointer(&g.atlasBuf)); err != nil {
g.Close()
return nil, err
}
// Overlay defaults: no text, white on black if SetText is ever called.
if err := g.setTextArgs(g.atlasBuf, TextConfig{
Scale: 1,
FG: [3]uint32{940, 512, 512},
BG: [3]uint32{64, 512, 512},
}); err != nil {
g.Close()
return nil, err
}
return g, nil
}
func (g *OpenCLGenerator) GenerateFrame(dest []byte, frameIndex int) error {
var err C.cl_int
byteSize := C.size_t(len(dest))
// ВАЖНО: Мы маппим память MXL SDK ([]byte) напрямую в OpenCL буфер.
// GPU запишет данные прямо в общую память MXL без лишнего копирования (Zero-Copy).
if want := (g.width * g.height / 6) * 16; len(dest) < want {
return fmt.Errorf("opencl: dest too small: %d bytes, need %d", len(dest), want)
}
clMem := C.clCreateBuffer(
g.ctx,
C.CL_MEM_WRITE_ONLY|C.CL_MEM_USE_HOST_PTR,
byteSize,
unsafe.Pointer(&dest[0]),
&err,
)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: mxl host pointer mapping failed: %d", err)
}
defer C.clReleaseMemObject(clMem)
if err := g.setArg(0, C.sizeof_cl_mem, unsafe.Pointer(&clMem)); err != nil {
return err
}
w, h, f := C.int(g.width), C.int(g.height), C.int(frameIndex)
if err := g.setArg(1, C.sizeof_int, unsafe.Pointer(&w)); err != nil {
return err
}
if err := g.setArg(2, C.sizeof_int, unsafe.Pointer(&h)); err != nil {
return err
}
if err := g.setArg(3, C.sizeof_int, unsafe.Pointer(&f)); err != nil {
return err
}
// Каждый поток GPU пакует блок из 6 пикселей (16 байт)
globalWorkSize := C.size_t((g.width * g.height) / 6)
err = C.clEnqueueNDRangeKernel(g.queue, g.kernel, 1, nil, &globalWorkSize, nil, 0, nil, nil)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: dispatch failed: %d", err)
}
// Гарантированная синхронизация записи GPU в host-память: map/unmap.
// На discrete GPU драйвер держит копию в device memory; одного clFinish
// недостаточно — данные обязаны появиться в host_ptr только после unmap.
mapped := C.clEnqueueMapBuffer(g.queue, clMem, C.CL_TRUE, C.CL_MAP_READ, 0, byteSize, 0, nil, nil, &err)
if err != C.CL_SUCCESS {
return fmt.Errorf("opencl: map failed: %d", err)
}
if err = C.clEnqueueUnmapMemObject(g.queue, clMem, mapped, 0, nil, nil); err != C.CL_SUCCESS {
return fmt.Errorf("opencl: unmap failed: %d", err)
}
if err = C.clFinish(g.queue); err != C.CL_SUCCESS {
return fmt.Errorf("opencl: finish failed: %d", err)
}
return nil
}
func (g *OpenCLGenerator) Close() error {
if g.textBuf != nil {
C.clReleaseMemObject(g.textBuf)
g.textBuf = nil
}
if g.atlasBuf != nil {
C.clReleaseMemObject(g.atlasBuf)
g.atlasBuf = nil
}
if g.kernel != nil {
C.clReleaseKernel(g.kernel)
g.kernel = nil
}
if g.prog != nil {
C.clReleaseProgram(g.prog)
g.prog = nil
}
if g.queue != nil {
C.clReleaseCommandQueue(g.queue)
g.queue = nil
}
if g.ctx != nil {
C.clReleaseContext(g.ctx)
g.ctx = nil
}
return nil
}
+236
View File
@@ -0,0 +1,236 @@
// WGPU pattern gen: renders v210 frames with a wgpu (WebGPU/Vulkan)
// compute shader. Pure Go — no cgo in the GPU layer.
package generator
import (
"context"
"encoding/binary"
"fmt"
"os"
"time"
"github.com/gogpu/gputypes"
"github.com/gogpu/wgpu"
// Register all available GPU backends (Vulkan, DX12, GLES, Metal, etc.)
_ "github.com/gogpu/wgpu/hal/allbackends"
)
const wgpuWorkgroupSize = 64
// WGPUGenerator renders v210 frames on the GPU via wgpu and implements
// FrameGenerator. Per frame the kernel packs the pattern into a storage
// buffer, the GPU DMAs it into a persistent host-visible buffer, and the
// mapped contents are copied straight into the destination grain.
type WGPUGenerator struct {
instance *wgpu.Instance
adapter *wgpu.Adapter
device *wgpu.Device
queue *wgpu.Queue
shader *wgpu.ShaderModule
bgl *wgpu.BindGroupLayout
bg *wgpu.BindGroup
pl *wgpu.PipelineLayout
pipeline *wgpu.ComputePipeline
out *wgpu.Buffer
host *wgpu.Buffer
uniform *wgpu.Buffer
params []byte
width int
height int
blocks int
frameSize uint64
}
var _ FrameGenerator = (*WGPUGenerator)(nil)
func NewWGPUGenerator(width, height uint, kernelPath string) (*WGPUGenerator, error) {
g := &WGPUGenerator{
width: int(width),
height: int(height),
blocks: int(width*height) / 6,
params: make([]byte, 16),
}
g.frameSize = uint64(g.blocks) * 16
binary.LittleEndian.PutUint32(g.params[0:], uint32(width))
binary.LittleEndian.PutUint32(g.params[4:], uint32(height))
var err error
if g.instance, err = wgpu.CreateInstance(nil); err != nil {
return nil, fmt.Errorf("wgpu: create instance: %w", err)
}
if g.adapter, err = g.instance.RequestAdapter(nil); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: request adapter: %w", err)
}
if g.device, err = g.adapter.RequestDevice(nil); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: request device: %w", err)
}
g.queue = g.device.Queue()
wgsl, err := os.ReadFile(kernelPath)
if err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: read kernel: %w", err)
}
if g.shader, err = g.device.CreateShaderModule(&wgpu.ShaderModuleDescriptor{
Label: "v210-shader", WGSL: string(wgsl),
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: shader: %w", err)
}
if g.out, err = g.device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-out", Size: g.frameSize,
Usage: wgpu.BufferUsageStorage | wgpu.BufferUsageCopySrc,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: out buffer: %w", err)
}
if g.host, err = g.device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-host", Size: g.frameSize,
Usage: wgpu.BufferUsageCopyDst | wgpu.BufferUsageMapRead,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: host buffer: %w", err)
}
if g.uniform, err = g.device.CreateBuffer(&wgpu.BufferDescriptor{
Label: "v210-params", Size: uint64(len(g.params)),
Usage: wgpu.BufferUsageUniform | wgpu.BufferUsageCopyDst,
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: params buffer: %w", err)
}
if err := g.queue.WriteBuffer(g.uniform, 0, g.params); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: write params: %w", err)
}
if g.bgl, err = g.device.CreateBindGroupLayout(&wgpu.BindGroupLayoutDescriptor{
Label: "v210-bgl",
Entries: []gputypes.BindGroupLayoutEntry{
{Binding: 0, Visibility: wgpu.ShaderStageCompute, Buffer: &gputypes.BufferBindingLayout{Type: gputypes.BufferBindingTypeStorage}},
{Binding: 1, Visibility: wgpu.ShaderStageCompute, Buffer: &gputypes.BufferBindingLayout{Type: gputypes.BufferBindingTypeUniform}},
},
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: bind group layout: %w", err)
}
if g.bg, err = g.device.CreateBindGroup(&wgpu.BindGroupDescriptor{
Label: "v210-bg", Layout: g.bgl,
Entries: []wgpu.BindGroupEntry{
{Binding: 0, Buffer: g.out, Size: g.frameSize},
{Binding: 1, Buffer: g.uniform, Size: uint64(len(g.params))},
},
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: bind group: %w", err)
}
if g.pl, err = g.device.CreatePipelineLayout(&wgpu.PipelineLayoutDescriptor{
Label: "v210-pl", BindGroupLayouts: []*wgpu.BindGroupLayout{g.bgl},
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: pipeline layout: %w", err)
}
if g.pipeline, err = g.device.CreateComputePipeline(&wgpu.ComputePipelineDescriptor{
Label: "v210-pipeline", Layout: g.pl, Module: g.shader, EntryPoint: "main",
}); err != nil {
g.Close()
return nil, fmt.Errorf("wgpu: pipeline: %w", err)
}
return g, nil
}
func (g *WGPUGenerator) GenerateFrame(dest []byte, frameIndex int) error {
if uint64(len(dest)) < g.frameSize {
return fmt.Errorf("wgpu: dest too small: %d bytes, need %d", len(dest), g.frameSize)
}
binary.LittleEndian.PutUint32(g.params[8:], uint32(frameIndex))
if err := g.queue.WriteBuffer(g.uniform, 0, g.params); err != nil {
return fmt.Errorf("wgpu: write params: %w", err)
}
encoder, err := g.device.CreateCommandEncoder(nil)
if err != nil {
return fmt.Errorf("wgpu: encoder: %w", err)
}
pass, err := encoder.BeginComputePass(nil)
if err != nil {
return fmt.Errorf("wgpu: compute pass: %w", err)
}
pass.SetPipeline(g.pipeline)
pass.SetBindGroup(0, g.bg, nil)
pass.Dispatch(uint32((g.blocks+wgpuWorkgroupSize-1)/wgpuWorkgroupSize), 1, 1)
if err := pass.End(); err != nil {
return fmt.Errorf("wgpu: end pass: %w", err)
}
encoder.CopyBufferToBuffer(g.out, 0, g.host, 0, g.frameSize)
cmd, err := encoder.Finish()
if err != nil {
return fmt.Errorf("wgpu: finish: %w", err)
}
if _, err := g.queue.Submit(cmd); err != nil {
return fmt.Errorf("wgpu: submit: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := g.host.Map(ctx, wgpu.MapModeRead, 0, g.frameSize); err != nil {
return fmt.Errorf("wgpu: map: %w", err)
}
rng, err := g.host.MappedRange(0, g.frameSize)
if err != nil {
_ = g.host.Unmap()
return fmt.Errorf("wgpu: mapped range: %w", err)
}
copy(dest, rng.Bytes())
return g.host.Unmap()
}
func (g *WGPUGenerator) Close() error {
if g.pipeline != nil {
g.pipeline.Release()
g.pipeline = nil
}
if g.pl != nil {
g.pl.Release()
g.pl = nil
}
if g.bg != nil {
g.bg.Release()
g.bg = nil
}
if g.bgl != nil {
g.bgl.Release()
g.bgl = nil
}
if g.shader != nil {
g.shader.Release()
g.shader = nil
}
if g.uniform != nil {
g.uniform.Release()
g.uniform = nil
}
if g.host != nil {
g.host.Release()
g.host = nil
}
if g.out != nil {
g.out.Release()
g.out = nil
}
if g.device != nil {
g.device.Release()
g.device = nil
}
if g.adapter != nil {
g.adapter.Release()
g.adapter = nil
}
if g.instance != nil {
g.instance.Release()
g.instance = nil
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package generator
import (
"encoding/binary"
"path/filepath"
"testing"
)
func TestWGPUMoveSquare(t *testing.T) {
const width, height = 1920, 1080
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "v210_bars_move.wgsl"))
if err != nil {
t.Fatalf("init: %v", err)
}
defer g.Close()
buf := make([]byte, width*height*8/3)
sample := func(x, y int) (yc, cb, cr uint32) {
p := y*width + x
off := (p / 6) * 16
w0 := binary.LittleEndian.Uint32(buf[off:])
w1 := binary.LittleEndian.Uint32(buf[off+4:])
w2 := binary.LittleEndian.Uint32(buf[off+8:])
w3 := binary.LittleEndian.Uint32(buf[off+12:])
switch p % 6 {
case 0:
return (w0 >> 10) & 0x3FF, w0 & 0x3FF, (w0 >> 20) & 0x3FF
case 1:
return w1 & 0x3FF, w0 & 0x3FF, (w0 >> 20) & 0x3FF
case 2:
return (w1 >> 20) & 0x3FF, (w1 >> 10) & 0x3FF, w2 & 0x3FF
case 3:
return (w2 >> 10) & 0x3FF, (w1 >> 10) & 0x3FF, w2 & 0x3FF
case 4:
return w3 & 0x3FF, (w2 >> 20) & 0x3FF, (w3 >> 10) & 0x3FF
default:
return (w3 >> 20) & 0x3FF, (w2 >> 20) & 0x3FF, (w3 >> 10) & 0x3FF
}
}
// square center x=960 sits on the green bar (534/253/207):
// inverted -> Y=1004-534=470, Cb=1024-253=771, Cr=1024-207=817
const invY, invCb, invCr = 470, 771, 817
const greenY = 534 // bar 3 covers x in [823,1098)
// tick 0: offset = 0, square centered at (960,540), half = 75
if err := g.GenerateFrame(buf, 0); err != nil {
t.Fatalf("tick 0: %v", err)
}
if y, cb, cr := sample(960, 540); y != invY || cb != invCb || cr != invCr {
t.Fatalf("tick 0 center: got %d/%d/%d, want inverted green 470/771/817", y, cb, cr)
}
if y, _, _ := sample(860, 540); y != greenY {
t.Fatalf("tick 0 left of square: Y=%d, want green %d", y, greenY)
}
// amplitude = centerX - half = 885: tick 79 (sin~1) puts the square at
// the far right, x in [1770,1920), over the blue bar (111/848/481):
// inverted -> Y=1004-111=893, Cb=1024-848=176, Cr=1024-481=543
const invBlueY, invBlueCb, invBlueCr = 893, 176, 543
if err := g.GenerateFrame(buf, 79); err != nil {
t.Fatalf("tick 79: %v", err)
}
if y, cb, cr := sample(1840, 540); y != invBlueY || cb != invBlueCb || cr != invBlueCr {
t.Fatalf("tick 79 shifted center: got %d/%d/%d, want inverted blue 893/176/543", y, cb, cr)
}
if y, _, _ := sample(960, 540); y != greenY {
t.Fatalf("tick 79 old center: Y=%d, want green %d (square moved away)", y, greenY)
}
// bars untouched far from the square
if y, _, _ := sample(100, 100); y != 721 {
t.Fatalf("tick 79 bars: Y=%d, want white 721", y)
}
}
+67
View File
@@ -0,0 +1,67 @@
package generator
import (
"encoding/binary"
"path/filepath"
"testing"
)
func TestWGPUGenerator(t *testing.T) {
const width, height = 1920, 1080
g, err := NewWGPUGenerator(width, height, filepath.Join("..", "..", "kernels", "v210_bars.wgsl"))
if err != nil {
t.Fatalf("init: %v", err)
}
defer g.Close()
buf := make([]byte, width*height*8/3)
if err := g.GenerateFrame(buf, 0); err != nil {
t.Fatalf("GenerateFrame: %v", err)
}
if err := g.GenerateFrame(buf, 1); err != nil {
t.Fatalf("GenerateFrame 2: %v", err)
}
want := [7][3]uint32{
{721, 512, 512}, {674, 176, 543}, {581, 589, 176},
{534, 253, 207}, {251, 771, 817}, {204, 435, 848}, {111, 848, 481},
}
barOf := func(x int) int {
if b := x * 7 / width; b < 7 {
return b
}
return 6
}
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
p := y*width + x
off := (p / 6) * 16
w0 := binary.LittleEndian.Uint32(buf[off:])
w1 := binary.LittleEndian.Uint32(buf[off+4:])
w2 := binary.LittleEndian.Uint32(buf[off+8:])
w3 := binary.LittleEndian.Uint32(buf[off+12:])
var yv, cb, cr uint32
switch p % 6 {
case 0:
yv, cb, cr = (w0>>10)&0x3FF, w0&0x3FF, (w0>>20)&0x3FF
case 1:
yv = w1 & 0x3FF
case 2:
yv, cb, cr = (w1>>20)&0x3FF, (w1>>10)&0x3FF, w2&0x3FF
case 3:
yv = (w2 >> 10) & 0x3FF
case 4:
yv, cb, cr = w3&0x3FF, (w2>>20)&0x3FF, (w3>>10)&0x3FF
case 5:
yv = (w3 >> 20) & 0x3FF
}
b := want[barOf(x)]
if yv != b[0] {
t.Fatalf("pixel (%d,%d): Y=%d want %d", x, y, yv, b[0])
}
if x%2 == 0 && (cb != b[1] || cr != b[2]) {
t.Fatalf("pixel (%d,%d): Cb=%d Cr=%d want %d/%d", x, y, cb, cr, b[1], b[2])
}
}
}
}