initial
This commit is contained in:
+236
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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=
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
I’d define v1.0 as M0–M7 plus basic source selection. Audio can even be 1.1 if getting video stability right is more important.
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user