VideoTools/internal/ui/benchmarkview.go
Stu Leak 87c2d28e9f Add comprehensive hardware encoder benchmarking system
Implemented a full benchmark system that automatically detects available
hardware encoders, tests them with different presets, measures FPS
performance, and recommends optimal settings for the user's system.

Features:
- Automatic test video generation (30s 1080p test pattern)
- Hardware encoder detection (NVENC, QSV, AMF, VideoToolbox)
- Comprehensive encoder testing across multiple presets
- Real-time progress UI with live results
- Performance scoring based on FPS metrics
- Top 10 results display with recommendation
- Config persistence for benchmark results
- One-click apply to use recommended settings

UI Components:
- Benchmark button in main menu header
- Progress view showing current test and results
- Final results view with ranked encoders
- Apply/Close actions for recommendation

Integration:
- Added to main menu between "Benchmark" and "Logs" buttons
- Saves results to ~/.config/VideoTools/benchmark.json
- Comprehensive debug logging for troubleshooting

This allows users to optimize their encoding settings based on their
specific hardware capabilities rather than guessing which encoder
will work best.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 09:16:36 -05:00

308 lines
7.8 KiB
Go

package ui
import (
"fmt"
"image/color"
"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"
)
// BuildBenchmarkProgressView creates the benchmark progress UI
func BuildBenchmarkProgressView(
onCancel func(),
titleColor, bgColor, textColor color.Color,
) *BenchmarkProgressView {
view := &BenchmarkProgressView{
titleColor: titleColor,
bgColor: bgColor,
textColor: textColor,
onCancel: onCancel,
}
view.build()
return view
}
// BenchmarkProgressView shows real-time benchmark progress
type BenchmarkProgressView struct {
titleColor color.Color
bgColor color.Color
textColor color.Color
onCancel func()
container *fyne.Container
statusLabel *widget.Label
progressBar *widget.ProgressBar
currentLabel *widget.Label
resultsBox *fyne.Container
cancelBtn *widget.Button
}
func (v *BenchmarkProgressView) build() {
// Header
title := canvas.NewText("ENCODER BENCHMARK", v.titleColor)
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
title.TextSize = 24
v.cancelBtn = widget.NewButton("Cancel", v.onCancel)
v.cancelBtn.Importance = widget.DangerImportance
header := container.NewBorder(
nil, nil,
nil,
v.cancelBtn,
container.NewCenter(title),
)
// Status section
v.statusLabel = widget.NewLabel("Initializing benchmark...")
v.statusLabel.TextStyle = fyne.TextStyle{Bold: true}
v.statusLabel.Alignment = fyne.TextAlignCenter
v.progressBar = widget.NewProgressBar()
v.progressBar.Min = 0
v.progressBar.Max = 100
v.currentLabel = widget.NewLabel("")
v.currentLabel.Alignment = fyne.TextAlignCenter
v.currentLabel.Wrapping = fyne.TextWrapWord
statusSection := container.NewVBox(
v.statusLabel,
v.progressBar,
v.currentLabel,
)
// Results section
resultsTitle := widget.NewLabel("Results")
resultsTitle.TextStyle = fyne.TextStyle{Bold: true}
resultsTitle.Alignment = fyne.TextAlignCenter
v.resultsBox = container.NewVBox()
resultsScroll := container.NewVScroll(v.resultsBox)
resultsScroll.SetMinSize(fyne.NewSize(0, 300))
resultsSection := container.NewBorder(
resultsTitle,
nil, nil, nil,
resultsScroll,
)
// Main layout
body := container.NewBorder(
header,
nil, nil, nil,
container.NewVBox(
statusSection,
widget.NewSeparator(),
resultsSection,
),
)
v.container = container.NewPadded(body)
}
// GetContainer returns the main container
func (v *BenchmarkProgressView) GetContainer() *fyne.Container {
return v.container
}
// UpdateProgress updates the progress bar and labels
func (v *BenchmarkProgressView) UpdateProgress(current, total int, encoder, preset string) {
pct := float64(current) / float64(total)
v.progressBar.SetValue(pct)
v.statusLabel.SetText(fmt.Sprintf("Testing encoder %d of %d", current, total))
v.currentLabel.SetText(fmt.Sprintf("Testing: %s (preset: %s)", encoder, preset))
v.progressBar.Refresh()
v.statusLabel.Refresh()
v.currentLabel.Refresh()
}
// AddResult adds a completed test result to the display
func (v *BenchmarkProgressView) AddResult(result benchmark.Result) {
var statusColor color.Color
var statusText string
if result.Error != "" {
statusColor = color.RGBA{R: 255, G: 68, B: 68, A: 255} // Red
statusText = fmt.Sprintf("FAILED: %s", result.Error)
} else {
statusColor = color.RGBA{R: 76, G: 232, B: 112, A: 255} // Green
statusText = fmt.Sprintf("%.1f FPS | %.1fs encoding time", result.FPS, result.EncodingTime)
}
// Status indicator
statusRect := canvas.NewRectangle(statusColor)
statusRect.SetMinSize(fyne.NewSize(6, 0))
// Encoder label
encoderLabel := widget.NewLabel(fmt.Sprintf("%s (%s)", result.Encoder, result.Preset))
encoderLabel.TextStyle = fyne.TextStyle{Bold: true}
// Status label
statusLabel := widget.NewLabel(statusText)
statusLabel.Wrapping = fyne.TextWrapWord
// Card content
content := container.NewBorder(
nil, nil,
statusRect,
nil,
container.NewVBox(encoderLabel, statusLabel),
)
// Card background
card := canvas.NewRectangle(v.bgColor)
card.CornerRadius = 4
item := container.NewPadded(
container.NewMax(card, content),
)
v.resultsBox.Add(item)
v.resultsBox.Refresh()
}
// SetComplete marks the benchmark as complete
func (v *BenchmarkProgressView) SetComplete() {
v.statusLabel.SetText("Benchmark complete!")
v.progressBar.SetValue(1.0)
v.currentLabel.SetText("")
v.cancelBtn.SetText("Close")
v.statusLabel.Refresh()
v.progressBar.Refresh()
v.currentLabel.Refresh()
v.cancelBtn.Refresh()
}
// BuildBenchmarkResultsView creates the final results/recommendation UI
func BuildBenchmarkResultsView(
results []benchmark.Result,
recommendation benchmark.Result,
onApply func(),
onClose func(),
titleColor, bgColor, textColor color.Color,
) fyne.CanvasObject {
// Header
title := canvas.NewText("BENCHMARK RESULTS", titleColor)
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
title.TextSize = 24
closeBtn := widget.NewButton("Close", onClose)
closeBtn.Importance = widget.LowImportance
header := container.NewBorder(
nil, nil,
nil,
closeBtn,
container.NewCenter(title),
)
// Recommendation section
if recommendation.Encoder != "" {
recTitle := widget.NewLabel("RECOMMENDED ENCODER")
recTitle.TextStyle = fyne.TextStyle{Bold: true}
recTitle.Alignment = fyne.TextAlignCenter
recEncoder := widget.NewLabel(fmt.Sprintf("%s (preset: %s)", recommendation.Encoder, recommendation.Preset))
recEncoder.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
recEncoder.Alignment = fyne.TextAlignCenter
recStats := widget.NewLabel(fmt.Sprintf("%.1f FPS | %.1fs encoding time | Score: %.1f",
recommendation.FPS, recommendation.EncodingTime, recommendation.Score))
recStats.Alignment = fyne.TextAlignCenter
applyBtn := widget.NewButton("Apply to Settings", onApply)
applyBtn.Importance = widget.HighImportance
recCard := canvas.NewRectangle(color.RGBA{R: 68, G: 136, B: 255, A: 50})
recCard.CornerRadius = 8
recContent := container.NewVBox(
recTitle,
recEncoder,
recStats,
container.NewCenter(applyBtn),
)
recommendationSection := container.NewPadded(
container.NewMax(recCard, recContent),
)
// Top results list
topResultsTitle := widget.NewLabel("Top Encoders")
topResultsTitle.TextStyle = fyne.TextStyle{Bold: true}
topResultsTitle.Alignment = fyne.TextAlignCenter
var resultItems []fyne.CanvasObject
for i, result := range results {
if result.Error != "" {
continue
}
rankLabel := widget.NewLabel(fmt.Sprintf("#%d", i+1))
rankLabel.TextStyle = fyne.TextStyle{Bold: true}
encoderLabel := widget.NewLabel(fmt.Sprintf("%s (%s)", result.Encoder, result.Preset))
statsLabel := widget.NewLabel(fmt.Sprintf("%.1f FPS | %.1fs | Score: %.1f",
result.FPS, result.EncodingTime, result.Score))
statsLabel.TextStyle = fyne.TextStyle{Italic: true}
content := container.NewBorder(
nil, nil,
rankLabel,
nil,
container.NewVBox(encoderLabel, statsLabel),
)
card := canvas.NewRectangle(bgColor)
card.CornerRadius = 4
item := container.NewPadded(
container.NewMax(card, content),
)
resultItems = append(resultItems, item)
}
resultsBox := container.NewVBox(resultItems...)
resultsScroll := container.NewVScroll(resultsBox)
resultsScroll.SetMinSize(fyne.NewSize(0, 300))
resultsSection := container.NewBorder(
topResultsTitle,
nil, nil, nil,
resultsScroll,
)
// Main layout
body := container.NewBorder(
header,
nil, nil, nil,
container.NewVBox(
recommendationSection,
widget.NewSeparator(),
resultsSection,
),
)
return container.NewPadded(body)
}
// No results case
emptyMsg := widget.NewLabel("No benchmark results available")
emptyMsg.Alignment = fyne.TextAlignCenter
body := container.NewBorder(
header,
nil, nil, nil,
container.NewCenter(emptyMsg),
)
return container.NewPadded(body)
}