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>
308 lines
7.8 KiB
Go
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)
|
|
}
|