Public release README.md fix
This commit is contained in:
@@ -0,0 +1,592 @@
|
||||
# MXL Player refactoring plan
|
||||
|
||||
## Goal
|
||||
|
||||
Build a player with two persistent logical feed slots: video and audio. Each
|
||||
slot may be configured, started, stopped, replaced, reconnected, or removed
|
||||
while the program is running.
|
||||
|
||||
When both slots are configured, the user may enable or disable synchronization
|
||||
at any time. Synchronization is a runtime relationship between the slots, not a
|
||||
startup mode.
|
||||
|
||||
The design must leave a clean extension point for an `mxlfabrics` reader after
|
||||
the local MXL player is stable. Playlist timing and selection must remain above
|
||||
media readers and playback workers.
|
||||
|
||||
## Current status
|
||||
|
||||
- Stages 0 through 11 are complete.
|
||||
- The reader-factory extension point from Stage 12 is complete; the
|
||||
`mxlfabrics` implementation remains future work.
|
||||
- Stage 13 is complete, including manual/timed navigation, looping,
|
||||
pause/resume, failure policies, playlist-level retry configuration, and GUI
|
||||
diagnostics.
|
||||
- Playlist lifecycle messages are explicit `PlaylistEvent` values produced by
|
||||
`PlaylistEventCoordinator`; runtime cancellation owns shutdown, so shared
|
||||
event channels are not closed by either endpoint.
|
||||
|
||||
## Required behaviour
|
||||
|
||||
### No configured feeds
|
||||
|
||||
- Start the normal GUI and renderer.
|
||||
- Show a placeholder, simple shader, or empty player surface.
|
||||
- Allow the user to enter either or both UUIDs.
|
||||
|
||||
### One configured feed
|
||||
|
||||
- An audio UUID starts audio playback.
|
||||
- A video UUID starts video playback.
|
||||
- A failed feed reconnects according to its retry policy.
|
||||
- The user may stop and later resume the feed without clearing its UUID.
|
||||
- The user may replace or remove the UUID at runtime.
|
||||
|
||||
### Two configured feeds, synchronization disabled
|
||||
|
||||
- Audio and video run as independent workers.
|
||||
- Failure of one feed must not stop, close, or restart the other.
|
||||
- Each feed has its own state, last error, and retry counter.
|
||||
- The user may stop or resume either feed independently.
|
||||
- Synchronization can be enabled at runtime.
|
||||
|
||||
### Two configured feeds, synchronization enabled
|
||||
|
||||
- Both feeds are read through one synchronization group.
|
||||
- Failure of either member fails the group attempt.
|
||||
- Reconnection recreates the complete group.
|
||||
- Synchronization can be disabled at runtime. The two feeds then continue as
|
||||
independent workers.
|
||||
- The user may stop both feeds together. Stopping only one member dismantles
|
||||
the group and lets the other member continue independently.
|
||||
|
||||
### Stop, resume, and remove semantics
|
||||
|
||||
- `StopVideo` stops video reading and retrying but retains the video UUID.
|
||||
- `StopAudio` stops audio reading and retrying but retains the audio UUID.
|
||||
- `StopAll` stops both feeds and all retry activity but retains both UUIDs.
|
||||
- A stopped feed may be resumed without re-entering its UUID.
|
||||
- Removing a feed stops it and clears its UUID.
|
||||
- Stopping one detached feed has no effect on the other feed.
|
||||
- Stopping one member of a synchronized group disables synchronization,
|
||||
stops the selected feed, and continues the other feed independently.
|
||||
- Stopping both members of a synchronized group closes the group atomically.
|
||||
- Stop commands cancel active reads and retry backoff promptly.
|
||||
|
||||
### Playlist behaviour
|
||||
|
||||
A playlist is an ordered list of playback entries. Each entry describes a
|
||||
complete desired session state and may contain:
|
||||
|
||||
- synchronized video and audio UUIDs;
|
||||
- independent video and audio UUIDs;
|
||||
- video only;
|
||||
- audio only;
|
||||
- an optional per-entry playback duration.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
1. synchronized video A + audio A, 10 seconds
|
||||
2. synchronized video B + audio B, 15 seconds
|
||||
3. video C, 10 seconds
|
||||
4. video D, 20 seconds
|
||||
5. audio E, 30 seconds
|
||||
```
|
||||
|
||||
The user may select any entry manually. Automatic playback advances after the
|
||||
current entry's duration and loops from the final entry back to the first.
|
||||
|
||||
Playlist orchestration belongs above `SessionController`. Selecting an entry
|
||||
must translate its desired state into the same controller operations used by
|
||||
CLI and GUI controls. Source readers, workers, retry supervisors, renderer, and
|
||||
audio output must not know that a playlist exists.
|
||||
|
||||
## Decisions to confirm before implementation
|
||||
|
||||
- `MaxAttempts == 0` means retry indefinitely.
|
||||
- The initial connection counts as attempt 1.
|
||||
- Retry counters reset only after useful media has been received for a defined
|
||||
stability period, not merely after a reader opens.
|
||||
- Enabling or disabling synchronization starts a new lifecycle and resets the
|
||||
applicable retry counters.
|
||||
- Explicitly removing either feed disables requested synchronization.
|
||||
- Explicitly stopping one member of a synchronized group disables requested
|
||||
synchronization. Resuming that feed does not silently recreate the group;
|
||||
the user can enable synchronization again.
|
||||
- Requesting synchronization with only one configured feed records the request,
|
||||
continues independent playback, and waits for the second feed.
|
||||
- Exhausting attempts leaves the UUID configured and the unit in `Failed` until
|
||||
the user retries, replaces the UUID, changes synchronization, or removes it.
|
||||
|
||||
These are proposed defaults. Change them here before implementing the affected
|
||||
stage if different behaviour is desired.
|
||||
|
||||
## Target architecture
|
||||
|
||||
```text
|
||||
CLI initial values GUI runtime commands
|
||||
| |
|
||||
+----------------+-----------------+
|
||||
v
|
||||
SessionController
|
||||
desired vs actual state
|
||||
|
|
||||
+-----------+-----------+
|
||||
| |
|
||||
sync disabled sync enabled
|
||||
VideoWorker SyncGroupWorker
|
||||
AudioWorker (video + audio)
|
||||
| |
|
||||
+-----------+-----------+
|
||||
v
|
||||
stable output layer
|
||||
video sink / audio sink
|
||||
```
|
||||
|
||||
### Ownership rules
|
||||
|
||||
- `SessionController` is the only owner allowed to change playback topology.
|
||||
- A worker exclusively owns its reader and closes it when the worker stops.
|
||||
- Independent audio and video workers never close each other's resources.
|
||||
- A synchronized worker owns and closes the entire group.
|
||||
- GUI and renderer consume status or media; they never inspect live readers.
|
||||
- Every topology change increments a generation number. Late events from an old
|
||||
generation are ignored.
|
||||
|
||||
### Package direction
|
||||
|
||||
```text
|
||||
internal/source
|
||||
Local MXL reader adapters and media metadata.
|
||||
No retry, GUI, SDL, Vulkan, or application policy.
|
||||
|
||||
internal/playback
|
||||
Feed configuration, commands, state, retry supervisor, independent workers,
|
||||
synchronized worker, and session controller.
|
||||
|
||||
internal/output (optional once extraction is useful)
|
||||
Stable video and audio sink interfaces and their adapters.
|
||||
|
||||
cmd/mxl-player
|
||||
Process initialization, CLI translation, GUI event translation, and wiring.
|
||||
```
|
||||
|
||||
Do not introduce a single broad media-source interface. Video, audio, and a
|
||||
synchronized pair have different results and should use small explicit
|
||||
interfaces.
|
||||
|
||||
## Runtime state
|
||||
|
||||
Each independently supervised playback unit uses:
|
||||
|
||||
```text
|
||||
Idle -> Connecting -> Playing -> Reconnecting -> Failed
|
||||
^ | | |
|
||||
+---------+------------+------------+-> Stopping -> Idle
|
||||
```
|
||||
|
||||
`Idle` may still have a configured UUID. Configuration and running state are
|
||||
separate: a configured slot can be stopped without being removed.
|
||||
|
||||
Synchronization additionally distinguishes desired and actual state:
|
||||
|
||||
```text
|
||||
Disabled
|
||||
TransitioningOn
|
||||
Enabled
|
||||
TransitioningOff
|
||||
WaitingForSecondFeed
|
||||
```
|
||||
|
||||
The GUI should receive immutable status snapshots containing at least:
|
||||
|
||||
- configured UUID;
|
||||
- desired and actual synchronization state;
|
||||
- playback state per active unit;
|
||||
- current attempt and configured limit;
|
||||
- last error;
|
||||
- last successful media time;
|
||||
- received and dropped media counters.
|
||||
|
||||
## Retry policy
|
||||
|
||||
Retry belongs above `internal/source`.
|
||||
|
||||
One attempt is:
|
||||
|
||||
```text
|
||||
open reader(s) -> receive media -> terminal read error -> close reader(s)
|
||||
```
|
||||
|
||||
The supervisor then either stops or waits using cancellable capped exponential
|
||||
backoff with small jitter before opening again.
|
||||
|
||||
Requirements:
|
||||
|
||||
- Apply the same policy to initial connection and later disconnection.
|
||||
- Allow finite and infinite attempts.
|
||||
- Cancel immediately on shutdown, UUID replacement, or topology change.
|
||||
- Cancel immediately when the user stops the supervised feed.
|
||||
- Never run two retry loops for the same playback unit.
|
||||
- Retain and publish the final error after attempt exhaustion.
|
||||
- Classify configuration/format errors separately from transient availability
|
||||
errors. Whether permanent errors should retry is decided explicitly.
|
||||
|
||||
## Runtime synchronization transitions
|
||||
|
||||
### Enable synchronization
|
||||
|
||||
1. Record synchronization as desired and increment the topology generation.
|
||||
2. Cancel both independent workers.
|
||||
3. Wait until both workers close their readers.
|
||||
4. Clear stale queued audio.
|
||||
5. Create the synchronized worker with the existing UUIDs.
|
||||
6. Establish a new shared timeline and publish `Enabled` after valid paired data.
|
||||
7. If opening or reading fails, retry the group as one unit.
|
||||
|
||||
The renderer may retain the last video frame or show a placeholder during the
|
||||
transition.
|
||||
|
||||
### Disable synchronization
|
||||
|
||||
1. Record synchronization as disabled and increment the topology generation.
|
||||
2. Cancel and close the synchronized worker and group.
|
||||
3. Clear audio belonging to the old synchronized timeline.
|
||||
4. Start independent workers for both configured slots.
|
||||
5. Let either worker begin playing without waiting for the other.
|
||||
|
||||
## Playlist model
|
||||
|
||||
The implemented model is conceptually:
|
||||
|
||||
```go
|
||||
type PlaylistEntry struct {
|
||||
Name string
|
||||
Video PlaylistFeed // domain + UUID, optional
|
||||
Audio PlaylistFeed // domain + UUID, optional
|
||||
SyncRequested bool
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type Playlist struct {
|
||||
Entries []PlaylistEntry
|
||||
Loop bool
|
||||
OnFailure PlaylistFailurePolicy // wait or next
|
||||
Retry *RetryPolicy // optional playlist-wide override
|
||||
}
|
||||
```
|
||||
|
||||
The playlist controller owns:
|
||||
|
||||
- current entry index;
|
||||
- manual selection;
|
||||
- automatic advance timer;
|
||||
- loop behaviour;
|
||||
- pause/resume of automatic advance;
|
||||
- applying one entry atomically to the session controller.
|
||||
|
||||
Before implementing playlists, decide when an entry's duration begins. The
|
||||
recommended rule is when its requested playback topology first reaches
|
||||
`Playing`, so connection and retry time do not consume the viewing period.
|
||||
Also decide whether an entry that exhausts retries waits for user action or
|
||||
automatically advances; this should be configurable rather than implicit.
|
||||
|
||||
## Implementation stages
|
||||
|
||||
Only begin a stage after the previous stage's acceptance criteria pass. Keep
|
||||
each stage small enough for one focused review and commit.
|
||||
|
||||
### Stage 0 — Behaviour contract and baseline
|
||||
|
||||
Work:
|
||||
|
||||
- Confirm or edit the decisions in this document.
|
||||
- Record the current CLI examples and observable behaviour.
|
||||
- Run the existing build/tests and record known failures.
|
||||
- Identify commands used for a local audio feed, video feed, and paired feeds.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Behaviour choices are unambiguous.
|
||||
- A repeatable baseline command is documented.
|
||||
- No application code changes.
|
||||
|
||||
### Stage 1 — Pure configuration model
|
||||
|
||||
Work:
|
||||
|
||||
- Introduce configuration types for domain, two optional feed slots, desired
|
||||
synchronization, and retry policy.
|
||||
- Make validation pure: return errors instead of printing or exiting.
|
||||
- Permit no-feed startup.
|
||||
- Require a domain only when at least one connection is requested.
|
||||
- Translate CLI arguments into the same configuration later used by GUI actions.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Unit tests cover zero, one, and two feeds plus invalid domains/policies.
|
||||
- Existing playable CLI combinations still translate correctly.
|
||||
- No playback lifecycle is moved yet.
|
||||
|
||||
### Stage 2 — Normalize low-level source APIs
|
||||
|
||||
Work:
|
||||
|
||||
- Use consistent `VideoSource`, `AudioSource`, and `SyncSource` naming.
|
||||
- Make every blocking read context-aware and bounded.
|
||||
- Remove duplicate read paths where safe.
|
||||
- Centralize audio-fragment extraction and video-frame construction.
|
||||
- Define classifiable source errors.
|
||||
- Make MXL payload ownership and copying rules explicit.
|
||||
- Make a synchronized read fail when either member cannot provide its part.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Source tests cover cancellation and important MXL error classification.
|
||||
- A read cannot hide forever inside an internal retry loop.
|
||||
- Existing audio-only, video-only, and synchronized smoke tests still work.
|
||||
|
||||
### Stage 3 — Retry supervisor
|
||||
|
||||
Work:
|
||||
|
||||
- Implement a reusable supervisor around open, run, close, and retry.
|
||||
- Add finite/infinite limits and cancellable capped backoff.
|
||||
- Publish state transitions and attempt counts.
|
||||
- Add deterministic timing hooks so tests do not sleep in real time.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Tests cover initial failure, runtime failure, exhaustion, successful recovery,
|
||||
counter reset, and cancellation during backoff.
|
||||
- The supervisor has no SDL, Vulkan, ImGui, or concrete MXL dependency.
|
||||
|
||||
### Stage 4 — Independent video worker
|
||||
|
||||
Work:
|
||||
|
||||
- Move video reader ownership and retry lifecycle into a video worker.
|
||||
- Deliver owned frames through a bounded latest-frame channel or video sink.
|
||||
- Stop writing directly into renderer staging memory from the source worker.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Video reconnects without application restart.
|
||||
- Replacing or removing its UUID cancels the old reader promptly.
|
||||
- Slow rendering does not create an unbounded live-stream backlog.
|
||||
|
||||
### Stage 5 — Independent audio worker
|
||||
|
||||
Work:
|
||||
|
||||
- Move audio reader ownership and retry lifecycle into an audio worker.
|
||||
- Centralize channel interleaving.
|
||||
- Use bounded ordered buffering and explicit backpressure.
|
||||
- Define when the SDL queue is cleared during reconnect/replacement.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Audio reconnects without application restart.
|
||||
- Audio is correct for mono, stereo, and more than two channels.
|
||||
- Cancellation cannot leave stale samples playing indefinitely.
|
||||
|
||||
### Stage 6 — Session controller and independent dual playback
|
||||
|
||||
Work:
|
||||
|
||||
- Add typed controller commands rather than overloaded channel values.
|
||||
- Own video and audio slot configuration in the controller.
|
||||
- Track configured and running/desired-active state separately for each slot.
|
||||
- Add topology generations and ignore stale worker events.
|
||||
- Run both independent workers when both UUIDs are configured.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Video failure does not interrupt audio.
|
||||
- Audio failure does not interrupt video.
|
||||
- Either UUID can be replaced while the other feed stays alive.
|
||||
- Either feed can be stopped and resumed while the other stays alive.
|
||||
- Stopping both feeds leaves the application and configured UUIDs intact.
|
||||
- Race detector finds no shared-state races in controller/worker tests.
|
||||
|
||||
### Stage 7 — Synchronized worker
|
||||
|
||||
Work:
|
||||
|
||||
- Move sync-group ownership into one supervised worker.
|
||||
- Treat missing video or audio data as failure of the group attempt.
|
||||
- Recreate all synchronized resources on reconnect.
|
||||
- Publish paired media from one established timeline.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Failure of either member restarts the complete group.
|
||||
- Retry exhaustion stops the group cleanly and retains both configured UUIDs.
|
||||
- Cancellation releases the group and both readers promptly.
|
||||
|
||||
### Stage 8 — Runtime sync toggle
|
||||
|
||||
Work:
|
||||
|
||||
- Implement atomic enable and disable transitions described above.
|
||||
- Preserve configured UUIDs across both transitions.
|
||||
- Handle toggle commands during connection, playback, and retry backoff.
|
||||
- Handle per-feed and stop-all commands in detached and synchronized playback.
|
||||
- Define and implement behaviour when only one slot is configured.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Detached feeds can be joined without restarting the application.
|
||||
- A sync group can be detached into independent feeds without restarting.
|
||||
- Rapid repeated toggles cannot leave duplicate workers or readers.
|
||||
- Stopping one synchronized member detaches the group and preserves playback of
|
||||
the other member.
|
||||
- Stopping both synchronized members closes the group without starting new
|
||||
independent workers.
|
||||
- Old-generation errors cannot change the new topology.
|
||||
|
||||
### Stage 9 — Stable dynamic outputs and idle startup
|
||||
|
||||
Work:
|
||||
|
||||
- Initialize GUI and rendering without requiring a successful source open.
|
||||
- Show a placeholder when no video frame is available.
|
||||
- Reconfigure video resources when dimensions/stride/format change.
|
||||
- Reconfigure the audio stream when rate/channel/device properties change.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- `mxl-player` with no feed opens a usable GUI.
|
||||
- The user can move among idle, audio-only, video-only, detached dual-feed, and
|
||||
synchronized playback without restarting.
|
||||
- Output resources do not depend on the initial CLI topology.
|
||||
|
||||
### Stage 10 — GUI and CLI integration
|
||||
|
||||
Work:
|
||||
|
||||
- Convert GUI actions into controller commands.
|
||||
- Add fields/actions for both UUIDs, reconnect, remove, retry, and sync toggle.
|
||||
- Add independent start/stop controls and a stop-all action.
|
||||
- Display desired/actual sync state and independent/group retry status.
|
||||
- Add CLI options for retry limits and initial synchronization preference.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- CLI values only establish initial desired state.
|
||||
- Every important runtime operation is available in the GUI.
|
||||
- GUI never reads or closes source pointers directly.
|
||||
|
||||
### Stage 11 — Resilience verification
|
||||
|
||||
Test at least:
|
||||
|
||||
- missing producer on initial connection;
|
||||
- producer disappearance during playback;
|
||||
- independent audio-only and video-only failure;
|
||||
- one failed detached feed while the other remains alive;
|
||||
- failure of either synchronized member;
|
||||
- finite exhaustion and infinite retry cancellation;
|
||||
- UUID replacement during reads and backoff;
|
||||
- stopping or resuming either feed during reads and retry backoff;
|
||||
- stopping both feeds while detached and synchronized;
|
||||
- synchronization toggles during reads and backoff;
|
||||
- shutdown during blocked reads;
|
||||
- resolution, rate, and channel-count changes;
|
||||
- mono, stereo, and multichannel audio;
|
||||
- long runtime and repeated connect/disconnect cycles;
|
||||
- `go test -race` for testable non-GPU packages.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- The specified failure boundaries hold in every test.
|
||||
- No known goroutine, MXL reader, sync group, SDL stream, or Vulkan resource leak.
|
||||
- Player state remains understandable after every exhausted retry sequence.
|
||||
|
||||
### Stage 12 — Reader backend extension point
|
||||
|
||||
Work:
|
||||
|
||||
- Introduce small reader factories only after local MXL behaviour is solid.
|
||||
- Keep playback workers dependent on reader interfaces/factories, not `go-mxl`.
|
||||
- Implement local MXL as the first backend.
|
||||
- Add `mxlfabrics` as a second backend without changing controller semantics.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Backend selection does not alter retry or synchronization semantics.
|
||||
- Fake readers can drive all controller and supervisor tests.
|
||||
- Local MXL remains the reference implementation.
|
||||
|
||||
### Stage 13 — Simple playlist (complete)
|
||||
|
||||
Work:
|
||||
|
||||
- Add ordered playlist entries containing optional video/audio UUIDs,
|
||||
synchronization preference, and per-entry duration.
|
||||
- Apply entry changes atomically through `SessionController`.
|
||||
- Add manual previous, next, and direct-entry selection.
|
||||
- Add timed automatic advance and optional looping.
|
||||
- Add pause/resume for automatic playlist advancement.
|
||||
- Expose the current entry and remaining time in the GUI.
|
||||
- Define behaviour for stopped playback, retry exhaustion, invalid entries, and
|
||||
manual selection while an automatic timer is active.
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- A playlist may freely mix synchronized pairs, independent pairs, video-only,
|
||||
and audio-only entries.
|
||||
- Manual selection works regardless of the current playback topology.
|
||||
- Timed entries advance in order and loop without leaking old workers/readers.
|
||||
- Entry duration follows the documented start rule and uses cancellable timers.
|
||||
- An old entry's timers and worker events cannot affect a newly selected entry.
|
||||
- Playlist logic contains no direct MXL, SDL, Vulkan, or renderer operations.
|
||||
|
||||
## Suggested command API
|
||||
|
||||
Names may change, but commands must have one explicit meaning:
|
||||
|
||||
```go
|
||||
SetVideo(uuid string)
|
||||
SetAudio(uuid string)
|
||||
StartVideo()
|
||||
StartAudio()
|
||||
StartAll()
|
||||
StopVideo()
|
||||
StopAudio()
|
||||
StopAll()
|
||||
RemoveVideo()
|
||||
RemoveAudio()
|
||||
EnableSync()
|
||||
DisableSync()
|
||||
ReconnectVideo()
|
||||
ReconnectAudio()
|
||||
RetryNow()
|
||||
Stop()
|
||||
```
|
||||
|
||||
In synchronized operation, reconnecting either slot means reconnecting the
|
||||
group. The UI should make that consequence visible.
|
||||
|
||||
## Out of scope for the initial player refactoring
|
||||
|
||||
- Playlist implementation before the controller, dynamic topology, and output
|
||||
lifecycle are stable. The planned playlist work is Stage 13.
|
||||
- Automatic multi-UUID failure-based failover selection.
|
||||
- Snapshot, waveform, vectorscope, and advanced diagnostics.
|
||||
- A large generic media framework.
|
||||
- `mxlfabrics` implementation before the local player passes resilience tests.
|
||||
- Unrelated renderer or GUI redesign.
|
||||
|
||||
## Working method
|
||||
|
||||
For every stage:
|
||||
|
||||
1. Agree on the narrow change and expected behaviour.
|
||||
2. Make the change without pulling later-stage responsibilities forward.
|
||||
3. Format and run focused tests.
|
||||
4. Run the relevant manual smoke test.
|
||||
5. Review ownership, cancellation, and error handling.
|
||||
6. Record discoveries or revised decisions in this document.
|
||||
7. Commit the stage separately when accepted.
|
||||
@@ -0,0 +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
|
||||
|
||||
## Done
|
||||
- [x] resize broken again
|
||||
- [x] q for quit
|
||||
@@ -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,24 @@
|
||||
03.09.26
|
||||
|
||||
## Distrobox ubuntu:latest
|
||||
0) check that libmxl installed, PKG_CONFIG_PATH leads to valid libmxl.pc
|
||||
1) git clone repo
|
||||
2) install golang libsdl3-dev spdlog-dev (for some reasons sdplog required in libmxl.pc)
|
||||
3) cd go-mxl-player
|
||||
4) go build -o ./build/mxl-player ./cmd/mxl-player/
|
||||
|
||||
## Distrobox fedora:latest
|
||||
0) check that libmxl installed, PKG_CONFIG_PATH leads to valid libmxl.pc
|
||||
1) git clone repo
|
||||
2) dnf install golang SDL3-devel spdlog-devel g++
|
||||
3) cd go-mxl-player
|
||||
4) go build -o ./build/mxl-player ./cmd/mxl-player/
|
||||
|
||||
## MacOS test. Real hardware
|
||||
|
||||
0) check libmxl installed
|
||||
1) git clone repo
|
||||
2)
|
||||
3) cd go-mxl-player
|
||||
4) go build -o ./build/mxl-player ./cmd/mxl-player
|
||||
5) install_name_tool -add_rpath $PATH_TO_MXL_LIB ./build/mxl-player
|
||||
Reference in New Issue
Block a user