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
+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
}