and the video comes into chat

This commit is contained in:
Dmitry Sergeev
2026-08-22 14:59:54 +03:00
parent 32f7dea31c
commit 84572fb88c
11 changed files with 554 additions and 13 deletions
+184 -8
View File
@@ -1,9 +1,14 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"mxl-player/internal/source"
"runtime"
"time"
"unsafe"
vk "github.com/christerso/vulkan-go/vk"
@@ -13,8 +18,8 @@ import (
const (
APP_NAME = "MXL Player"
APP_VER = "0.0.1"
WIN_WIDTH int32 = 1280
WIN_HEIGHT int32 = 720
WIN_WIDTH int32 = 1920
WIN_HEIGHT int32 = 1080
)
const (
@@ -41,10 +46,10 @@ var (
func sdlError() string { return cstr(sdlGetError()) }
var loaded = false
var sdlLoaded = false
func loadSDLMissing() error {
if loaded {
if sdlLoaded {
return nil
}
h, err := purego.Dlopen("libSDL3.so.0", purego.RTLD_NOW|purego.RTLD_GLOBAL)
@@ -60,7 +65,7 @@ func loadSDLMissing() error {
purego.RegisterLibFunc(&sdlVulkanCreateSurface, h, "SDL_Vulkan_CreateSurface")
purego.RegisterLibFunc(&sdlPollEvent, h, "SDL_PollEvent")
purego.RegisterLibFunc(&sdlGetWindowSizeInPixels, h, "SDL_GetWindowSizeInPixels")
loaded = true
sdlLoaded = true
return nil
}
@@ -85,6 +90,11 @@ func cbytes(s string) *byte {
}
func main() {
// TODO: remove flags defaults
mxlDomain := flag.String("d", "/dev/shm/mxl", "MXL domain")
mxlVideoFlowID := flag.String("v", "5fbec3b1-1b0f-417d-9059-8b94a47197ed", "MXL video flow UUID")
flag.Parse()
runtime.LockOSThread()
if err := loadSDLMissing(); err != nil {
panic(err)
@@ -94,7 +104,7 @@ func main() {
return
}
windowHandler := sdlCreateWindow(cbytes(APP_NAME), WIN_WIDTH, WIN_HEIGHT, windowVulkan|windowResizable)
windowHandler := sdlCreateWindow(cbytes(fmt.Sprintf("%s %s", APP_NAME, APP_VER)), WIN_WIDTH, WIN_HEIGHT, windowVulkan|windowResizable)
if windowHandler == 0 {
sdlQuit()
log.Fatalf("SDL_CreateWindow: %s", sdlError())
@@ -173,7 +183,8 @@ func main() {
var vkColorSpace uint32
formats, _ := vkPhysDevice.SurfaceFormats(vkSurf)
for _, f := range formats {
if f.Format == vk.FormatB8G8R8A8Srgb && f.ColorSpace == vk.ColorSpaceSRGBNonlinear {
// Prefer 10-bit RGB (A2B10G10R10_UNORM = 64) to preserve V210's 10 bits
if f.Format == vk.Format(64) && f.ColorSpace == vk.ColorSpaceSRGBNonlinear {
vkFormat = f.Format
vkColorSpace = f.ColorSpace
}
@@ -208,6 +219,97 @@ func main() {
panic(err.Error())
}
// MXL Source
mxlSrc, err := source.Open(*mxlDomain, *mxlVideoFlowID)
if err != nil {
log.Fatalf("source: %v\n", err)
}
defer mxlSrc.Close()
frameSize := vk.DeviceSize(mxlSrc.Stride()) * vk.DeviceSize(mxlSrc.Height())
fmt.Printf("source: %dx%d stride=%d frameSize=%d\n",
mxlSrc.Width(), mxlSrc.Height(), mxlSrc.Stride(), frameSize)
// Staging buffer: host-visible, persistently mapped. The reader goroutine
// writes V210 bytes here; the GPU copies from it.
staging, err := vkDevice.CreateBuffer(vkPhysDevice, vk.BufferConfig{
Size: frameSize,
Usage: vk.BufferUsageTransferSrc,
Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent,
Map: true,
})
if err != nil {
panic(err)
}
defer vkDevice.DestroyBuffer(staging)
// Device-local V210 buffer: fast for the GPU to read (M3 compute), CPU can't
// write it. Filled each frame by a CopyBuffer from staging.
v210Buf, err := vkDevice.CreateBuffer(vkPhysDevice, vk.BufferConfig{
Size: frameSize,
Usage: vk.BufferUsageTransferDst | vk.BufferUsageStorageBuffer,
Properties: vk.MemoryDeviceLocal,
Map: false,
})
if err != nil {
panic(err)
}
defer vkDevice.DestroyBuffer(v210Buf)
// Decode pipeline: fullscreen triangle, fragment reads V210 from the
// staging buffer and writes 10-bit RGB to the swapchain color attachment.
vertModule, err := vkDevice.CreateShaderModule(vertSPV)
if err != nil {
panic(err)
}
defer vkDevice.DestroyShaderModule(vertModule)
fragModule, err := vkDevice.CreateShaderModule(fragSPV)
if err != nil {
panic(err)
}
defer vkDevice.DestroyShaderModule(fragModule)
// Descriptor set layout: binding 0 = storage buffer (V210), fragment stage.
decodeDSL, err := vkDevice.CreateDescriptorSetLayout([]vk.DescriptorBinding{
{Binding: 0, Type: vk.DescriptorStorageBuffer, Count: 1, Stages: vk.ShaderStageFragment},
})
if err != nil {
panic(err)
}
defer vkDevice.DestroyDescriptorSetLayout(decodeDSL)
// Pipeline layout: the set layout + push constants {width,height,strideBytes}.
decodeLayout, err := vkDevice.CreatePipelineLayout([]vk.DescriptorSetLayout{decodeDSL}, vk.ShaderStageFragment, 12)
if err != nil {
panic(err)
}
defer vkDevice.DestroyPipelineLayout(decodeLayout)
decodePipeline, err := vkDevice.CreateGraphicsPipeline(vk.GraphicsPipelineConfig{
Layout: decodeLayout,
RenderPass: vkRenderPass,
VertexShader: vertModule,
FragShader: fragModule,
Topology: vk.TopologyTriangleList,
PolygonMode: vk.PolygonFill,
CullMode: vk.CullNone,
FrontFace: vk.FrontFaceCounterClockwise,
})
if err != nil {
panic(err)
}
defer vkDevice.DestroyPipeline(decodePipeline)
// Descriptor pool + set, bound once to the staging buffer
descPool, err := vkDevice.CreateDescriptorPool(1, map[vk.DescriptorType]uint32{
vk.DescriptorStorageBuffer: 1,
})
if err != nil {
panic(err)
}
defer vkDevice.DestroyDescriptorPool(descPool)
decodeSet, err := vkDevice.AllocateDescriptorSet(descPool, decodeDSL)
if err != nil {
panic(err)
}
vkDevice.UpdateBufferDescriptor(decodeSet, 0, vk.DescriptorStorageBuffer, staging.Buffer, 0, vk.WholeSize)
// Swapchain
var (
vkExtent vk.Extent2D
vkSwapchain vk.SwapchainKHR
@@ -350,6 +452,46 @@ func main() {
defer vkDevice.DestroyFence(inFlight)
defer vkDevice.WaitIdle()
// Reader goroutine: stages V210 bytes into the staging buffer.
// Single-flight handshake: it only writes staging after the render thread
// grants permission (post-WaitFence), so the GPU is never reading it.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
grant := make(chan struct{})
staged := make(chan struct{})
go func() {
for {
select {
case <-grant:
case <-ctx.Done():
return
}
f, err := mxlSrc.NextCtx(ctx, 200*time.Millisecond)
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
log.Printf("source: %v", err)
cancel()
return
}
// Copy the borrowed payload into staging BEFORE the next read
// invalidates it. Within the grain's valid lifetime
vk.CopyToMapped(staging.Mapped, f.Payload)
// fmt.Println("staged", f.Index)
select {
case staged <- struct{}{}:
case <-ctx.Done():
return
}
}
}()
type decodePushConstants struct {
Width, Height, StrideBytes, WinW, WinH uint32
}
// Core Loop
running := true
for running {
@@ -382,7 +524,22 @@ func main() {
panic(err)
}
// Record: clear color & depth attachments
// Grant the goroutine permission to write staging (GPU is idle now),
// then wait for it to stage a new payload.
select {
case grant <- struct{}{}:
case <-ctx.Done():
running = false
continue
}
select {
case <-staged:
case <-ctx.Done():
running = false
continue
}
// Record: copy staged V210 into device-local buffer, then clear
cmd := vkCommands[0]
if err := cmd.Reset(); err != nil {
panic(err)
@@ -390,6 +547,7 @@ func main() {
if err := cmd.Begin(vk.CommandBufferOneTimeSubmit); err != nil {
panic(err)
}
cmd.CopyBuffer(staging.Buffer, v210Buf.Buffer, frameSize)
cmd.BeginRenderPass(
vkRenderPass,
fbs[imageIndex],
@@ -399,6 +557,24 @@ func main() {
vk.ClearDepthStencil(1.0, 0),
},
)
cmd.SetViewport(vk.Viewport{
X: 0, Y: 0,
Width: float32(vkExtent.Width),
Height: float32(vkExtent.Height),
MinDepth: 0, MaxDepth: 1,
})
cmd.SetScissor(vk.Rect2D{Offset: vk.Offset2D{X: 0, Y: 0}, Extent: vkExtent})
cmd.BindPipeline(decodePipeline)
cmd.BindDescriptorSet(decodeLayout, 0, decodeSet)
decodePC := decodePushConstants{
Width: mxlSrc.Width(),
Height: mxlSrc.Height(),
StrideBytes: mxlSrc.Stride(),
WinW: vkExtent.Width,
WinH: vkExtent.Height,
}
cmd.PushConstants(decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&decodePC), 20)
cmd.Draw(3, 1, 0, 0)
cmd.EndRenderPass()
if err := cmd.End(); err != nil {
panic(err)
+9
View File
@@ -0,0 +1,9 @@
package main
import _ "embed"
//go:embed shaders/triangle.vert.spv
var vertSPV []byte
//go:embed shaders/decode.frag.spv
var fragSPV []byte
+76
View File
@@ -0,0 +1,76 @@
#version 450
layout(set = 0, binding = 0, std430) readonly buffer V210 {
uint words[];
};
layout(push_constant) uniform PC {
uint width;
uint height;
uint strideBytes;
uint winW;
uint winH;
} pc;
layout(location = 0) out vec4 fragColor;
void main() {
// Letterbox: fit video into the window, preserving aspect ratio.
float sx = float(pc.winW) / float(pc.width);
float sy = float(pc.winH) / float(pc.height);
float scale = min(sx, sy);
float dispW = float(pc.width) * scale;
float dispH = float(pc.height) * scale;
float offX = (float(pc.winW) - dispW) * 0.5;
float offY = (float(pc.winH) - dispH) * 0.5;
float fbx = gl_FragCoord.x - offX;
float fby = gl_FragCoord.y - offY;
if (fbx < 0.0 || fbx >= dispW || fby < 0.0 || fby >= dispH) {
fragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
// Framebuffer y is bottom-origin; flip to video top-origin.
uint x = uint(fbx / scale);
uint y = uint(fby / scale);
// V210: 6 pixels per group of 4 words; 3 ten-bit components per word
// (bits 0-9 / 10-19 / 20-29). Stream order: Cb Y Cr Y Cb Y Cr Y ...
uint group = x / 6u;
uint sub = x % 6u;
uint wordsPerLine = pc.strideBytes / 4u;
uint base = y * wordsPerLine + group * 4u;
uint w0 = words[base + 0u];
uint w1 = words[base + 1u];
uint w2 = words[base + 2u];
uint w3 = words[base + 3u];
uint cb0 = (w0 ) & 0x3FFu;
uint y0 = (w0 >> 10u) & 0x3FFu;
uint cr0 = (w0 >> 20u) & 0x3FFu;
uint y1 = (w1 ) & 0x3FFu;
uint cb1 = (w1 >> 10u) & 0x3FFu;
uint y2 = (w1 >> 20u) & 0x3FFu;
uint cr1 = (w2 ) & 0x3FFu;
uint y3 = (w2 >> 10u) & 0x3FFu;
uint cb2 = (w2 >> 20u) & 0x3FFu;
uint y4 = (w3 ) & 0x3FFu;
uint cr2 = (w3 >> 10u) & 0x3FFu;
uint y5 = (w3 >> 20u) & 0x3FFu;
float Y, Cb, Cr;
if (sub == 0u) { Y = float(y0); Cb = float(cb0); Cr = float(cr0); }
else if (sub == 1u) { Y = float(y1); Cb = float(cb0); Cr = float(cr0); }
else if (sub == 2u) { Y = float(y2); Cb = float(cb1); Cr = float(cr1); }
else if (sub == 3u) { Y = float(y3); Cb = float(cb1); Cr = float(cr1); }
else if (sub == 4u) { Y = float(y4); Cb = float(cb2); Cr = float(cr2); }
else { Y = float(y5); Cb = float(cb2); Cr = float(cr2); }
float yf = (Y - 64.0) / 876.0;
float uf = (Cb - 512.0) / 896.0;
float vf = (Cr - 512.0) / 896.0;
float r = yf + 1.5748 * vf;
float g = yf - 0.1873 * uf - 0.4681 * vf;
float b = yf + 1.8556 * uf;
fragColor = vec4(clamp(r, 0.0, 1.0), clamp(g, 0.0, 1.0), clamp(b, 0.0, 1.0), 1.0);
}
Binary file not shown.
+12
View File
@@ -0,0 +1,12 @@
#version 450
// Fullscreen triangle, no vertex buffer. One triangle covers the viewport.
vec2 positions[3] = vec2[](
vec2(-1.0, -1.0),
vec2( 3.0, -1.0),
vec2(-1.0, 3.0)
);
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}
Binary file not shown.
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"flag"
"fmt"
"log"
"mxl-player/internal/source"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
domain := flag.String("domain", "/dev/shm/mxl", "MXL domain")
flowID := flag.String("flow", "5fbec3b1-1b0f-417d-9059-8b94a47197ed", "Flow UUID")
timeout := flag.Duration("timeout", 200*time.Millisecond, "Per-grain read timeout")
count := flag.Int("count", 10, "Stop after N grains (0 = run forever)")
flag.Parse()
src, err := source.Open(*domain, *flowID)
if err != nil {
log.Fatal(err)
}
defer src.Close()
fmt.Printf("format=%s rate=%d/%d stride=%d grainCount=%d\n",
src.Format(),
src.Rate().Num,
src.Rate().Den,
src.Stride(),
src.GrainCount())
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
grains := 0
for {
select {
case <-stop:
fmt.Printf("\nstopping: %d grains read\n", grains)
return
default:
}
f, err := src.Next(*timeout)
if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
lines := uint32(0)
if src.Stride() > 0 {
lines = f.Size / src.Stride()
}
fmt.Printf("idx=%d size=%d (%dx%d, %d lines) invalid=%v\n",
f.Index, f.Size, f.Width, f.Height, lines, f.Invalid)
grains++
if *count > 0 && grains >= *count {
fmt.Printf("done: %d grains read\n", grains)
return
}
}
}