Compare commits
6 Commits
2b247d0fd6
...
2563c34604
| Author | SHA1 | Date | |
|---|---|---|---|
| 2563c34604 | |||
| 9889054b9f | |||
| b0bcd879e3 | |||
| 3b51bea58a | |||
| 3398cdf3f1 | |||
| 16692b693a |
+208
-77
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"mxl-player/internal/imgui"
|
||||||
"mxl-player/internal/renderer"
|
"mxl-player/internal/renderer"
|
||||||
"mxl-player/internal/sdl"
|
"mxl-player/internal/sdl"
|
||||||
"mxl-player/internal/source"
|
"mxl-player/internal/source"
|
||||||
@@ -14,8 +15,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
cimgui "github.com/AllenDang/cimgui-go/imgui"
|
||||||
vk "github.com/christerso/vulkan-go/vk"
|
vk "github.com/christerso/vulkan-go/vk"
|
||||||
"github.com/qvest-digital/go-mxl/mxl"
|
|
||||||
pflag "github.com/spf13/pflag"
|
pflag "github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,8 +77,10 @@ func checkMXLargs(args appArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// video: 5fbec3b1-1b0f-417d-9059-8b94a47197ed
|
// timelapse video: 5fbec3b1-1b0f-417d-9059-8b94a47197ed
|
||||||
// audio: 5fbec3b1-1b0f-417d-9059-8b94a47197ec
|
// timelapse audio: 5fbec3b1-1b0f-417d-9059-8b94a47197ec
|
||||||
|
// f1 video: 5fbec3b1-1b0f-417d-9059-8b94a47197ef
|
||||||
|
// f1 audio: 5fbec3b1-1b0f-417d-9059-8b94a47197eb
|
||||||
var args appArgs
|
var args appArgs
|
||||||
flagSet := pflag.NewFlagSet(APP_NAME, pflag.ContinueOnError)
|
flagSet := pflag.NewFlagSet(APP_NAME, pflag.ContinueOnError)
|
||||||
flagSet.Usage = func() { printUsage(os.Stderr) }
|
flagSet.Usage = func() { printUsage(os.Stderr) }
|
||||||
@@ -137,6 +140,13 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImGui init
|
||||||
|
gui := imgui.New()
|
||||||
|
defer gui.Destroy()
|
||||||
|
sdl.StartTextInput(windowHandler)
|
||||||
|
defer sdl.StopTextInput(windowHandler)
|
||||||
|
// fin on ImGui init
|
||||||
|
|
||||||
if err := vk.Load(); err != nil {
|
if err := vk.Load(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -250,7 +260,6 @@ func main() {
|
|||||||
fmt.Printf("sync: video %dx%d audio %dch batch=%d\n",
|
fmt.Printf("sync: video %dx%d audio %dch batch=%d\n",
|
||||||
syncSrc.Width(), syncSrc.Height(), aChans, audioBatch)
|
syncSrc.Width(), syncSrc.Height(), aChans, audioBatch)
|
||||||
case args.VideoFlowId != "":
|
case args.VideoFlowId != "":
|
||||||
|
|
||||||
videoSrc, err = source.Open(args.Domain, args.VideoFlowId)
|
videoSrc, err = source.Open(args.Domain, args.VideoFlowId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("source: %v", err)
|
log.Fatalf("source: %v", err)
|
||||||
@@ -317,33 +326,55 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer r.Destroy()
|
defer r.Destroy()
|
||||||
|
guiBackend, err := imgui.NewVulkanBackend(
|
||||||
|
vkPhysDevice, vkDevice, vkQueue, r.CmdPool(), r.RenderPass())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer guiBackend.Destroy()
|
||||||
|
r.ImGuiDraw = func(cmd vk.CommandBuffer) {
|
||||||
|
guiBackend.RecordDraw(cmd, gui.LastDrawData())
|
||||||
|
}
|
||||||
defer vkDevice.WaitIdle()
|
defer vkDevice.WaitIdle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GUI state (accessible from doReconnect + goroutine)
|
||||||
|
var (
|
||||||
|
domainStr string = args.Domain
|
||||||
|
videoStr string = args.VideoFlowId
|
||||||
|
audioStr string = args.AudioFlowId
|
||||||
|
showStats bool = true
|
||||||
|
)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
grant := make(chan struct{}, 1)
|
type reconnectParams struct {
|
||||||
|
domain string
|
||||||
|
video string
|
||||||
|
audio string
|
||||||
|
}
|
||||||
|
// One control channel: grant (empty params) or reconnect (with params).
|
||||||
|
control := make(chan reconnectParams, 1)
|
||||||
staged := make(chan uint64)
|
staged := make(chan uint64)
|
||||||
failed := make(chan struct{})
|
|
||||||
|
|
||||||
reopen := func() error {
|
reopen := func(params reconnectParams) error {
|
||||||
|
// Close current sources
|
||||||
if syncSrc != nil {
|
if syncSrc != nil {
|
||||||
_ = syncSrc.Close()
|
_ = syncSrc.Close()
|
||||||
|
syncSrc = nil
|
||||||
}
|
}
|
||||||
if videoSrc != nil {
|
if videoSrc != nil {
|
||||||
_ = videoSrc.Close()
|
_ = videoSrc.Close()
|
||||||
|
videoSrc = nil
|
||||||
}
|
}
|
||||||
if audioSrc != nil {
|
if audioSrc != nil {
|
||||||
_ = audioSrc.Close()
|
_ = audioSrc.Close()
|
||||||
|
audioSrc = nil
|
||||||
}
|
}
|
||||||
for {
|
// Try once. Return error if fails — caller loops back to select
|
||||||
select {
|
// and can pick up new reconnect params or a new grant.
|
||||||
case <-ctx.Done():
|
if params.video != "" && params.audio != "" {
|
||||||
return ctx.Err()
|
s, e := source.OpenSync(params.domain, params.video, params.audio)
|
||||||
default:
|
|
||||||
}
|
|
||||||
if syncSrc != nil {
|
|
||||||
s, e := source.OpenSync(args.Domain, args.VideoFlowId, args.AudioFlowId)
|
|
||||||
if e == nil {
|
if e == nil {
|
||||||
if r != nil {
|
if r != nil {
|
||||||
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
|
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
|
||||||
@@ -354,11 +385,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncSrc = s
|
syncSrc = s
|
||||||
|
aChans = s.Channels()
|
||||||
|
audioBatch = uint64(s.AudioRate().Num) / uint64(s.Rate().Num)
|
||||||
|
if audioBatch == 0 {
|
||||||
|
audioBatch = 1
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("source: reopen retry: %v", e)
|
return e
|
||||||
} else if videoSrc != nil {
|
} else if params.video != "" {
|
||||||
s, e := source.Open(args.Domain, args.VideoFlowId)
|
s, e := source.Open(params.domain, params.video)
|
||||||
if e == nil {
|
if e == nil {
|
||||||
if r != nil {
|
if r != nil {
|
||||||
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
|
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
|
||||||
@@ -371,17 +407,29 @@ func main() {
|
|||||||
videoSrc = s
|
videoSrc = s
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("source: reopen retry: %v", e)
|
return e
|
||||||
} else if audioSrc != nil {
|
} else if params.audio != "" {
|
||||||
s, e := source.OpenAudio(args.Domain, args.AudioFlowId)
|
s, e := source.OpenAudio(params.domain, params.audio)
|
||||||
if e == nil {
|
if e == nil {
|
||||||
audioSrc = s
|
audioSrc = s
|
||||||
|
aChans = s.Channels()
|
||||||
|
audioBatch = uint64(s.Rate().Num) / (100 * uint64(s.Rate().Den))
|
||||||
|
if audioBatch == 0 {
|
||||||
|
audioBatch = 1
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
log.Printf("source: reopen retry: %v", e)
|
return e
|
||||||
}
|
}
|
||||||
time.Sleep(500 * time.Millisecond)
|
return fmt.Errorf("reopen: no flow specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doReconnect := func() {
|
||||||
|
select {
|
||||||
|
case <-control:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -391,6 +439,25 @@ func main() {
|
|||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
case params := <-control:
|
||||||
|
if params.video != "" || params.audio != "" {
|
||||||
|
if rerr := reopen(params); rerr != nil {
|
||||||
|
if errors.Is(rerr, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("source: reopen failed: %v, retrying", rerr)
|
||||||
|
select {
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case control <- params:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
queued := sdl.GetAudioStreamQueued(audioStream)
|
queued := sdl.GetAudioStreamQueued(audioStream)
|
||||||
@@ -408,19 +475,23 @@ func main() {
|
|||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, mxl.ErrFlowInvalid) {
|
log.Printf("source: %v", err)
|
||||||
log.Printf("source: flow invalid, reopening")
|
params := reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}
|
||||||
if rerr := reopen(); rerr != nil {
|
select {
|
||||||
log.Printf("source: reopen failed: %v", rerr)
|
case <-control:
|
||||||
cancel()
|
default:
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
case control <- params:
|
||||||
|
default:
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("source: %v", err)
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if f.Samples != nil && audioStream != 0 {
|
if f.Samples != nil && audioStream != 0 {
|
||||||
sdl.PutAudioStreamData(audioStream, interleaveAudio(f.Samples))
|
sdl.PutAudioStreamData(audioStream, interleaveAudio(f.Samples))
|
||||||
}
|
}
|
||||||
@@ -429,11 +500,30 @@ func main() {
|
|||||||
|
|
||||||
// Video (with or without sync) mode: grant/staged handshake.
|
// Video (with or without sync) mode: grant/staged handshake.
|
||||||
for {
|
for {
|
||||||
|
params := <-control
|
||||||
|
if params.video != "" || params.audio != "" {
|
||||||
|
// Reconnect request from Connect button or auto-retry.
|
||||||
select {
|
select {
|
||||||
case <-grant:
|
case <-control: // drain any pending grant
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
if rerr := reopen(params); rerr != nil {
|
||||||
|
if errors.Is(rerr, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("source: reopen failed: %v, retrying", rerr)
|
||||||
|
select {
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
case control <- params:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
var payload []byte
|
var payload []byte
|
||||||
var grainIdx uint64
|
var grainIdx uint64
|
||||||
@@ -444,24 +534,19 @@ func main() {
|
|||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, mxl.ErrFlowInvalid) {
|
log.Printf("source: %v", err)
|
||||||
log.Printf("source: flow invalid, reopening")
|
// Drain any pending grant, then send reconnect.
|
||||||
if rerr := reopen(); rerr != nil {
|
select {
|
||||||
log.Printf("source: reopen failed: %v", rerr)
|
case <-control:
|
||||||
cancel()
|
default:
|
||||||
return
|
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case failed <- struct{}{}:
|
case control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("source: %v", err)
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
payload = vFrame.Payload
|
payload = vFrame.Payload
|
||||||
grainIdx = vFrame.Index
|
grainIdx = vFrame.Index
|
||||||
if aFrame.Samples != nil && audioStream != 0 {
|
if aFrame.Samples != nil && audioStream != 0 {
|
||||||
@@ -474,27 +559,20 @@ func main() {
|
|||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.Is(err, mxl.ErrFlowInvalid) {
|
log.Printf("source: %v", err)
|
||||||
log.Printf("source: flow invalid, reopening")
|
select {
|
||||||
if rerr := reopen(); rerr != nil {
|
case <-control:
|
||||||
log.Printf("source: reopen failed: %v", rerr)
|
default:
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case failed <- struct{}{}:
|
case control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("source: %v", err)
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
payload = f.Payload
|
payload = f.Payload
|
||||||
grainIdx = f.Index
|
grainIdx = f.Index
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if r != nil {
|
if r != nil {
|
||||||
@@ -516,11 +594,15 @@ func main() {
|
|||||||
sdl.SetWindowFullscreen(windowHandler, true)
|
sdl.SetWindowFullscreen(windowHandler, true)
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
|
fps float64
|
||||||
lastIndex uint64
|
lastIndex uint64
|
||||||
dropped uint64
|
dropped uint64
|
||||||
frameCount uint64
|
frameCount uint64
|
||||||
lastReport time.Time
|
lastReport time.Time
|
||||||
|
lastFrame time.Time
|
||||||
)
|
)
|
||||||
|
lastFrame = time.Now()
|
||||||
|
|
||||||
for running {
|
for running {
|
||||||
frameStart := time.Now()
|
frameStart := time.Now()
|
||||||
var event [128]byte
|
var event [128]byte
|
||||||
@@ -532,16 +614,24 @@ func main() {
|
|||||||
case sdl.EventWindowResized, sdl.EventPixelSizeChanged:
|
case sdl.EventWindowResized, sdl.EventPixelSizeChanged:
|
||||||
resized = true
|
resized = true
|
||||||
case sdl.EventKeyDown:
|
case sdl.EventKeyDown:
|
||||||
|
if gui.IO().WantCaptureKeyboard() {
|
||||||
|
break
|
||||||
|
}
|
||||||
key := *(*int32)(unsafe.Pointer(&event[28]))
|
key := *(*int32)(unsafe.Pointer(&event[28]))
|
||||||
switch uint32(key) {
|
switch uint32(key) {
|
||||||
|
case sdl.KeyQ:
|
||||||
|
fallthrough
|
||||||
case sdl.KeyEscape:
|
case sdl.KeyEscape:
|
||||||
running = false
|
running = false
|
||||||
case sdl.KeyF:
|
case sdl.KeyF:
|
||||||
fullscreen = !fullscreen
|
fullscreen = !fullscreen
|
||||||
sdl.SetWindowFullscreen(windowHandler, fullscreen)
|
sdl.SetWindowFullscreen(windowHandler, fullscreen)
|
||||||
resized = true
|
resized = true
|
||||||
|
case sdl.KeyF1:
|
||||||
|
showStats = !showStats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
gui.ProcessEvent(&event)
|
||||||
}
|
}
|
||||||
if !running {
|
if !running {
|
||||||
break
|
break
|
||||||
@@ -558,7 +648,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
if !granted {
|
if !granted {
|
||||||
select {
|
select {
|
||||||
case grant <- struct{}{}:
|
case control <- reconnectParams{}:
|
||||||
granted = true
|
granted = true
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
running = false
|
running = false
|
||||||
@@ -566,20 +656,77 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var shownIndex uint64
|
var shownIndex uint64
|
||||||
|
hasFrame := false
|
||||||
select {
|
select {
|
||||||
case shownIndex = <-staged:
|
case shownIndex = <-staged:
|
||||||
granted = false
|
granted = false
|
||||||
case <-failed:
|
hasFrame = true
|
||||||
granted = false
|
|
||||||
continue
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
running = false
|
running = false
|
||||||
continue
|
continue
|
||||||
case <-time.After(100 * time.Millisecond):
|
case <-time.After(100 * time.Millisecond):
|
||||||
continue
|
// No frame staged. Reset granted so we re-grant on next iteration.
|
||||||
|
granted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stats
|
||||||
|
if hasFrame {
|
||||||
|
if lastIndex != 0 && shownIndex > lastIndex {
|
||||||
|
if g := shownIndex - lastIndex - 1; g > 0 {
|
||||||
|
dropped += g
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastIndex = shownIndex
|
||||||
|
frameCount++
|
||||||
|
if now := time.Now(); now.Sub(lastReport) >= time.Second {
|
||||||
|
dt := now.Sub(lastReport).Seconds()
|
||||||
|
fps = float64(frameCount) / dt
|
||||||
|
fmt.Printf("fps=%.1f dropped=%d idx=%d frameTime=%.2fms\n",
|
||||||
|
fps, dropped, shownIndex, float64(now.Sub(frameStart).Microseconds())/1000.0)
|
||||||
|
frameCount = 0
|
||||||
|
dropped = 0
|
||||||
|
lastReport = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end of stats
|
||||||
if r != nil {
|
if r != nil {
|
||||||
|
gui.BeginFrame(time.Since(lastFrame), int32(r.Extent().Width), int32(r.Extent().Height))
|
||||||
|
// test widget
|
||||||
|
// cimgui.Begin("Test")
|
||||||
|
if showStats {
|
||||||
|
cimgui.SetNextWindowPos(cimgui.Vec2{X: 10, Y: 10})
|
||||||
|
cimgui.SetNextWindowSize(cimgui.Vec2{X: 200, Y: 200})
|
||||||
|
cimgui.BeginV("Stats", &showStats,
|
||||||
|
cimgui.WindowFlagsNoTitleBar|cimgui.WindowFlagsNoResize|cimgui.WindowFlagsNoScrollbar)
|
||||||
|
cimgui.Text(fmt.Sprintf("FPS: %.1f", fps))
|
||||||
|
cimgui.Text(fmt.Sprintf("Dropped: %d", dropped))
|
||||||
|
cimgui.Text(fmt.Sprintf("Index: %d", shownIndex))
|
||||||
|
if videoSrc != nil {
|
||||||
|
cimgui.Text(fmt.Sprintf("Video: %dx%d %.2fp", videoSrc.Width(), videoSrc.Height(),
|
||||||
|
float32(videoSrc.Rate().Num/videoSrc.Rate().Den)))
|
||||||
|
}
|
||||||
|
if syncSrc != nil {
|
||||||
|
cimgui.Text(fmt.Sprintf("Video: %dx%d %.2fp", syncSrc.Width(), syncSrc.Height(),
|
||||||
|
float32(syncSrc.Rate().Num/syncSrc.Rate().Den)))
|
||||||
|
cimgui.Text(fmt.Sprintf("Audio: %dch %dkHz", syncSrc.Channels(), syncSrc.AudioRate().Num))
|
||||||
|
}
|
||||||
|
cimgui.Text("\nPress F1 to hide stats")
|
||||||
|
cimgui.Text("Q or Esc to quit")
|
||||||
|
cimgui.Text("F for fullscreen")
|
||||||
|
cimgui.End()
|
||||||
|
}
|
||||||
|
cimgui.Begin("Connection")
|
||||||
|
cimgui.InputTextWithHint("Domain", "/dev/shm/mxl", &domainStr, 0, nil)
|
||||||
|
cimgui.InputTextWithHint("Video UUID", "", &videoStr, 0, nil)
|
||||||
|
cimgui.InputTextWithHint("Audio UUID", "", &audioStr, 0, nil)
|
||||||
|
cimgui.Checkbox("Show stats", &showStats)
|
||||||
|
if cimgui.Button("Connect") {
|
||||||
|
doReconnect()
|
||||||
|
}
|
||||||
|
cimgui.End()
|
||||||
|
gui.EndFrame()
|
||||||
|
lastFrame = time.Now()
|
||||||
|
// end of test widget
|
||||||
var w, h, stride uint32
|
var w, h, stride uint32
|
||||||
if syncSrc != nil {
|
if syncSrc != nil {
|
||||||
w, h, stride = syncSrc.Width(), syncSrc.Height(), syncSrc.Stride()
|
w, h, stride = syncSrc.Width(), syncSrc.Height(), syncSrc.Stride()
|
||||||
@@ -601,22 +748,6 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if lastIndex != 0 && shownIndex > lastIndex {
|
|
||||||
if g := shownIndex - lastIndex - 1; g > 0 {
|
|
||||||
dropped += g
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastIndex = shownIndex
|
|
||||||
frameCount++
|
|
||||||
if now := time.Now(); now.Sub(lastReport) >= time.Second {
|
|
||||||
dt := now.Sub(lastReport).Seconds()
|
|
||||||
fps := float64(frameCount) / dt
|
|
||||||
fmt.Printf("fps=%.1f dropped=%d idx=%d frameTime=%.2fms\n",
|
|
||||||
fps, dropped, shownIndex, float64(now.Sub(frameStart).Microseconds())/1000.0)
|
|
||||||
frameCount = 0
|
|
||||||
dropped = 0
|
|
||||||
lastReport = now
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
|
# Useful links
|
||||||
|
https://pthom.github.io/imgui_explorer/
|
||||||
|
|
||||||
# List of bugs, that need to be fixed
|
# List of bugs, that need to be fixed
|
||||||
|
|
||||||
## Major
|
## Major
|
||||||
|
- check how it looks like with more than 2 audio channels
|
||||||
|
|
||||||
## Minor
|
## Minor
|
||||||
- hotkeys on keyDown, especially toggle keys. Way to recreate: press F and hold
|
- hotkeys on keyDown, especially toggle keys. Way to recreate: press F and hold
|
||||||
|
|
||||||
## TODO:
|
## TODO:
|
||||||
|
- some sort of playlist with id's
|
||||||
- CLI option to run fullscreen
|
- CLI option to run fullscreen
|
||||||
- fabrics bridge reader. Step by step. Start with local
|
- fabrics bridge reader. Step by step. Start with local
|
||||||
- basic UI: stats, fields for domain, flow ids, label, etc.
|
- basic UI: stats, fields for domain, flow ids, label, etc.
|
||||||
- snapshot
|
- snapshot
|
||||||
- waveform, vectorscope
|
- waveform, vectorscope
|
||||||
- some image, when audio only
|
- some image, when audio only
|
||||||
- q for quit
|
|
||||||
|
## Done
|
||||||
|
- [x] resize broken again
|
||||||
|
- [x] q for quit
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ require (
|
|||||||
require github.com/qvest-digital/go-mxl v1.1.0-rc.1
|
require github.com/qvest-digital/go-mxl v1.1.0-rc.1
|
||||||
|
|
||||||
require github.com/spf13/pflag v1.0.10
|
require github.com/spf13/pflag v1.0.10
|
||||||
|
|
||||||
|
require github.com/AllenDang/cimgui-go v1.5.0
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/AllenDang/cimgui-go v1.5.0 h1:wnS4h1wWyE+3x59cHJnNSszka7pIR+pehim6ifOk9Rg=
|
||||||
|
github.com/AllenDang/cimgui-go v1.5.0/go.mod h1:w7LE33Ex/ZOHqaFBWBHOIVMY4yLSwGLpJMUpX/fb5p4=
|
||||||
github.com/christerso/vulkan-go v0.0.0-20260618152204-bff25e5b7646 h1:xfc6rFxs+U4XktxQa7jyPfg9g0xlb5STDsQn8sURsyo=
|
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/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 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# M9 — GUI (Dear ImGui)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The player already works (M0–M8): SDL3 window (purego), Vulkan renderer
|
||||||
|
(`internal/renderer`, `christerso/vulkan-go/vk` wrapper), MXL source
|
||||||
|
(`internal/source`). All rendering happens inside one render pass, one
|
||||||
|
command buffer, single-flight (fence-gated).
|
||||||
|
|
||||||
|
Dear ImGui is a header-only C++ library. The Go binding `cimgui-go`
|
||||||
|
compiles its core at build time via cgo. cimgui-go ships SDL2 + Vulkan
|
||||||
|
backends in C++, but we use SDL3 via purego, so we write custom Go
|
||||||
|
backends that bridge to our existing SDL3 and Vulkan code. ImGui's core
|
||||||
|
(no backend) has no SDL/Vulkan dependency — it just produces draw data
|
||||||
|
(vertices, indices, commands). Our backends feed it input and render its
|
||||||
|
draw data.
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
|
||||||
|
```
|
||||||
|
cmd/mxl-player/main.go
|
||||||
|
|
|
||||||
|
+-- internal/imgui/ new package
|
||||||
|
| imgui.go context, NewFrame/EndFrame, draw data
|
||||||
|
| input_sdl3.go SDL3 events -> ImGui IO
|
||||||
|
| backend_vulkan.go Vulkan pipeline + font atlas + draw
|
||||||
|
|
|
||||||
|
+-- internal/renderer/ existing, unchanged
|
||||||
|
+-- internal/sdl/ existing, add a few input helpers
|
||||||
|
+-- internal/source/ existing, unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
The ImGui Vulkan backend renders *inside* the existing render pass,
|
||||||
|
after the video `Draw(3)`, before `EndRenderPass`. It has its own
|
||||||
|
pipeline, vertex/index buffers, font texture, and descriptor set —
|
||||||
|
all owned by `internal/imgui`, not `internal/renderer`.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### M9.1 — ImGui core init + SDL3 input backend
|
||||||
|
|
||||||
|
Goal: ImGui context exists, receives SDL3 input, produces draw data
|
||||||
|
(not yet rendered).
|
||||||
|
|
||||||
|
1. `go get github.com/AllenDang/cimgui-go@v1.5.0`
|
||||||
|
2. Create `internal/imgui/imgui.go`:
|
||||||
|
- `type Context struct { ctx *imgui.Context; io *imgui.IO }`
|
||||||
|
- `func New() *Context` — creates context + IO, sets display size
|
||||||
|
from `sdl.GetWindowSizeInPixels`, sets delta time each frame.
|
||||||
|
- `func (c *Context) BeginFrame(dt time.Duration, winW, winH int32)`
|
||||||
|
— updates IO (display size, delta time), calls `imgui.NewFrame()`.
|
||||||
|
- `func (c *Context) EndFrame() *imgui.DrawData` — calls
|
||||||
|
`imgui.EndFrame()` / `imgui.Render()`, returns draw data for the
|
||||||
|
Vulkan backend to consume.
|
||||||
|
- `func (c *Context) Destroy()`
|
||||||
|
3. Create `internal/imgui/input_sdl3.go`:
|
||||||
|
- `func (c *Context) ProcessEvent(event *[128]byte)` — called from
|
||||||
|
the main loop for every SDL3 event, mutates `c.io`:
|
||||||
|
- `SDL_EVENT_KEY_DOWN` / `SDL_EVENT_KEY_UP` → set key in
|
||||||
|
`io.AddInputCharactersUTF8` for text, set key index.
|
||||||
|
- `SDL_EVENT_MOUSE_BUTTON_DOWN` / `UP` → set mouse button.
|
||||||
|
- `SDL_EVENT_MOUSE_MOTION` → set mouse position.
|
||||||
|
- `SDL_EVENT_MOUSE_WHEEL` → set wheel delta.
|
||||||
|
- `SDL_EVENT_WINDOW_RESIZED` → set display size.
|
||||||
|
- Key mapping: SDL3 scancode → ImGui key enum (a static lookup table
|
||||||
|
or switch).
|
||||||
|
- Mouse: ImGui wants float coordinates; SDL3 gives int32.
|
||||||
|
4. Wire into `main.go`:
|
||||||
|
- After window creation: `imguiCtx := imgui.New()`
|
||||||
|
- `defer imguiCtx.Destroy()`
|
||||||
|
- In the event poll loop: `imguiCtx.ProcessEvent(&event)`
|
||||||
|
- Before any widget code: `imguiCtx.BeginFrame(dt, winW, winH)`
|
||||||
|
5. SDL3 text input: call `SDL_StartTextInput` on window creation so
|
||||||
|
`SDL_EVENT_TEXT_INPUT` events fire (add to `internal/sdl`).
|
||||||
|
- SDL3 text input event: `SDL_EVENT_TEXT_INPUT = 0x303`, data is a
|
||||||
|
UTF-8 string at a fixed offset in the event struct.
|
||||||
|
|
||||||
|
Verify: app runs, no crash, window shows video as before. ImGui is
|
||||||
|
initialized but invisible (no widgets yet, no render backend).
|
||||||
|
|
||||||
|
### M9.2 — ImGui Vulkan render backend
|
||||||
|
|
||||||
|
Goal: ImGui draw data appears on screen inside the existing render
|
||||||
|
pass.
|
||||||
|
|
||||||
|
1. Create `internal/imgui/backend_vulkan.go`:
|
||||||
|
- `type VulkanBackend struct { ... }`
|
||||||
|
- Holds: pipeline, pipeline layout, descriptor set layout/pool/set,
|
||||||
|
font image + view + sampler, vertex buffer, index buffer (all
|
||||||
|
`vk.*` types from the wrapper).
|
||||||
|
2. `func NewVulkanBackend(pd vk.PhysicalDevice, dev vk.Device, queue
|
||||||
|
vk.Queue, cmdPool vk.CommandPool, rp vk.RenderPass, format
|
||||||
|
vk.Format) (*VulkanBackend, error)`:
|
||||||
|
- **Font atlas**: `imgui.GetIO().Fonts.Build()` then
|
||||||
|
`GetTexDataAsRGBA32(&pixels, &w, &h)` → create
|
||||||
|
`vk.CreateImage2D(w, h, Sampled|TransferDst)` →
|
||||||
|
`vk.CreateImageView` → upload pixels via staging buffer +
|
||||||
|
`CopyBufferToImage` → `vk.CreateSampler`.
|
||||||
|
- **Descriptor set**: layout with one
|
||||||
|
`DescriptorCombinedImageSampler` binding (binding 0, fragment
|
||||||
|
stage) → pool → set →
|
||||||
|
`vk.UpdateImageDescriptor(set, 0, fontView, fontSampler)`.
|
||||||
|
- **Pipeline layout**: one set layout, push constants
|
||||||
|
(ImGui's `ImDrawVert`-based push constant, 16 bytes for the
|
||||||
|
scale/translate vec2s, vertex+fragment stages).
|
||||||
|
- **Graphics pipeline**: `vk.CreateGraphicsPipeline` with
|
||||||
|
`Blend: true`, vertex shader + fragment shader (embed ImGui's
|
||||||
|
SPIR-V or compile from `imgui_impl_vulkan`'s GLSL). Vertex
|
||||||
|
attributes: position (vec2), UV (vec2), color (vec4) — matches
|
||||||
|
`ImDrawVert` layout.
|
||||||
|
- **Vertex/index buffers**: created with a max size (e.g. 1 MB
|
||||||
|
vertex, 256 KB index); recreated (larger) if draw data exceeds
|
||||||
|
capacity.
|
||||||
|
3. Embed ImGui shaders:
|
||||||
|
- Compile `imgui/shaders/imgui.vert` and `imgui/shaders/imgui.frag`
|
||||||
|
(from Dear ImGui's repo) to SPIR-V with `glslc`.
|
||||||
|
- `//go:embed` in `backend_vulkan.go`.
|
||||||
|
4. `func (b *VulkanBackend) RecordDraw(cmd vk.CommandBuffer, drawData
|
||||||
|
*imgui.DrawData, frameIndex uint32)`:
|
||||||
|
- Called inside the existing render pass (between video `Draw(3)`
|
||||||
|
and `EndRenderPass`).
|
||||||
|
- Map/`CopyToMapped` vertex + index data from `drawData` into the
|
||||||
|
staging buffers (or use `vk.Map`/`Unmap` on a host-visible
|
||||||
|
buffer).
|
||||||
|
- `cmd.BindPipeline(b.pipeline)`
|
||||||
|
- `cmd.BindDescriptorSet(b.layout, 0, b.set)`
|
||||||
|
- `cmd.BindVertexBuffers(...)`
|
||||||
|
- `cmd.BindIndexBuffer(...)`
|
||||||
|
- `cmd.SetViewport(...)` (full window)
|
||||||
|
- Push constants (scale/translate from drawData).
|
||||||
|
- For each draw list, for each command: `cmd.DrawIndexed(...)`.
|
||||||
|
5. Wire into `main.go` (or `renderer.DrawFrame`):
|
||||||
|
- After `cmd.Draw(3, 1, 0, 0)` (video), before `EndRenderPass`:
|
||||||
|
`imguiBackend.RecordDraw(cmd, drawData, imageIndex)`.
|
||||||
|
|
||||||
|
Verify: add a test widget `imgui.Text("hello")` in `BeginFrame`/
|
||||||
|
`EndFrame`. Run — you should see "hello" overlaid on the video.
|
||||||
|
|
||||||
|
### M9.3 — The GUI: config panel + stats overlay
|
||||||
|
|
||||||
|
Goal: usable GUI for entering connection params and showing stats.
|
||||||
|
|
||||||
|
1. In `main.go`, between `imguiCtx.BeginFrame(...)` and
|
||||||
|
`imguiCtx.EndFrame()`, build the GUI:
|
||||||
|
- **Config panel** (window, shown by default):
|
||||||
|
```go
|
||||||
|
imgui.Begin("Connection")
|
||||||
|
imgui.InputText("Domain", &domainBuf, 256)
|
||||||
|
imgui.InputText("Video UUID", &videoBuf, 256)
|
||||||
|
imgui.InputText("Audio UUID", &audioBuf, 256)
|
||||||
|
if imgui.Button("Connect") {
|
||||||
|
// trigger source.Open / OpenSync with the entered values
|
||||||
|
}
|
||||||
|
imgui.End()
|
||||||
|
```
|
||||||
|
- **Stats overlay** (window, toggled by F1, no title bar, no
|
||||||
|
background, top-left):
|
||||||
|
```go
|
||||||
|
if showStats {
|
||||||
|
imgui.SetNextWindowPos(...)
|
||||||
|
imgui.Begin("Stats", &showStats, imgui.WindowFlagsNoTitleBar | imgui.WindowFlagsNoBackground)
|
||||||
|
imgui.Text(fmt.Sprintf("FPS: %.1f", fps))
|
||||||
|
imgui.Text(fmt.Sprintf("Dropped: %d", dropped))
|
||||||
|
imgui.Text(fmt.Sprintf("Index: %d", shownIndex))
|
||||||
|
imgui.Text(fmt.Sprintf("Frame: %dx%d", videoW, videoH))
|
||||||
|
imgui.Text(fmt.Sprintf("Format: v210 10-bit"))
|
||||||
|
imgui.End()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- **Dummy controls** for future M10 flow discovery:
|
||||||
|
```go
|
||||||
|
imgui.Button("List Flows") // no-op yet
|
||||||
|
imgui.Button("Refresh") // no-op yet
|
||||||
|
```
|
||||||
|
2. State: `domainBuf`, `videoBuf`, `audioBuf` are `[256]byte` buffers
|
||||||
|
(ImGui's `InputText` needs a fixed buffer + capacity). Convert to
|
||||||
|
Go string on "Connect".
|
||||||
|
3. "Connect" button:
|
||||||
|
- Close existing source if any.
|
||||||
|
- Call `source.Open` / `source.OpenSync` with the buffer values.
|
||||||
|
- On error, display `imgui.Text` in red below the button.
|
||||||
|
4. F1 toggle: handle in the keydown event switch, flip `showStats`.
|
||||||
|
|
||||||
|
Verify: type a domain + UUID, click Connect, video appears. Toggle
|
||||||
|
stats with F1. Resize window — GUI stays usable.
|
||||||
|
|
||||||
|
### M9.4 — Wire GUI to engine + polish
|
||||||
|
|
||||||
|
Goal: GUI controls the engine, not just displays.
|
||||||
|
|
||||||
|
1. **Connection lifecycle**: "Connect" button triggers source open +
|
||||||
|
renderer buffer creation (if resolution changed). "Disconnect"
|
||||||
|
button closes source, video freezes on last frame (or clears to
|
||||||
|
black).
|
||||||
|
2. **Stats read from engine**: expose `Stats` struct from the loop
|
||||||
|
(fps, dropped, index, frameTime, resolution, format). GUI reads it
|
||||||
|
each frame.
|
||||||
|
3. **Freeze control** (placeholder for M10): `imgui.Checkbox("Freeze",
|
||||||
|
&frozen)`. When frozen, stop calling `DrawFrame` (keep last image
|
||||||
|
on screen, keep polling events + GUI).
|
||||||
|
4. **Input focus**: when ImGui wants keyboard input (text field
|
||||||
|
focused), don't pass key events to the app (e.g., don't toggle
|
||||||
|
fullscreen on 'F' while typing). Check
|
||||||
|
`imgui.GetIO().WantCaptureKeyboard`.
|
||||||
|
5. **Mouse capture**: when ImGui wants mouse, don't let the app
|
||||||
|
process mouse events. Check `imgui.GetIO().WantCaptureMouse`.
|
||||||
|
6. **DPI awareness**: scale ImGui font + style by the window's
|
||||||
|
`SDL_GetDisplayContentScale` (or hardcode 1.0 for now; polish
|
||||||
|
later).
|
||||||
|
|
||||||
|
Verify: full workflow — launch app, enter params, connect, see video +
|
||||||
|
stats, toggle stats, disconnect, reconnect. All via GUI, no CLI flags
|
||||||
|
needed (though flags still work for headless/automation).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- ImGui's vertex layout (`ImDrawVert`): `pos [2]float32, uv [2]float32,
|
||||||
|
col uint32` = 20 bytes. The vertex shader applies a scale/translate
|
||||||
|
push constant to convert from ImGui's screen coordinates to clip
|
||||||
|
space.
|
||||||
|
- ImGui's fragment shader samples the font texture (and any user
|
||||||
|
textures) using the UV from the vertex. Color is the vertex color,
|
||||||
|
multiplied by the texture sample.
|
||||||
|
- The backend pipeline uses alpha blending:
|
||||||
|
`src=SRC_ALPHA, dst=ONE_MINUS_SRC_ALPHA, op=ADD`.
|
||||||
|
- ImGui produces draw data *after* `EndFrame`/`Render`. The flow is:
|
||||||
|
`BeginFrame` → build widgets → `EndFrame`/`Render` → get draw data →
|
||||||
|
record Vulkan commands from draw data → submit.
|
||||||
|
- Font atlas upload is one-time, during `NewVulkanBackend`. Vertex/index
|
||||||
|
buffers are updated every frame from ImGui's draw data (host-visible,
|
||||||
|
persistently mapped).
|
||||||
|
- The ImGui pipeline is separate from the decode pipeline. Both render
|
||||||
|
into the same render pass / framebuffer / color attachment.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[Window][Debug##Default]
|
||||||
|
Pos=60,60
|
||||||
|
Size=400,400
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Test]
|
||||||
|
Pos=60,60
|
||||||
|
Size=251,92
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Stats]
|
||||||
|
Pos=10,10
|
||||||
|
Size=200,200
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Connection]
|
||||||
|
Pos=425,351
|
||||||
|
Size=523,153
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
package imgui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
cimgui "github.com/AllenDang/cimgui-go/imgui"
|
||||||
|
"github.com/christerso/vulkan-go/vk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Renders ImGui draw data inside an existing Vulkan render pass
|
||||||
|
type VulkanBackend struct {
|
||||||
|
pd vk.PhysicalDevice
|
||||||
|
dev vk.Device
|
||||||
|
queue vk.Queue
|
||||||
|
cmdPool vk.CommandPool
|
||||||
|
|
||||||
|
pipeline vk.Pipeline
|
||||||
|
pipelineLayout vk.PipelineLayout
|
||||||
|
descSetLayout vk.DescriptorSetLayout
|
||||||
|
descPool vk.DescriptorPool
|
||||||
|
descSet vk.DescriptorSet
|
||||||
|
vertMod vk.ShaderModule
|
||||||
|
fragMod vk.ShaderModule
|
||||||
|
|
||||||
|
fontImg vk.AllocImage
|
||||||
|
fontView vk.ImageView
|
||||||
|
fontSampler vk.Sampler
|
||||||
|
|
||||||
|
vertBuf vk.AllocBuffer
|
||||||
|
idxBuf vk.AllocBuffer
|
||||||
|
vertSize vk.DeviceSize
|
||||||
|
idxSize vk.DeviceSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVulkanBackend creates the ImGui pipeline, font atlas, and buffers.
|
||||||
|
// rp is the existing render pass; format is the swapchain color format.
|
||||||
|
func NewVulkanBackend(pd vk.PhysicalDevice, dev vk.Device, queue vk.Queue, cmdPool vk.CommandPool, rp vk.RenderPass) (*VulkanBackend, error) {
|
||||||
|
b := &VulkanBackend{pd: pd, dev: dev, queue: queue, cmdPool: cmdPool}
|
||||||
|
|
||||||
|
// 1. Font atlas — build and upload via CreateTexture2D (handles staging).
|
||||||
|
io := cimgui.CurrentIO()
|
||||||
|
fontAtlas := io.Fonts()
|
||||||
|
cimgui.InternalImFontAtlasBuildMain(fontAtlas)
|
||||||
|
texData := fontAtlas.TexData()
|
||||||
|
w, h := texData.Width(), texData.Height()
|
||||||
|
pixelCount := int(w) * int(h) * 4 // RGBA32 = 4 bytes/pixel
|
||||||
|
pixels := unsafe.Slice((*byte)(unsafe.Pointer(texData.Pixels())), pixelCount)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
b.fontImg, b.fontView, err = dev.CreateTexture2D(pd, queue, cmdPool, uint32(w), uint32(h), pixels)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("imgui font atlas: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font sampler: linear, clamp to edge.
|
||||||
|
b.fontSampler, err = dev.CreateSampler(vk.SamplerConfig{
|
||||||
|
MagFilter: vk.FilterLinear,
|
||||||
|
MinFilter: vk.FilterLinear,
|
||||||
|
AddressModeU: vk.SamplerAddressModeClampToEdge,
|
||||||
|
AddressModeV: vk.SamplerAddressModeClampToEdge,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Shaders.
|
||||||
|
b.vertMod, err = dev.CreateShaderModule(vertSPV)
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.fragMod, err = dev.CreateShaderModule(fragSPV)
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Descriptor set: one combined image sampler (font), fragment stage.
|
||||||
|
b.descSetLayout, err = dev.CreateDescriptorSetLayout([]vk.DescriptorBinding{
|
||||||
|
{Binding: 0, Type: vk.DescriptorCombinedImageSampler, Count: 1, Stages: vk.ShaderStageFragment},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.descPool, err = dev.CreateDescriptorPool(1, map[vk.DescriptorType]uint32{
|
||||||
|
vk.DescriptorCombinedImageSampler: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.descSet, err = dev.AllocateDescriptorSet(b.descPool, b.descSetLayout)
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dev.UpdateImageDescriptor(b.descSet, 0, b.fontView, b.fontSampler)
|
||||||
|
|
||||||
|
// 4. Pipeline layout: one set + 16 bytes push constants (vertex+fragment).
|
||||||
|
b.pipelineLayout, err = dev.CreatePipelineLayout(
|
||||||
|
[]vk.DescriptorSetLayout{b.descSetLayout},
|
||||||
|
vk.ShaderStageVertex|vk.ShaderStageFragment, 16,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Graphics pipeline: vertex input (pos+uv+col = 20 bytes), alpha blend.
|
||||||
|
b.pipeline, err = dev.CreateGraphicsPipeline(vk.GraphicsPipelineConfig{
|
||||||
|
Layout: b.pipelineLayout,
|
||||||
|
RenderPass: rp,
|
||||||
|
VertexShader: b.vertMod,
|
||||||
|
FragShader: b.fragMod,
|
||||||
|
Bindings: []vk.VertexInputBinding{
|
||||||
|
{Binding: 0, Stride: 20, InputRate: vk.VertexInputRateVertex},
|
||||||
|
},
|
||||||
|
Attributes: []vk.VertexInputAttribute{
|
||||||
|
{Location: 0, Binding: 0, Format: vk.Format(103), Offset: 0}, // pos: R32G32Sfloat
|
||||||
|
{Location: 1, Binding: 0, Format: vk.Format(103), Offset: 8}, // uv: R32G32Sfloat
|
||||||
|
{Location: 2, Binding: 0, Format: vk.FormatR8G8B8A8Unorm, Offset: 16}, // col: R8G8B8A8Unorm
|
||||||
|
},
|
||||||
|
Topology: vk.TopologyTriangleList,
|
||||||
|
PolygonMode: vk.PolygonFill,
|
||||||
|
CullMode: vk.CullNone,
|
||||||
|
FrontFace: vk.FrontFaceCounterClockwise,
|
||||||
|
Blend: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Vertex/index buffers: host-visible, mapped. Start at 64KB/16KB, grow if needed.
|
||||||
|
b.vertSize = 1 << 16
|
||||||
|
b.idxSize = 1 << 14
|
||||||
|
b.vertBuf, err = dev.CreateBuffer(pd, vk.BufferConfig{
|
||||||
|
Size: b.vertSize,
|
||||||
|
Usage: vk.BufferUsageVertexBuffer,
|
||||||
|
Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent,
|
||||||
|
Map: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b.idxBuf, err = dev.CreateBuffer(pd, vk.BufferConfig{
|
||||||
|
Size: vk.DeviceSize(b.idxSize),
|
||||||
|
Usage: vk.BufferUsageIndexBuffer,
|
||||||
|
Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent,
|
||||||
|
Map: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
b.destroyAll()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordDraw records ImGui draw commands into the command buffer.
|
||||||
|
// Must be called inside the render pass, after your scene draw, before EndRenderPass.
|
||||||
|
func (b *VulkanBackend) RecordDraw(cmd vk.CommandBuffer, drawData *cimgui.DrawData) {
|
||||||
|
if !drawData.Valid() || drawData.CmdListsCount() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
totalVtx := int(drawData.TotalVtxCount())
|
||||||
|
totalIdx := int(drawData.TotalIdxCount())
|
||||||
|
vertBytes := totalVtx * 20
|
||||||
|
idxBytes := totalIdx * 2
|
||||||
|
|
||||||
|
// Grow vertex buffer if needed.
|
||||||
|
if vk.DeviceSize(vertBytes) > b.vertSize {
|
||||||
|
b.dev.DestroyBuffer(b.vertBuf)
|
||||||
|
b.vertSize = vk.DeviceSize(vertBytes) * 2
|
||||||
|
b.vertBuf, _ = b.dev.CreateBuffer(b.pd, vk.BufferConfig{
|
||||||
|
Size: b.vertSize,
|
||||||
|
Usage: vk.BufferUsageVertexBuffer,
|
||||||
|
Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent,
|
||||||
|
Map: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Grow index buffer if needed.
|
||||||
|
if vk.DeviceSize(idxBytes) > vk.DeviceSize(b.idxBuf.Size) {
|
||||||
|
b.dev.DestroyBuffer(b.idxBuf)
|
||||||
|
b.idxBuf, _ = b.dev.CreateBuffer(b.pd, vk.BufferConfig{
|
||||||
|
Size: vk.DeviceSize(idxBytes) * 2,
|
||||||
|
Usage: vk.BufferUsageIndexBuffer,
|
||||||
|
Properties: vk.MemoryHostVisible | vk.MemoryHostCoherent,
|
||||||
|
Map: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all vertex/index data into the mapped buffers.
|
||||||
|
vertOffset := 0
|
||||||
|
idxOffset := 0
|
||||||
|
cmdLists := drawData.CommandLists()
|
||||||
|
for _, list := range cmdLists {
|
||||||
|
// Vertices: GetVertexBuffer returns raw C pointer + byte size.
|
||||||
|
vtxPtr, vtxBytes2 := list.GetVertexBuffer()
|
||||||
|
if vtxBytes2 > 0 {
|
||||||
|
src := unsafe.Slice((*byte)(vtxPtr), vtxBytes2)
|
||||||
|
dst := unsafe.Slice((*byte)(b.vertBuf.Mapped), vertBytes)
|
||||||
|
copy(dst[vertOffset:], src)
|
||||||
|
vertOffset += vtxBytes2
|
||||||
|
}
|
||||||
|
// Indices: GetIndexBuffer returns raw C pointer + byte size.
|
||||||
|
idxPtr, idxBytes2 := list.GetIndexBuffer()
|
||||||
|
if idxBytes2 > 0 {
|
||||||
|
src := unsafe.Slice((*byte)(idxPtr), idxBytes2)
|
||||||
|
dst := unsafe.Slice((*byte)(b.idxBuf.Mapped), idxBytes)
|
||||||
|
copy(dst[idxOffset:], src)
|
||||||
|
idxOffset += idxBytes2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push constants: scale + translate (transforms ImGui pixels to clip space).
|
||||||
|
displaySize := drawData.DisplaySize()
|
||||||
|
displayPos := drawData.DisplayPos()
|
||||||
|
scale := [2]float32{2.0 / displaySize.X, 2.0 / displaySize.Y}
|
||||||
|
translate := [2]float32{
|
||||||
|
-1.0 - 2.0*displayPos.X/displaySize.X,
|
||||||
|
-1.0 - 2.0*displayPos.Y/displaySize.Y,
|
||||||
|
}
|
||||||
|
pc := struct {
|
||||||
|
Scale [2]float32
|
||||||
|
Translate [2]float32
|
||||||
|
}{
|
||||||
|
Scale: scale,
|
||||||
|
Translate: translate,
|
||||||
|
}
|
||||||
|
cmd.PushConstants(b.pipelineLayout, vk.ShaderStageVertex|vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 16)
|
||||||
|
|
||||||
|
// Bind pipeline + descriptor set + vertex/index buffers.
|
||||||
|
cmd.BindPipeline(b.pipeline)
|
||||||
|
cmd.BindDescriptorSet(b.pipelineLayout, 0, b.descSet)
|
||||||
|
offsets := []vk.DeviceSize{0}
|
||||||
|
cmd.BindVertexBuffers(0, []vk.Buffer{b.vertBuf.Buffer}, offsets)
|
||||||
|
cmd.BindIndexBuffer(b.idxBuf.Buffer, 0, vk.IndexTypeUint16)
|
||||||
|
|
||||||
|
// Draw each command list, translating clip rects to scissors.
|
||||||
|
vtxOff := uint32(0)
|
||||||
|
idxOff := uint32(0)
|
||||||
|
for _, list := range cmdLists {
|
||||||
|
cmds := list.Commands()
|
||||||
|
for _, dc := range cmds {
|
||||||
|
if dc.HasUserCallback() {
|
||||||
|
dc.CallUserCallback(list)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clip := dc.ClipRect()
|
||||||
|
sx := int32(clip.X - displayPos.X)
|
||||||
|
sy := int32(clip.Y - displayPos.Y)
|
||||||
|
ex := int32(clip.Z - displayPos.X)
|
||||||
|
ey := int32(clip.W - displayPos.Y)
|
||||||
|
if sx < 0 {
|
||||||
|
sx = 0
|
||||||
|
}
|
||||||
|
if sy < 0 {
|
||||||
|
sy = 0
|
||||||
|
}
|
||||||
|
if ex > int32(displaySize.X) {
|
||||||
|
ex = int32(displaySize.X)
|
||||||
|
}
|
||||||
|
if ey > int32(displaySize.Y) {
|
||||||
|
ey = int32(displaySize.Y)
|
||||||
|
}
|
||||||
|
if ex > sx && ey > sy {
|
||||||
|
cmd.SetScissor(vk.Rect2D{
|
||||||
|
Offset: vk.Offset2D{X: sx, Y: sy},
|
||||||
|
Extent: vk.Extent2D{Width: uint32(ex - sx), Height: uint32(ey - sy)},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cmd.DrawIndexed(dc.ElemCount(), 1, idxOff+dc.IdxOffset(), int32(vtxOff+dc.VtxOffset()), 0)
|
||||||
|
}
|
||||||
|
vtxOff += uint32(list.VtxBuffer().Size)
|
||||||
|
idxOff += uint32(list.IdxBuffer().Size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *VulkanBackend) destroyAll() {
|
||||||
|
b.dev.DestroyBuffer(b.idxBuf)
|
||||||
|
b.dev.DestroyBuffer(b.vertBuf)
|
||||||
|
b.dev.DestroyPipeline(b.pipeline)
|
||||||
|
b.dev.DestroyPipelineLayout(b.pipelineLayout)
|
||||||
|
b.dev.DestroyDescriptorPool(b.descPool)
|
||||||
|
b.dev.DestroyDescriptorSetLayout(b.descSetLayout)
|
||||||
|
b.dev.DestroyShaderModule(b.fragMod)
|
||||||
|
b.dev.DestroyShaderModule(b.vertMod)
|
||||||
|
b.dev.DestroySampler(b.fontSampler)
|
||||||
|
b.dev.DestroyImageView(b.fontView)
|
||||||
|
b.dev.DestroyImage(b.fontImg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *VulkanBackend) Destroy() {
|
||||||
|
b.destroyAll()
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package imgui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cimgui "github.com/AllenDang/cimgui-go/imgui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImGui context & IO wrapper
|
||||||
|
type Context struct {
|
||||||
|
ctx *cimgui.Context
|
||||||
|
io *cimgui.IO
|
||||||
|
lastDrawData *cimgui.DrawData
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Context {
|
||||||
|
ctx := cimgui.CreateContext()
|
||||||
|
cimgui.SetCurrentContext(ctx)
|
||||||
|
io := cimgui.CurrentIO()
|
||||||
|
return &Context{ctx: ctx, io: io}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Destroy() {
|
||||||
|
cimgui.DestroyContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts new ImGui frame
|
||||||
|
// dt - time since last frame
|
||||||
|
func (c *Context) BeginFrame(dt time.Duration, winW, winH int32) {
|
||||||
|
c.io.SetDeltaTime(float32(dt.Seconds()))
|
||||||
|
c.io.SetDisplaySize(cimgui.Vec2{X: float32(winW), Y: float32(winH)})
|
||||||
|
cimgui.NewFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ends the frame, renders
|
||||||
|
// and returns data for Vulkan
|
||||||
|
func (c *Context) EndFrame() *cimgui.DrawData {
|
||||||
|
cimgui.EndFrame()
|
||||||
|
cimgui.Render()
|
||||||
|
c.lastDrawData = cimgui.CurrentDrawData()
|
||||||
|
return cimgui.CurrentDrawData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// expose io to input backend
|
||||||
|
func (c *Context) IO() *cimgui.IO { return c.io }
|
||||||
|
|
||||||
|
func (c *Context) LastDrawData() *cimgui.DrawData { return c.lastDrawData }
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package imgui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"mxl-player/internal/sdl"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
cimgui "github.com/AllenDang/cimgui-go/imgui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SDL3 event (128 byte raw buffer) -> imgui
|
||||||
|
func (c *Context) ProcessEvent(event *[128]byte) {
|
||||||
|
eventType := *(*uint32)(unsafe.Pointer(&event[0]))
|
||||||
|
|
||||||
|
// CommonEvent (16 bytes: type uint32 + reserved uint32 + timestamp uint64)
|
||||||
|
switch eventType {
|
||||||
|
case sdl.EventKeyDown, sdl.EventKeyUp:
|
||||||
|
scancode := *(*uint32)(unsafe.Pointer(&event[24]))
|
||||||
|
down := eventType == sdl.EventKeyDown
|
||||||
|
key := sdlScancodeToImGuiKey(scancode)
|
||||||
|
if key >= 0 {
|
||||||
|
c.io.AddKeyEvent(key, down)
|
||||||
|
}
|
||||||
|
// MouseMotionEvent: +16 windowID(4), +20 which(4), +24 state(4), +28 x(float32), +32 y(float32)
|
||||||
|
case sdl.EventMouseMotion:
|
||||||
|
x := *(*float32)(unsafe.Pointer(&event[28]))
|
||||||
|
y := *(*float32)(unsafe.Pointer(&event[32]))
|
||||||
|
c.io.AddMousePosEvent(x, y)
|
||||||
|
// MouseButtonEvent: +16 windowID(4), +20 which(4), +24 button(1), +25 down(bool)
|
||||||
|
case sdl.EventMouseButtonDown, sdl.EventMouseButtonUp:
|
||||||
|
button := *(*uint8)(unsafe.Pointer(&event[24]))
|
||||||
|
down := eventType == sdl.EventMouseButtonDown
|
||||||
|
c.io.AddMouseButtonEvent(int32(button-1), down)
|
||||||
|
// MouseWheelEvent: +16 windowID(4), +20 which(4), +24 x(float32), +28 y(float32)
|
||||||
|
case sdl.EventMouseWheel:
|
||||||
|
x := *(*float32)(unsafe.Pointer(&event[24]))
|
||||||
|
y := *(*float32)(unsafe.Pointer(&event[28]))
|
||||||
|
c.io.AddMouseWheelEvent(x, y)
|
||||||
|
// TextInputEvent: +16 windowID(4), +20 text(*char pointer, 8 bytes on 64-bit)
|
||||||
|
case sdl.EventTextInput:
|
||||||
|
// text pointer is at offset 24, 8 bytes (pointer)
|
||||||
|
ptr := *(*uintptr)(unsafe.Pointer(&event[24]))
|
||||||
|
if ptr != 0 {
|
||||||
|
text := cstr(ptr)
|
||||||
|
c.io.AddInputCharactersUTF8(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sdlScancodeToImGuiKey(scancode uint32) cimgui.Key {
|
||||||
|
switch scancode {
|
||||||
|
case 40: // SDL_SCANCODE_RETURN
|
||||||
|
return cimgui.KeyEnter
|
||||||
|
case 42: // SDL_SCANCODE_BACKSPACE
|
||||||
|
return cimgui.KeyBackspace
|
||||||
|
case 41: // SDL_SCANCODE_ESCAPE
|
||||||
|
return cimgui.KeyEscape
|
||||||
|
case 43: // SDL_SCANCODE_TAB
|
||||||
|
return cimgui.KeyTab
|
||||||
|
case 44: // SDL_SCANCODE_SPACE
|
||||||
|
return cimgui.KeySpace
|
||||||
|
case 80: // SDL_SCANCODE_LEFT
|
||||||
|
return cimgui.KeyLeftArrow
|
||||||
|
case 79: // SDL_SCANCODE_RIGHT
|
||||||
|
return cimgui.KeyRightArrow
|
||||||
|
case 82: // SDL_SCANCODE_UP
|
||||||
|
return cimgui.KeyUpArrow
|
||||||
|
case 81: // SDL_SCANCODE_DOWN
|
||||||
|
return cimgui.KeyDownArrow
|
||||||
|
case 225: // SDL_SCANCODE_LSHIFT
|
||||||
|
return cimgui.KeyLeftShift
|
||||||
|
case 229: // SDL_SCANCODE_RSHIFT
|
||||||
|
return cimgui.KeyRightShift
|
||||||
|
case 224: // SDL_SCANCODE_LCTRL
|
||||||
|
return cimgui.KeyLeftCtrl
|
||||||
|
case 228: // SDL_SCANCODE_RCTRL
|
||||||
|
return cimgui.KeyRightCtrl
|
||||||
|
case 226: // SDL_SCANCODE_LALT
|
||||||
|
return cimgui.KeyLeftAlt
|
||||||
|
case 230: // SDL_SCANCODE_RALT
|
||||||
|
return cimgui.KeyRightAlt
|
||||||
|
default:
|
||||||
|
// Letters A-Z: scancodes 4-29 map to ImGui KeyA-KeyZ
|
||||||
|
if scancode >= 4 && scancode <= 29 {
|
||||||
|
return cimgui.Key(int(cimgui.KeyA) + int(scancode-4))
|
||||||
|
}
|
||||||
|
// Numbers 0-9: scancodes 30-39
|
||||||
|
if scancode >= 30 && scancode <= 39 {
|
||||||
|
return cimgui.Key(int(cimgui.Key0) + int(scancode-30))
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cstr reads a NUL-terminated C string at p.
|
||||||
|
// TODO: same func used in Vulkan
|
||||||
|
// Maybe i should create something like "package common"
|
||||||
|
// for funcs like this
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package imgui
|
||||||
|
|
||||||
|
import _ "embed"
|
||||||
|
|
||||||
|
//go:embed shaders/imgui.vert.spv
|
||||||
|
var vertSPV []byte
|
||||||
|
|
||||||
|
//go:embed shaders/imgui.frag.spv
|
||||||
|
var fragSPV []byte
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#version 450 core
|
||||||
|
layout(location = 0) out vec4 fColor;
|
||||||
|
|
||||||
|
layout(set=0, binding=0) uniform sampler2D sTexture;
|
||||||
|
|
||||||
|
layout(location = 0) in struct {
|
||||||
|
vec4 Color;
|
||||||
|
vec2 UV;
|
||||||
|
} In;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fColor = In.Color * texture(sTexture, In.UV.st);
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1,25 @@
|
|||||||
|
#version 450 core
|
||||||
|
layout(location = 0) in vec2 aPos;
|
||||||
|
layout(location = 1) in vec2 aUV;
|
||||||
|
layout(location = 2) in vec4 aColor;
|
||||||
|
|
||||||
|
layout(push_constant) uniform uPushConstant {
|
||||||
|
vec2 uScale;
|
||||||
|
vec2 uTranslate;
|
||||||
|
} pc;
|
||||||
|
|
||||||
|
out gl_PerVertex {
|
||||||
|
vec4 gl_Position;
|
||||||
|
};
|
||||||
|
|
||||||
|
layout(location = 0) out struct {
|
||||||
|
vec4 Color;
|
||||||
|
vec2 UV;
|
||||||
|
} Out;
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
Out.Color = aColor;
|
||||||
|
Out.UV = aUV;
|
||||||
|
gl_Position = vec4(aPos * pc.uScale + pc.uTranslate, 0, 1);
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -59,6 +59,9 @@ type Renderer struct {
|
|||||||
imageAvailable vk.Semaphore
|
imageAvailable vk.Semaphore
|
||||||
renderFinished vk.Semaphore
|
renderFinished vk.Semaphore
|
||||||
inFlight vk.Fence
|
inFlight vk.Fence
|
||||||
|
|
||||||
|
// Called inside render pass, after video draw
|
||||||
|
ImGuiDraw func(cmd vk.CommandBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// public params, needed to create a Renderer
|
// public params, needed to create a Renderer
|
||||||
@@ -441,6 +444,9 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
|
|||||||
}
|
}
|
||||||
cmd.PushConstants(r.decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 20)
|
cmd.PushConstants(r.decodeLayout, vk.ShaderStageFragment, 0, unsafe.Pointer(&pc), 20)
|
||||||
cmd.Draw(3, 1, 0, 0)
|
cmd.Draw(3, 1, 0, 0)
|
||||||
|
if r.ImGuiDraw != nil {
|
||||||
|
r.ImGuiDraw(cmd)
|
||||||
|
}
|
||||||
cmd.EndRenderPass()
|
cmd.EndRenderPass()
|
||||||
if err := cmd.End(); err != nil {
|
if err := cmd.End(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -465,3 +471,6 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) CmdPool() vk.CommandPool { return r.cmdPool }
|
||||||
|
func (r *Renderer) RenderPass() vk.RenderPass { return r.renderPass }
|
||||||
|
|||||||
+23
-1
@@ -16,10 +16,22 @@ const (
|
|||||||
EventQuit uint32 = 0x100
|
EventQuit uint32 = 0x100
|
||||||
EventWindowResized uint32 = 0x206
|
EventWindowResized uint32 = 0x206
|
||||||
EventPixelSizeChanged uint32 = 0x207
|
EventPixelSizeChanged uint32 = 0x207
|
||||||
EventKeyDown uint32 = 0x300
|
|
||||||
|
|
||||||
|
EventKeyDown uint32 = 0x300
|
||||||
|
EventKeyUp uint32 = 0x301
|
||||||
|
|
||||||
|
EventMouseMotion uint32 = 0x400
|
||||||
|
EventMouseButtonDown uint32 = 0x401
|
||||||
|
EventMouseButtonUp uint32 = 0x402
|
||||||
|
EventMouseWheel uint32 = 0x403
|
||||||
|
|
||||||
|
EventTextInput uint32 = 0x303
|
||||||
|
|
||||||
|
// it's about SDL_keycode, not SDL_scancode
|
||||||
KeyEscape uint32 = 0x1B
|
KeyEscape uint32 = 0x1B
|
||||||
KeyF uint32 = 0x66
|
KeyF uint32 = 0x66
|
||||||
|
KeyQ uint32 = 0x71
|
||||||
|
KeyF1 uint32 = 0x4000003A
|
||||||
|
|
||||||
InitAudio uint32 = 0x00000010
|
InitAudio uint32 = 0x00000010
|
||||||
AudioDeviceDefaultPlayback uint32 = 0xFFFFFFFF
|
AudioDeviceDefaultPlayback uint32 = 0xFFFFFFFF
|
||||||
@@ -49,6 +61,9 @@ var (
|
|||||||
sdlDestroyAudioStream func(stream uintptr)
|
sdlDestroyAudioStream func(stream uintptr)
|
||||||
sdlGetAudioPlaybackDevices func(count *int32) uintptr
|
sdlGetAudioPlaybackDevices func(count *int32) uintptr
|
||||||
sdlGetAudioDeviceName func(devid uint32) uintptr
|
sdlGetAudioDeviceName func(devid uint32) uintptr
|
||||||
|
|
||||||
|
sdlStartTextInput func(window uintptr)
|
||||||
|
sdlStopTextInput func(window uintptr)
|
||||||
)
|
)
|
||||||
|
|
||||||
var loaded = false
|
var loaded = false
|
||||||
@@ -79,6 +94,9 @@ func Load() error {
|
|||||||
purego.RegisterLibFunc(&sdlDestroyAudioStream, h, "SDL_DestroyAudioStream")
|
purego.RegisterLibFunc(&sdlDestroyAudioStream, h, "SDL_DestroyAudioStream")
|
||||||
purego.RegisterLibFunc(&sdlGetAudioPlaybackDevices, h, "SDL_GetAudioPlaybackDevices")
|
purego.RegisterLibFunc(&sdlGetAudioPlaybackDevices, h, "SDL_GetAudioPlaybackDevices")
|
||||||
purego.RegisterLibFunc(&sdlGetAudioDeviceName, h, "SDL_GetAudioDeviceName")
|
purego.RegisterLibFunc(&sdlGetAudioDeviceName, h, "SDL_GetAudioDeviceName")
|
||||||
|
// input
|
||||||
|
purego.RegisterLibFunc(&sdlStartTextInput, h, "SDL_StartTextInput")
|
||||||
|
purego.RegisterLibFunc(&sdlStopTextInput, h, "SDL_StopTextInput")
|
||||||
loaded = true
|
loaded = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -174,3 +192,7 @@ func GetAudioPlaybackDevices() []AudioDevice {
|
|||||||
}
|
}
|
||||||
return devs
|
return devs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Input wrappers
|
||||||
|
func StartTextInput(window uintptr) { sdlStartTextInput(window) }
|
||||||
|
func StopTextInput(window uintptr) { sdlStopTextInput(window) }
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ func (s *SyncSource) Close() error {
|
|||||||
|
|
||||||
// NextSync reads both at a synced timestamp. Returns video Frame + audio AudioFrame
|
// NextSync reads both at a synced timestamp. Returns video Frame + audio AudioFrame
|
||||||
func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout time.Duration) (Frame, AudioFrame, error) {
|
func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout time.Duration) (Frame, AudioFrame, error) {
|
||||||
|
var timeouts int
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -421,7 +422,19 @@ func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout ti
|
|||||||
s.idx = mxl.CurrentIndex(s.rate)
|
s.idx = mxl.CurrentIndex(s.rate)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return Frame{}, AudioFrame{}, fmt.Errorf("GetGraing: %w", gerr)
|
if errors.Is(gerr, mxl.ErrOutOfRangeLate) {
|
||||||
|
s.idx = mxl.CurrentIndex(s.rate)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if errors.Is(gerr, mxl.ErrOutOfRangeEarly) {
|
||||||
|
select {
|
||||||
|
case <-time.After(5 * time.Millisecond):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return Frame{}, AudioFrame{}, ctx.Err()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return Frame{}, AudioFrame{}, fmt.Errorf("GetGrain: %w", gerr)
|
||||||
}
|
}
|
||||||
// read audio at the same timestamp
|
// read audio at the same timestamp
|
||||||
aIdx := mxl.TimestampToIndex(s.aRate, ts)
|
aIdx := mxl.TimestampToIndex(s.aRate, ts)
|
||||||
@@ -451,14 +464,19 @@ func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout ti
|
|||||||
}
|
}
|
||||||
// even if audio failed, video returns
|
// even if audio failed, video returns
|
||||||
return vFrame, aFrame, nil
|
return vFrame, aFrame, nil
|
||||||
case errors.Is(err, mxl.ErrTimeout), errors.Is(err, mxl.ErrOutOfRangeEarly):
|
case errors.Is(err, mxl.ErrTimeout), errors.Is(err, mxl.ErrOutOfRangeEarly), errors.Is(err, mxl.ErrOutOfRangeLate):
|
||||||
|
timeouts++
|
||||||
|
if timeouts > 10 {
|
||||||
|
timeouts = 0
|
||||||
|
s.idx = mxl.CurrentIndex(s.rate)
|
||||||
|
return Frame{}, AudioFrame{}, fmt.Errorf("sync: feeds not responding")
|
||||||
|
}
|
||||||
|
s.idx = mxl.CurrentIndex(s.rate)
|
||||||
select {
|
select {
|
||||||
case <-time.After(5 * time.Millisecond):
|
case <-time.After(5 * time.Millisecond):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return Frame{}, AudioFrame{}, ctx.Err()
|
return Frame{}, AudioFrame{}, ctx.Err()
|
||||||
}
|
}
|
||||||
case errors.Is(err, mxl.ErrOutOfRangeLate):
|
|
||||||
s.idx = mxl.CurrentIndex(s.rate)
|
|
||||||
default:
|
default:
|
||||||
return Frame{}, AudioFrame{}, fmt.Errorf("WaitForDataAt: %w", err)
|
return Frame{}, AudioFrame{}, fmt.Errorf("WaitForDataAt: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "mxl-player/internal/imgui"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
imgui.New()
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Window][Debug##Default]
|
||||||
|
Pos=60,60
|
||||||
|
Size=400,400
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Stats]
|
||||||
|
Pos=10,10
|
||||||
|
Size=200,200
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
|
[Window][Connection]
|
||||||
|
Pos=60,60
|
||||||
|
Size=110,146
|
||||||
|
Collapsed=0
|
||||||
|
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
VIDEO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197ef"
|
||||||
|
AUDIO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197eb"
|
||||||
|
VIDEO_URI=$1
|
||||||
|
if [[ -z "${VIDEO_URI}" ]] then
|
||||||
|
VIDEO_URI="${HOME}/Videos/test-vid/f1.ts"
|
||||||
|
fi
|
||||||
|
export GST_PLUGIN_PATH="${HOME}/.gst-plugin:${GST_PLUGIN_PATH}"
|
||||||
|
mxl-gst-looping-filesrc -d /dev/shm/mxl -i "${VIDEO_URI}" --video-id "${VIDEO_ID}" --audio-id "${AUDIO_ID}" 2>/dev/null
|
||||||
|
# sleep 5
|
||||||
|
# kill -9 $(pidof "mxl-gst-looping-filesrc")
|
||||||
|
# echo -e "\ngst-looping-filesrc killed"
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#!/bin/bash
|
||||||
|
VIDEO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197ef"
|
||||||
|
AUDIO_ID="5fbec3b1-1b0f-417d-9059-8b94a47197eb"
|
||||||
|
VIDEO_URI=$1
|
||||||
|
if [[ -z "${VIDEO_URI}" ]] then
|
||||||
|
VIDEO_URI="${HOME}/Videos/test-vid/f1.ts"
|
||||||
|
fi
|
||||||
|
export GST_PLUGIN_PATH="${HOME}/.gst-plugin:${GST_PLUGIN_PATH}"
|
||||||
|
mxl-gst-looping-filesrc -d /dev/shm/mxl -i "${VIDEO_URI}" --video-id "${VIDEO_ID}" --audio-id "${AUDIO_ID}" 2>/dev/null &
|
||||||
|
sleep 1
|
||||||
|
echo "Writer has been started"
|
||||||
|
go run ../cmd/mxl-player -d /dev/shm/mxl -v "${VIDEO_ID}" -a "${AUDIO_ID}" &> /tmp/player.log &
|
||||||
|
echo "mxl-player has been started"
|
||||||
|
sleep 5
|
||||||
|
pkill mxl-gst
|
||||||
|
echo "Writer stopped" >> /tmp/player.log
|
||||||
|
sleep 5
|
||||||
|
echo "Writer has been started again" >> /tmp/player.log
|
||||||
|
mxl-gst-looping-filesrc -d /dev/shm/mxl -i "${VIDEO_URI}" --video-id "${VIDEO_ID}" --audio-id "${AUDIO_ID}" 2>/dev/null
|
||||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user