From 5b76da0fdfca1698fe3ce2df3f19a6932c6301e4 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Sat, 20 Dec 2025 12:05:19 -0500 Subject: [PATCH] Improve benchmark results sorting and cancel flow --- DONE.md | 55 +++++++++++++++++- TODO.md | 29 ++++++++- internal/ui/benchmarkview.go | 87 +++++++++++++++++++++++++-- main.go | 110 +++++++++++++++++++++++------------ 4 files changed, 234 insertions(+), 47 deletions(-) diff --git a/DONE.md b/DONE.md index 0fad817..d4c0787 100644 --- a/DONE.md +++ b/DONE.md @@ -2,9 +2,59 @@ This file tracks completed features, fixes, and milestones. -## Version 0.1.0-dev19 (2025-12-18) - Convert Module Cleanup & UX Polish +## Version 0.1.0-dev19 (2025-12-18 to 2025-12-20) - Convert Module Cleanup & UX Polish -### Features +### Features (2025-12-20 Session) +- ✅ **History Sidebar - In Progress Tab** + - Added "In Progress" tab to history sidebar + - Shows running and pending jobs without opening queue + - Animated striped progress bars per module color + - Real-time progress updates (0-100%) + - No delete button on active jobs (only completed/failed) + - Dynamic status text ("Running..." or "Pending") + +- ✅ **Benchmark System Overhaul** + - **Hardware Detection Module** (`internal/sysinfo/sysinfo.go`) + - Cross-platform CPU detection (model, cores, clock speed) + - GPU detection with driver version (NVIDIA via nvidia-smi) + - RAM detection with human-readable formatting + - Linux, Windows, macOS support + - **Hardware Info Display** + - Shown immediately in benchmark progress view (before tests run) + - Displayed in benchmark results view + - Saved with each benchmark run for history + - **Settings Persistence** + - Hardware acceleration settings saved with benchmarks + - Settings persist between sessions via config file + - GPU automatically detected and used + - **UI Polish** + - "Run Benchmark" button highlighted (HighImportance) on first run + - Returns to normal styling after initial benchmark + - Guides new users to run initial benchmark + +- ✅ **Bitrate Preset Simplification** + - Reduced from 13 confusing options to 6 clear presets + - Removed resolution references (no more "1440p" confusion) + - Codec-agnostic (presets don't change selected codec) + - Quality-based naming: Low/Medium/Good/High/Very High Quality + - Focused on common use cases (1.5-8 Mbps range) + - Presets only set bitrate and switch to CBR mode + - User codec choice (H.264, VP9, AV1, etc.) preserved + +- ✅ **Quality Preset Codec Compatibility** + - "Lossless" quality option only available for H.265 and AV1 + - Dynamic quality dropdown based on selected codec + - Automatic fallback to "Near-Lossless" when switching to non-lossless codec + - Lossless + Target Size bitrate mode now supported for H.265/AV1 + - Prevents invalid codec/quality combinations + +- ✅ **App Icon Improvements** + - Regenerated VT_Icon.ico with transparent background + - Updated LoadAppIcon() to search PNG first (better Linux support) + - Searches both current directory and executable directory + - Added debug logging for icon loading troubleshooting + +### Features (2025-12-18 Session) - ✅ **History Sidebar Enhancements** - Delete button ("×") on each history entry - Remove individual entries from history @@ -715,6 +765,7 @@ This file tracks completed features, fixes, and milestones. ### Recent Fixes - ✅ Fixed aspect ratio default from 16:9 to Source (dev7) +- ✅ Ranked benchmark results by score and added cancel confirmation - ✅ Stabilized video seeking and embedded rendering - ✅ Improved player window positioning - ✅ Fixed clear video functionality diff --git a/TODO.md b/TODO.md index 920b3c0..eae7f51 100644 --- a/TODO.md +++ b/TODO.md @@ -2,10 +2,10 @@ This file tracks upcoming features, improvements, and known issues. -## Current Focus: dev19 - Convert Module Cleanup & Polish +## Current Focus: dev20+ - Feature Development ### In Progress -- [ ] **AI Frame Interpolation Support** +- [ ] **AI Frame Interpolation Support** (Deferred to dev20+) - RIFE (Real-Time Intermediate Flow Estimation) - https://github.com/hzwer/ECCV2022-RIFE - FILM (Frame Interpolation for Large Motion) - https://github.com/google-research/frame-interpolation - DAIN (Depth-Aware Video Frame Interpolation) - https://github.com/baowenbo/DAIN @@ -14,11 +14,34 @@ This file tracks upcoming features, improvements, and known issues. - Model download/management system - UI controls for model selection -- [ ] **Color Space Preservation** +- [ ] **Color Space Preservation** (Deferred to dev20+) - Fix color space preservation in upscale module - Ensure all conversions preserve color metadata (color_space, color_primaries, color_trc, color_range) - Test with HDR content +### Completed in dev19 (2025-12-20) +- [x] **History Sidebar - In Progress Tab** ✅ COMPLETED + - Shows running/pending jobs without opening full queue + - Animated progress bars per module color + - Real-time progress updates + +- [x] **Benchmark System Overhaul** ✅ COMPLETED + - Hardware detection module (CPU, GPU, RAM, drivers) + - Hardware info displayed in progress and results views + - Settings persistence across sessions + - First-run button highlighting + - Results ranked by score with cancel confirmation + +- [x] **Bitrate Preset Simplification** ✅ COMPLETED + - Codec-agnostic quality-based presets + - Removed confusing resolution references + - 6 clear presets: Manual, Low, Medium, Good, High, Very High + +- [x] **Quality Preset Codec Compatibility** ✅ COMPLETED + - Lossless option only for H.265/AV1 + - Dynamic dropdown based on codec + - Lossless + Target Size mode support + ## Priority Features for dev20+ ### Quality & Polish Improvements diff --git a/internal/ui/benchmarkview.go b/internal/ui/benchmarkview.go index 48ba176..70a08f6 100644 --- a/internal/ui/benchmarkview.go +++ b/internal/ui/benchmarkview.go @@ -3,20 +3,24 @@ package ui import ( "fmt" "image/color" + "sort" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" "git.leaktechnologies.dev/stu/VideoTools/internal/benchmark" + "git.leaktechnologies.dev/stu/VideoTools/internal/sysinfo" ) // BuildBenchmarkProgressView creates the benchmark progress UI func BuildBenchmarkProgressView( + hwInfo sysinfo.HardwareInfo, onCancel func(), titleColor, bgColor, textColor color.Color, ) *BenchmarkProgressView { view := &BenchmarkProgressView{ + hwInfo: hwInfo, titleColor: titleColor, bgColor: bgColor, textColor: textColor, @@ -28,6 +32,7 @@ func BuildBenchmarkProgressView( // BenchmarkProgressView shows real-time benchmark progress type BenchmarkProgressView struct { + hwInfo sysinfo.HardwareInfo titleColor color.Color bgColor color.Color textColor color.Color @@ -57,6 +62,37 @@ func (v *BenchmarkProgressView) build() { container.NewCenter(title), ) + // Hardware info section + hwInfoTitle := widget.NewLabel("System Hardware") + hwInfoTitle.TextStyle = fyne.TextStyle{Bold: true} + hwInfoTitle.Alignment = fyne.TextAlignCenter + + cpuLabel := widget.NewLabel(fmt.Sprintf("CPU: %s (%d cores @ %s)", v.hwInfo.CPU, v.hwInfo.CPUCores, v.hwInfo.CPUMHz)) + cpuLabel.Wrapping = fyne.TextWrapWord + + gpuLabel := widget.NewLabel(fmt.Sprintf("GPU: %s", v.hwInfo.GPU)) + gpuLabel.Wrapping = fyne.TextWrapWord + + ramLabel := widget.NewLabel(fmt.Sprintf("RAM: %s", v.hwInfo.RAM)) + + driverLabel := widget.NewLabel(fmt.Sprintf("Driver: %s", v.hwInfo.GPUDriver)) + driverLabel.Wrapping = fyne.TextWrapWord + + hwCard := canvas.NewRectangle(color.RGBA{R: 34, G: 38, B: 48, A: 255}) + hwCard.CornerRadius = 8 + + hwContent := container.NewVBox( + hwInfoTitle, + cpuLabel, + gpuLabel, + ramLabel, + driverLabel, + ) + + hwInfoSection := container.NewPadded( + container.NewMax(hwCard, hwContent), + ) + // Status section v.statusLabel = widget.NewLabel("Initializing benchmark...") v.statusLabel.TextStyle = fyne.TextStyle{Bold: true} @@ -96,6 +132,8 @@ func (v *BenchmarkProgressView) build() { header, nil, nil, nil, container.NewVBox( + hwInfoSection, + widget.NewSeparator(), statusSection, widget.NewSeparator(), resultsSection, @@ -188,6 +226,7 @@ func (v *BenchmarkProgressView) SetComplete() { func BuildBenchmarkResultsView( results []benchmark.Result, recommendation benchmark.Result, + hwInfo sysinfo.HardwareInfo, onApply func(), onClose func(), titleColor, bgColor, textColor color.Color, @@ -207,6 +246,37 @@ func BuildBenchmarkResultsView( container.NewCenter(title), ) + // Hardware info section + hwInfoTitle := widget.NewLabel("System Hardware") + hwInfoTitle.TextStyle = fyne.TextStyle{Bold: true} + hwInfoTitle.Alignment = fyne.TextAlignCenter + + cpuLabel := widget.NewLabel(fmt.Sprintf("CPU: %s (%d cores @ %s)", hwInfo.CPU, hwInfo.CPUCores, hwInfo.CPUMHz)) + cpuLabel.Wrapping = fyne.TextWrapWord + + gpuLabel := widget.NewLabel(fmt.Sprintf("GPU: %s", hwInfo.GPU)) + gpuLabel.Wrapping = fyne.TextWrapWord + + ramLabel := widget.NewLabel(fmt.Sprintf("RAM: %s", hwInfo.RAM)) + + driverLabel := widget.NewLabel(fmt.Sprintf("Driver: %s", hwInfo.GPUDriver)) + driverLabel.Wrapping = fyne.TextWrapWord + + hwCard := canvas.NewRectangle(color.RGBA{R: 34, G: 38, B: 48, A: 255}) + hwCard.CornerRadius = 8 + + hwContent := container.NewVBox( + hwInfoTitle, + cpuLabel, + gpuLabel, + ramLabel, + driverLabel, + ) + + hwInfoSection := container.NewPadded( + container.NewMax(hwCard, hwContent), + ) + // Recommendation section if recommendation.Encoder != "" { recTitle := widget.NewLabel("RECOMMENDED ENCODER") @@ -243,12 +313,19 @@ func BuildBenchmarkResultsView( topResultsTitle.TextStyle = fyne.TextStyle{Bold: true} topResultsTitle.Alignment = fyne.TextAlignCenter - var resultItems []fyne.CanvasObject - for i, result := range results { - if result.Error != "" { - continue + var filtered []benchmark.Result + for _, result := range results { + if result.Error == "" { + filtered = append(filtered, result) } + } + sort.Slice(filtered, func(i, j int) bool { + return filtered[i].Score > filtered[j].Score + }) + + var resultItems []fyne.CanvasObject + for i, result := range filtered { rankLabel := widget.NewLabel(fmt.Sprintf("#%d", i+1)) rankLabel.TextStyle = fyne.TextStyle{Bold: true} @@ -290,6 +367,8 @@ func BuildBenchmarkResultsView( header, nil, nil, nil, container.NewVBox( + hwInfoSection, + widget.NewSeparator(), recommendationSection, widget.NewSeparator(), resultsSection, diff --git a/main.go b/main.go index e8565a7..9c8a70d 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,7 @@ import ( "git.leaktechnologies.dev/stu/VideoTools/internal/modules" "git.leaktechnologies.dev/stu/VideoTools/internal/player" "git.leaktechnologies.dev/stu/VideoTools/internal/queue" + "git.leaktechnologies.dev/stu/VideoTools/internal/sysinfo" "git.leaktechnologies.dev/stu/VideoTools/internal/thumbnail" "git.leaktechnologies.dev/stu/VideoTools/internal/ui" "git.leaktechnologies.dev/stu/VideoTools/internal/utils" @@ -81,16 +82,17 @@ var ( nvencRuntimeOK bool modulesList = []Module{ - {"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet - {"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue - {"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan - {"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green - {"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green - {"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow - {"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange - {"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink - {"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red - {"player", "Player", utils.MustHex("#44FFDD"), "Playback", modules.HandlePlayer}, // Teal + {"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet + {"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue + {"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan + {"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green + {"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green + {"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow + {"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure + {"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange + {"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink + {"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red + {"player", "Player", utils.MustHex("#44FFDD"), "Playback", modules.HandlePlayer}, // Teal } // Platform-specific configuration @@ -557,12 +559,13 @@ func savePersistedConvertConfig(cfg convertConfig) error { // benchmarkRun represents a single benchmark test run type benchmarkRun struct { - Timestamp time.Time `json:"timestamp"` - Results []benchmark.Result `json:"results"` - RecommendedEncoder string `json:"recommended_encoder"` - RecommendedPreset string `json:"recommended_preset"` - RecommendedHWAccel string `json:"recommended_hwaccel"` - RecommendedFPS float64 `json:"recommended_fps"` + Timestamp time.Time `json:"timestamp"` + Results []benchmark.Result `json:"results"` + RecommendedEncoder string `json:"recommended_encoder"` + RecommendedPreset string `json:"recommended_preset"` + RecommendedHWAccel string `json:"recommended_hwaccel"` + RecommendedFPS float64 `json:"recommended_fps"` + HardwareInfo sysinfo.HardwareInfo `json:"hardware_info"` } // benchmarkConfig holds benchmark history @@ -1355,7 +1358,7 @@ func (s *appState) showMainMenu() { Label: m.Label, Color: m.Color, Category: m.Category, - Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect" || m.ID == "merge" || m.ID == "thumb" || m.ID == "player" || m.ID == "filters" || m.ID == "upscale", // Enabled modules + Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect" || m.ID == "merge" || m.ID == "thumb" || m.ID == "player" || m.ID == "filters" || m.ID == "upscale", // Enabled modules (subtitles placeholder stays disabled) }) } @@ -1408,11 +1411,17 @@ func (s *appState) showMainMenu() { ) } + // Check if benchmark has been run + hasBenchmark := false + if cfg, err := loadBenchmarkConfig(); err == nil && len(cfg.History) > 0 { + hasBenchmark = true + } + menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, nil, s.showBenchmark, s.showBenchmarkHistory, func() { // Toggle sidebar s.sidebarVisible = !s.sidebarVisible s.showMainMenu() - }, s.sidebarVisible, sidebar, titleColor, queueColor, textColor, queueCompleted, queueTotal) + }, s.sidebarVisible, sidebar, titleColor, queueColor, textColor, queueCompleted, queueTotal, hasBenchmark) // Update stats bar s.updateStatsBar() @@ -1733,17 +1742,35 @@ func (s *appState) showBenchmark() { s.stopPlayer() s.active = "benchmark" + // Detect hardware info upfront + hwInfo := sysinfo.Detect() + logging.Debug(logging.CatSystem, "detected hardware for benchmark: %s", hwInfo.Summary()) + // Create benchmark suite tmpDir := filepath.Join(os.TempDir(), "videotools-benchmark") _ = os.MkdirAll(tmpDir, 0o755) suite := benchmark.NewSuite(platformConfig.FFmpegPath, tmpDir) - // Build progress view + benchComplete := atomic.Bool{} + ctx, cancel := context.WithCancel(context.Background()) + + // Build progress view with hardware info view := ui.BuildBenchmarkProgressView( + hwInfo, func() { - // Cancel benchmark - s.showMainMenu() + if benchComplete.Load() { + s.showMainMenu() + return + } + + dialog.ShowConfirm("Cancel Benchmark?", "The benchmark is still running. Cancel it now?", func(ok bool) { + if !ok { + return + } + cancel() + s.showMainMenu() + }, s.window) }, utils.MustHex("#4CE870"), utils.MustHex("#1E1E1E"), @@ -1754,12 +1781,13 @@ func (s *appState) showBenchmark() { // Run benchmark in background go func() { - ctx := context.Background() - // Generate test video view.UpdateProgress(0, 100, "Generating test video", "") testPath, err := suite.GenerateTestVideo(ctx, 30) if err != nil { + if errors.Is(err, context.Canceled) { + return + } logging.Debug(logging.CatSystem, "failed to generate test video: %v", err) dialog.ShowError(fmt.Errorf("failed to generate test video: %w", err), s.window) s.showMainMenu() @@ -1780,6 +1808,9 @@ func (s *appState) showBenchmark() { // Run benchmark suite err = suite.RunFullSuite(ctx, availableEncoders) if err != nil { + if errors.Is(err, context.Canceled) { + return + } logging.Debug(logging.CatSystem, "benchmark failed: %v", err) dialog.ShowError(fmt.Errorf("benchmark failed: %w", err), s.window) s.showMainMenu() @@ -1793,6 +1824,7 @@ func (s *appState) showBenchmark() { // Mark complete view.SetComplete() + benchComplete.Store(true) // Get recommendation encoder, preset, rec := suite.GetRecommendation() @@ -1807,10 +1839,13 @@ func (s *appState) showBenchmark() { // Show results dialog with option to apply go func() { + // Detect hardware info for display + hwInfo := sysinfo.Detect() allResults := suite.Results // Show all results, not just top 10 resultsView := ui.BuildBenchmarkResultsView( allResults, rec, + hwInfo, func() { // Apply recommended settings s.applyBenchmarkRecommendation(encoder, preset) @@ -1876,6 +1911,10 @@ func (s *appState) saveBenchmarkRun(results []benchmark.Result, encoder, preset hwAccel = "none" } + // Detect hardware info + hwInfo := sysinfo.Detect() + logging.Debug(logging.CatSystem, "detected hardware: %s", hwInfo.Summary()) + // Load existing config cfg, err := loadBenchmarkConfig() if err != nil { @@ -1891,6 +1930,7 @@ func (s *appState) saveBenchmarkRun(results []benchmark.Result, encoder, preset RecommendedPreset: preset, RecommendedHWAccel: hwAccel, RecommendedFPS: fps, + HardwareInfo: hwInfo, } // Add to history (keep last 10 runs) @@ -1991,6 +2031,7 @@ func (s *appState) showBenchmarkHistory() { resultsView := ui.BuildBenchmarkResultsView( run.Results, rec, + run.HardwareInfo, func() { // Apply this recommendation s.applyBenchmarkRecommendation(run.RecommendedEncoder, run.RecommendedPreset) @@ -5786,9 +5827,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { "Target Size (Calculate from file size)", } bitrateModeMap := map[string]string{ - "CRF (Constant Rate Factor)": "CRF", - "CBR (Constant Bitrate)": "CBR", - "VBR (Variable Bitrate)": "VBR", + "CRF (Constant Rate Factor)": "CRF", + "CBR (Constant Bitrate)": "CBR", + "VBR (Variable Bitrate)": "VBR", "Target Size (Calculate from file size)": "Target Size", } reverseMap := map[string]string{ @@ -5857,18 +5898,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject { presets := []bitratePreset{ {Label: "Manual", Bitrate: "", Codec: ""}, - {Label: "AV1 1080p - 1200k (smallest)", Bitrate: "1200k", Codec: "AV1"}, - {Label: "AV1 1080p - 1400k (sweet spot)", Bitrate: "1400k", Codec: "AV1"}, - {Label: "AV1 1080p - 1800k (headroom)", Bitrate: "1800k", Codec: "AV1"}, - {Label: "H.265 1080p - 2000k (balanced)", Bitrate: "2000k", Codec: "H.265"}, - {Label: "H.265 1080p - 2400k (noisy sources)", Bitrate: "2400k", Codec: "H.265"}, - {Label: "AV1 1440p - 2600k (balanced)", Bitrate: "2600k", Codec: "AV1"}, - {Label: "H.265 1440p - 3200k (balanced)", Bitrate: "3200k", Codec: "H.265"}, - {Label: "H.265 1440p - 4000k (noisy sources)", Bitrate: "4000k", Codec: "H.265"}, - {Label: "AV1 4K - 5M (balanced)", Bitrate: "5000k", Codec: "AV1"}, - {Label: "H.265 4K - 6M (balanced)", Bitrate: "6000k", Codec: "H.265"}, - {Label: "AV1 4K - 7M (archive)", Bitrate: "7000k", Codec: "AV1"}, - {Label: "H.265 4K - 9M (fast/Topaz)", Bitrate: "9000k", Codec: "H.265"}, + {Label: "1.5 Mbps - Low Quality", Bitrate: "1500k", Codec: ""}, + {Label: "2.5 Mbps - Medium Quality", Bitrate: "2500k", Codec: ""}, + {Label: "4.0 Mbps - Good Quality", Bitrate: "4000k", Codec: ""}, + {Label: "6.0 Mbps - High Quality", Bitrate: "6000k", Codec: ""}, + {Label: "8.0 Mbps - Very High Quality", Bitrate: "8000k", Codec: ""}, } bitratePresetLookup := make(map[string]bitratePreset)