Gui #2

Merged
itten merged 5 commits from GUI into main 2026-08-25 22:35:38 +03:00
22 changed files with 1124 additions and 120 deletions
+245 -114
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"log"
"mxl-player/internal/imgui"
"mxl-player/internal/renderer"
"mxl-player/internal/sdl"
"mxl-player/internal/source"
@@ -14,8 +15,8 @@ import (
"time"
"unsafe"
cimgui "github.com/AllenDang/cimgui-go/imgui"
vk "github.com/christerso/vulkan-go/vk"
"github.com/qvest-digital/go-mxl/mxl"
pflag "github.com/spf13/pflag"
)
@@ -76,8 +77,10 @@ func checkMXLargs(args appArgs) {
}
func main() {
// video: 5fbec3b1-1b0f-417d-9059-8b94a47197ed
// audio: 5fbec3b1-1b0f-417d-9059-8b94a47197ec
// timelapse video: 5fbec3b1-1b0f-417d-9059-8b94a47197ed
// timelapse audio: 5fbec3b1-1b0f-417d-9059-8b94a47197ec
// f1 video: 5fbec3b1-1b0f-417d-9059-8b94a47197ef
// f1 audio: 5fbec3b1-1b0f-417d-9059-8b94a47197eb
var args appArgs
flagSet := pflag.NewFlagSet(APP_NAME, pflag.ContinueOnError)
flagSet.Usage = func() { printUsage(os.Stderr) }
@@ -137,6 +140,13 @@ func main() {
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 {
panic(err)
}
@@ -250,7 +260,6 @@ func main() {
fmt.Printf("sync: video %dx%d audio %dch batch=%d\n",
syncSrc.Width(), syncSrc.Height(), aChans, audioBatch)
case args.VideoFlowId != "":
videoSrc, err = source.Open(args.Domain, args.VideoFlowId)
if err != nil {
log.Fatalf("source: %v", err)
@@ -317,71 +326,110 @@ func main() {
panic(err)
}
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()
}
// 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())
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)
failed := make(chan struct{})
reopen := func() error {
reopen := func(params reconnectParams) error {
// Close current sources
if syncSrc != nil {
_ = syncSrc.Close()
syncSrc = nil
}
if videoSrc != nil {
_ = videoSrc.Close()
videoSrc = nil
}
if audioSrc != nil {
_ = audioSrc.Close()
audioSrc = nil
}
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if syncSrc != nil {
s, e := source.OpenSync(args.Domain, args.VideoFlowId, args.AudioFlowId)
if e == nil {
if r != nil {
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
if newSize != r.FrameSize() {
if e = r.RecreateBuffers(newSize); e != nil {
return e
}
// Try once. Return error if fails — caller loops back to select
// and can pick up new reconnect params or a new grant.
if params.video != "" && params.audio != "" {
s, e := source.OpenSync(params.domain, params.video, params.audio)
if e == nil {
if r != nil {
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
if newSize != r.FrameSize() {
if e = r.RecreateBuffers(newSize); e != nil {
return e
}
}
syncSrc = s
return nil
}
log.Printf("source: reopen retry: %v", e)
} else if videoSrc != nil {
s, e := source.Open(args.Domain, args.VideoFlowId)
if e == nil {
if r != nil {
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
if newSize != r.FrameSize() {
if e = r.RecreateBuffers(newSize); e != nil {
return e
}
syncSrc = s
aChans = s.Channels()
audioBatch = uint64(s.AudioRate().Num) / uint64(s.Rate().Num)
if audioBatch == 0 {
audioBatch = 1
}
return nil
}
return e
} else if params.video != "" {
s, e := source.Open(params.domain, params.video)
if e == nil {
if r != nil {
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
if newSize != r.FrameSize() {
if e = r.RecreateBuffers(newSize); e != nil {
return e
}
}
videoSrc = s
return nil
}
log.Printf("source: reopen retry: %v", e)
} else if audioSrc != nil {
s, e := source.OpenAudio(args.Domain, args.AudioFlowId)
if e == nil {
audioSrc = s
return nil
}
log.Printf("source: reopen retry: %v", e)
videoSrc = s
return nil
}
time.Sleep(500 * time.Millisecond)
return e
} else if params.audio != "" {
s, e := source.OpenAudio(params.domain, params.audio)
if e == nil {
audioSrc = s
aChans = s.Channels()
audioBatch = uint64(s.Rate().Num) / (100 * uint64(s.Rate().Den))
if audioBatch == 0 {
audioBatch = 1
}
return nil
}
return e
}
return fmt.Errorf("reopen: no flow specified")
}
doReconnect := func() {
select {
case <-control:
default:
}
control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}
}
go func() {
@@ -391,6 +439,25 @@ func main() {
select {
case <-ctx.Done():
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:
}
queued := sdl.GetAudioStreamQueued(audioStream)
@@ -408,18 +475,22 @@ func main() {
if errors.Is(err, context.Canceled) {
return
}
if errors.Is(err, mxl.ErrFlowInvalid) {
log.Printf("source: flow invalid, reopening")
if rerr := reopen(); rerr != nil {
log.Printf("source: reopen failed: %v", rerr)
cancel()
return
}
continue
}
log.Printf("source: %v", err)
cancel()
return
params := reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}
select {
case <-control:
default:
}
select {
case <-time.After(500 * time.Millisecond):
case <-ctx.Done():
return
}
select {
case control <- params:
default:
}
continue
}
if f.Samples != nil && audioStream != 0 {
sdl.PutAudioStreamData(audioStream, interleaveAudio(f.Samples))
@@ -429,10 +500,29 @@ func main() {
// Video (with or without sync) mode: grant/staged handshake.
for {
select {
case <-grant:
case <-ctx.Done():
return
params := <-control
if params.video != "" || params.audio != "" {
// Reconnect request from Connect button or auto-retry.
select {
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():
return
}
select {
case control <- params:
default:
}
}
continue
}
var payload []byte
@@ -444,23 +534,18 @@ func main() {
if errors.Is(err, context.Canceled) {
return
}
if errors.Is(err, mxl.ErrFlowInvalid) {
log.Printf("source: flow invalid, reopening")
if rerr := reopen(); rerr != nil {
log.Printf("source: reopen failed: %v", rerr)
cancel()
return
}
select {
case failed <- struct{}{}:
case <-ctx.Done():
return
}
continue
}
log.Printf("source: %v", err)
cancel()
return
// Drain any pending grant, then send reconnect.
select {
case <-control:
default:
}
select {
case control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}:
case <-ctx.Done():
return
}
continue
}
payload = vFrame.Payload
grainIdx = vFrame.Index
@@ -474,27 +559,20 @@ func main() {
if errors.Is(err, context.Canceled) {
return
}
if errors.Is(err, mxl.ErrFlowInvalid) {
log.Printf("source: flow invalid, reopening")
if rerr := reopen(); rerr != nil {
log.Printf("source: reopen failed: %v", rerr)
cancel()
return
}
select {
case failed <- struct{}{}:
case <-ctx.Done():
return
}
continue
}
log.Printf("source: %v", err)
cancel()
return
select {
case <-control:
default:
}
select {
case control <- reconnectParams{domain: domainStr, video: videoStr, audio: audioStr}:
case <-ctx.Done():
return
}
continue
}
payload = f.Payload
grainIdx = f.Index
}
if r != nil {
@@ -516,11 +594,15 @@ func main() {
sdl.SetWindowFullscreen(windowHandler, true)
}
var (
fps float64
lastIndex uint64
dropped uint64
frameCount uint64
lastReport time.Time
lastFrame time.Time
)
lastFrame = time.Now()
for running {
frameStart := time.Now()
var event [128]byte
@@ -532,16 +614,24 @@ func main() {
case sdl.EventWindowResized, sdl.EventPixelSizeChanged:
resized = true
case sdl.EventKeyDown:
if gui.IO().WantCaptureKeyboard() {
break
}
key := *(*int32)(unsafe.Pointer(&event[28]))
switch uint32(key) {
case sdl.KeyQ:
fallthrough
case sdl.KeyEscape:
running = false
case sdl.KeyF:
fullscreen = !fullscreen
sdl.SetWindowFullscreen(windowHandler, fullscreen)
resized = true
case sdl.KeyF1:
showStats = !showStats
}
}
gui.ProcessEvent(&event)
}
if !running {
break
@@ -558,7 +648,7 @@ func main() {
}
if !granted {
select {
case grant <- struct{}{}:
case control <- reconnectParams{}:
granted = true
case <-ctx.Done():
running = false
@@ -566,20 +656,77 @@ func main() {
}
}
var shownIndex uint64
hasFrame := false
select {
case shownIndex = <-staged:
granted = false
case <-failed:
granted = false
continue
hasFrame = true
case <-ctx.Done():
running = false
continue
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 {
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
if syncSrc != nil {
w, h, stride = syncSrc.Width(), syncSrc.Height(), syncSrc.Stride()
@@ -601,22 +748,6 @@ func main() {
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 {
time.Sleep(10 * time.Millisecond)
}
+10 -1
View File
@@ -1,14 +1,23 @@
# Useful links
https://pthom.github.io/imgui_explorer/
# List of bugs, that need to be fixed
## Major
- check how it looks like with more than 2 audio channels
## Minor
- hotkeys on keyDown, especially toggle keys. Way to recreate: press F and hold
## TODO:
- some sort of playlist with id's
- CLI option to run fullscreen
- fabrics bridge reader. Step by step. Start with local
- basic UI: stats, fields for domain, flow ids, label, etc.
- snapshot
- waveform, vectorscope
- some image, when audio only
- q for quit
## Done
- [x] resize broken again
- [x] q for quit
+2
View File
@@ -10,3 +10,5 @@ require (
require github.com/qvest-digital/go-mxl v1.1.0-rc.1
require github.com/spf13/pflag v1.0.10
require github.com/AllenDang/cimgui-go v1.5.0
+2
View File
@@ -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/go.mod h1:DIo6W2yFav1DflNKc0W0Oc7mecbdBg2btsaHhIYmwUA=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
+233
View File
@@ -0,0 +1,233 @@
# M9 — GUI (Dear ImGui)
## Context
The player already works (M0M8): 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.
+20
View File
@@ -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
+301
View File
@@ -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()
}
+47
View File
@@ -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 }
+108
View File
@@ -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))
}
+9
View File
@@ -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
+14
View File
@@ -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.
+25
View File
@@ -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.
+9
View File
@@ -59,6 +59,9 @@ type Renderer struct {
imageAvailable vk.Semaphore
renderFinished vk.Semaphore
inFlight vk.Fence
// Called inside render pass, after video draw
ImGuiDraw func(cmd vk.CommandBuffer)
}
// 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.Draw(3, 1, 0, 0)
if r.ImGuiDraw != nil {
r.ImGuiDraw(cmd)
}
cmd.EndRenderPass()
if err := cmd.End(); err != nil {
return err
@@ -465,3 +471,6 @@ func (r *Renderer) DrawFrame(videoW, videoH, stride uint32) error {
}
return nil
}
func (r *Renderer) CmdPool() vk.CommandPool { return r.cmdPool }
func (r *Renderer) RenderPass() vk.RenderPass { return r.renderPass }
+23 -1
View File
@@ -16,10 +16,22 @@ const (
EventQuit uint32 = 0x100
EventWindowResized uint32 = 0x206
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
KeyF uint32 = 0x66
KeyQ uint32 = 0x71
KeyF1 uint32 = 0x4000003A
InitAudio uint32 = 0x00000010
AudioDeviceDefaultPlayback uint32 = 0xFFFFFFFF
@@ -49,6 +61,9 @@ var (
sdlDestroyAudioStream func(stream uintptr)
sdlGetAudioPlaybackDevices func(count *int32) uintptr
sdlGetAudioDeviceName func(devid uint32) uintptr
sdlStartTextInput func(window uintptr)
sdlStopTextInput func(window uintptr)
)
var loaded = false
@@ -79,6 +94,9 @@ func Load() error {
purego.RegisterLibFunc(&sdlDestroyAudioStream, h, "SDL_DestroyAudioStream")
purego.RegisterLibFunc(&sdlGetAudioPlaybackDevices, h, "SDL_GetAudioPlaybackDevices")
purego.RegisterLibFunc(&sdlGetAudioDeviceName, h, "SDL_GetAudioDeviceName")
// input
purego.RegisterLibFunc(&sdlStartTextInput, h, "SDL_StartTextInput")
purego.RegisterLibFunc(&sdlStopTextInput, h, "SDL_StopTextInput")
loaded = true
return nil
}
@@ -174,3 +192,7 @@ func GetAudioPlaybackDevices() []AudioDevice {
}
return devs
}
// Input wrappers
func StartTextInput(window uintptr) { sdlStartTextInput(window) }
func StopTextInput(window uintptr) { sdlStopTextInput(window) }
+22 -4
View File
@@ -403,6 +403,7 @@ func (s *SyncSource) Close() error {
// 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) {
var timeouts int
for {
select {
case <-ctx.Done():
@@ -421,7 +422,19 @@ func (s *SyncSource) NextSync(ctx context.Context, audioBatch uint64, timeout ti
s.idx = mxl.CurrentIndex(s.rate)
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
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
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 {
case <-time.After(5 * time.Millisecond):
case <-ctx.Done():
return Frame{}, AudioFrame{}, ctx.Err()
}
case errors.Is(err, mxl.ErrOutOfRangeLate):
s.idx = mxl.CurrentIndex(s.rate)
default:
return Frame{}, AudioFrame{}, fmt.Errorf("WaitForDataAt: %w", err)
}
+7
View File
@@ -0,0 +1,7 @@
package main
import "mxl-player/internal/imgui"
func main() {
imgui.New()
}
+15
View File
@@ -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
+12
View File
@@ -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"
+20
View File
@@ -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
View File
Binary file not shown.