This commit is contained in:
Dmitry Sergeev
2026-08-20 17:02:17 +03:00
commit b953e38a69
6 changed files with 274 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
package main
import (
"fmt"
"log"
"runtime"
"unsafe"
vk "github.com/christerso/vulkan-go/vk"
"github.com/ebitengine/purego"
sdl "github.com/jupiterrider/purego-sdl3/sdl"
)
const (
APP_NAME = "MXL Player"
APP_VER = "0.0.1"
WIN_WIDTH int32 = 1280
WIN_HEIGHT int32 = 720
)
const (
initVideo uint32 = 0x00000020
windowVulkan uint64 = 0x0000000010000000
windowResizable uint64 = 0x0000000000000020
eventQuit uint32 = 0x100
eventKeyDown uint32 = 0x300
keyEscape uint32 = 0x1B
)
var (
sdlInit func(flags uint32) bool
sdlQuit func()
sdlGetError func() uintptr
sdlCreateWindow func(title *byte, w, h int32, flags uint64) uintptr
sdlDestroyWindow func(window uintptr)
sdlVulkanGetInstanceExtensions func(count *uint32) uintptr
sdlVulkanCreateSurface func(window, instance, allocator uintptr, surface *uint64) bool
sdlPollEvent func(event unsafe.Pointer) bool
sdlGetWindowSizeInPixels func(window uintptr, w, h *int32) bool
)
func sdlError() string { return cstr(sdlGetError()) }
var loaded = false
func loadSDLMissing() error {
if loaded {
return nil
}
h, err := purego.Dlopen("libSDL3.so.0", purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
return fmt.Errorf("win: load SDL3: %w", err)
}
purego.RegisterLibFunc(&sdlInit, h, "SDL_Init")
purego.RegisterLibFunc(&sdlQuit, h, "SDL_Quit")
purego.RegisterLibFunc(&sdlGetError, h, "SDL_GetError")
purego.RegisterLibFunc(&sdlCreateWindow, h, "SDL_CreateWindow")
purego.RegisterLibFunc(&sdlDestroyWindow, h, "SDL_DestroyWindow")
purego.RegisterLibFunc(&sdlVulkanGetInstanceExtensions, h, "SDL_Vulkan_GetInstanceExtensions")
purego.RegisterLibFunc(&sdlVulkanCreateSurface, h, "SDL_Vulkan_CreateSurface")
purego.RegisterLibFunc(&sdlPollEvent, h, "SDL_PollEvent")
purego.RegisterLibFunc(&sdlGetWindowSizeInPixels, h, "SDL_GetWindowSizeInPixels")
loaded = true
return nil
}
// cstr reads a NUL-terminated C string at p.
func cstr(p uintptr) string {
if p == 0 {
return ""
}
var n int
for *(*byte)(unsafe.Pointer(p + uintptr(n))) != 0 {
n++
}
return string(unsafe.Slice((*byte)(unsafe.Pointer(p)), n))
}
// cbytes returns a NUL-terminated copy of s as *byte, kept alive by the caller.
func cbytes(s string) *byte {
b := make([]byte, len(s)+1)
copy(b, s)
runtime.KeepAlive(b)
return &b[0]
}
func main() {
runtime.LockOSThread()
// SDL init
// if !sdl.Init(sdl.InitVideo) {
// log.Fatal(sdl.GetError())
// }
// defer sdl.Quit()
// window := sdl.CreateWindow(
// fmt.Sprintf("%s ver. %s", APP_NAME, APP_VER),
// 1280,
// 720,
// sdl.WindowVulkan|sdl.WindowResizable,
// )
// if window == nil {
// log.Fatal(sdl.GetError())
// }
// defer sdl.DestroyWindow(window)
if err := loadSDLMissing(); err != nil {
panic(err)
}
if !sdlInit(initVideo) {
log.Fatalf("SDL_Init: %s", sdlError())
return
}
windowHandler := sdlCreateWindow(cbytes(APP_NAME), WIN_WIDTH, WIN_HEIGHT, windowVulkan|windowResizable)
if windowHandler == 0 {
sdlQuit()
log.Fatalf("SDL_CreateWindow: %s", sdlError())
return
}
// Vulkan init
if err := vk.Load(); err != nil {
panic(err)
}
// Vulkan instance extensions SDL needs.
var sdlExtCount uint32
arr := sdlVulkanGetInstanceExtensions(&sdlExtCount)
if arr == 0 {
log.Fatal("sdlVulkanGetInstanceExtensions is 0")
return
}
sdlExtensions := make([]string, sdlExtCount)
for i := uint32(0); i < sdlExtCount; i++ {
p := *(*uintptr)(unsafe.Pointer(arr + uintptr(i)*unsafe.Sizeof(uintptr(0))))
sdlExtensions[i] = cstr(p)
}
// Vulkan Instance creation
var vkLayers []string
vkExtensions := append(sdlExtensions, vk.ExtDebugUtils)
vkInstance, err := vk.CreateInstance(vk.InstanceConfig{
ApplicationName: APP_NAME,
EngineName: "no engine",
Extensions: vkExtensions,
Layers: vkLayers,
})
if err != nil {
log.Fatalf(err.Error())
panic(err)
}
defer vkInstance.Destroy()
// Vulkan surface
var vkSurface uint64
if !sdlVulkanCreateSurface(windowHandler, uintptr(vkInstance), 0, &vkSurface) {
log.Fatalf("SDL_Vulkan_CreateSurface: %s", sdlError())
return
}
vkSurf := vk.SurfaceKHR(vkSurface)
defer vkInstance.DestroySurface(vkSurf)
// Vulkan Physical Device
devices, err := vkInstance.EnumeratePhysicalDevices()
if err != nil || len(devices) == 0 {
panic("No Vulkan devices")
}
for _, pd := range devices {
info := pd.Info()
fmt.Printf("%s (%s)\n", info.Name, info.Type)
}
vkPhysDevice := devices[0]
// Vulkan Surface & Graphics Queue
gfx, err := vkPhysDevice.GraphicsFamily()
if err != nil {
panic(err.Error())
}
if !vkPhysDevice.SurfaceSupport(gfx, vkSurf) {
log.Fatalf("graphics queue cannot present")
return
}
vkDevice, vkQueue, err := vkPhysDevice.CreateDevice(vk.DeviceConfig{
GraphicsFamily: gfx,
Extensions: []string{"VK_KHR_swapchain"},
})
if err != nil {
panic(err)
}
defer vkDevice.Destroy()
defer vkDevice.WaitIdle()
// TODO: colorspace & render pass
var vkFormat vk.Format
var vkColorSpace uint32
formats, _ := vkPhysDevice.SurfaceFormats(vkSurf)
for _, f := range formats {
if f.Format == vk.FormatB8G8R8A8Srgb && f.ColorSpace == vk.ColorSpaceSRGBNonlinear {
vkFormat = f.Format
vkColorSpace = f.ColorSpace
}
}
var vkPresentMode vk.PresentMode
modes, _ := vkPhysDevice.SurfacePresentModes(vkSurf)
for _, m := range modes {
if m == vk.PresentModeMailbox {
vkPresentMode = m
}
}
vkRenderPass, err := vkDevice.CreateColorDepthRenderPass(vkFormat, vk.FormatD32Sfloat)
if err != nil {
panic(err.Error())
}
defer vkDevice.DestroyRenderPass(vkRenderPass)
// Vulkan Command Pool
vkCommandPool, err := vkDevice.CreateCommandPool(gfx)
if err != nil {
panic(err.Error())
}
defer vkDevice.DestroyCommandPool(vkCommandPool)
vkCommands, err := vkDevice.AllocateCommandBuffers(vkCommandPool, 1)
if err != nil {
panic(err.Error())
}
// Core Loop
running := true
for running {
var event sdl.Event
for sdl.PollEvent(&event) {
switch event.Type() {
case sdl.EventQuit:
running = false
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
module mxl-player
go 1.25.0
require (
github.com/christerso/vulkan-go v0.0.0-20260618152204-bff25e5b7646
github.com/jupiterrider/purego-sdl3 v0.0.0-20260514083405-8523da70a041
)
require github.com/ebitengine/purego v0.10.2 // indirect
+6
View File
@@ -0,0 +1,6 @@
github.com/christerso/vulkan-go v0.0.0-20260618152204-bff25e5b7646 h1:xfc6rFxs+U4XktxQa7jyPfg9g0xlb5STDsQn8sURsyo=
github.com/christerso/vulkan-go v0.0.0-20260618152204-bff25e5b7646/go.mod h1:DIo6W2yFav1DflNKc0W0Oc7mecbdBg2btsaHhIYmwUA=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/jupiterrider/purego-sdl3 v0.0.0-20260514083405-8523da70a041 h1:sDTZNtan3t8NsIM15J4vvuFuqxN6qLtWYhmHnLy5TjU=
github.com/jupiterrider/purego-sdl3 v0.0.0-20260514083405-8523da70a041/go.mod h1:pRSNvzaSfMxcVPHP5VKo5VF5SAFp78pPIGlBIoI8KBw=
Executable
BIN
View File
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
Milestones
M0 — skeleton builds: Go project, go-mxl, SDL3, Vulkan initialization, logging, clean startup/shutdown. Program opens a window and exits correctly.
M1 — MXL source reader: Open one MXL video flow, read grains, print width/height/rate/index, recover cleanly from timeout, and never leak or retain an MXL payload beyond its valid lifetime.
M2 — raw Vulkan upload: Copy V210 payload directly into a mapped Vulkan staging buffer. No color conversion yet. Validate sizes, strides, synchronization, and buffer lifetime.
M3 — V210 GPU decoding: Implement v210.comp; unpack 10-bit samples on GPU and write to an RGBA16F image. Do not introduce an RGB8 intermediate.
M4 — correct color: Implement limited/full-range normalization, BT.709 first, then metadata-driven matrix selection. Add test patterns so you can verify black, white, gray, primary colors, and chroma alignment.
M5 — presentation: Scale RGBA16F to the window while preserving aspect ratio, recreate the swapchain correctly on resize, add fullscreen, VSync, and 10-bit swapchain output where supported with 8-bit fallback.
M6 — timing: Track MXL grain indexes, detect skipped/late frames, avoid accumulating latency, resync after source interruption, and expose render time / frame time / dropped frames.
M7 — resilience: Test writer disappearance, MXL restart, resolution/rate changes, long runtime, fullscreen switching, minimized windows, GPU device/surface errors, and clean shutdown during blocked reads.
M8 — audio: Only after video is reliable. Read Float32 MXL audio, feed SDL audio, add buffering, then synchronize audio/video.
M9 — usable player: Flow discovery/selection, CLI arguments, basic overlay, source info, color info, FPS, dropped frames, fullscreen and freeze controls.
M10 — packaging: Linux and Windows builds first; package libmxl/runtime dependencies appropriately, validate Vulkan loader requirements, and add CI builds.
Id define v1.0 as M0M7 plus basic source selection. Audio can even be 1.1 if getting video stability right is more important.
+8
View File
@@ -0,0 +1,8 @@
M0.1 Go module + SDL3 window
M0.2 Vulkan instance
M0.3 enumerate/select GPU
M0.4 SDL Vulkan surface
M0.5 logical device + queues
M0.6 swapchain
M0.7 clear window to a solid color
M0.8 resize + clean teardown