1package bootstrap
2
3import (
4 "context"
5 "path/filepath"
6
7 "github.com/bnema/dumber/assets"
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/infrastructure/config"
11 "github.com/bnema/dumber/internal/infrastructure/env"
12 "github.com/bnema/dumber/internal/infrastructure/filtering"
13 "github.com/bnema/dumber/internal/infrastructure/webkit"
14 "github.com/bnema/dumber/internal/ui/theme"
15 "github.com/rs/zerolog"
16)
17
18// WebKitStackInput holds the input for BuildWebKitStack.
19type WebKitStackInput struct {
20 Ctx context.Context
21 Config *config.Config
22 DataDir string
23 CacheDir string
24 ThemeManager *theme.Manager
25 ColorResolver port.ColorSchemeResolver
26 Logger zerolog.Logger
27}
28
29type WebKitStack struct {
30 Context *webkit.WebKitContext
31 Settings *webkit.SettingsManager
32 Injector *webkit.ContentInjector
33 MessageRouter *webkit.MessageRouter
34 Pool *webkit.WebViewPool
35 FilterManager *filtering.Manager
36}
37
38func BuildWebKitStack(input WebKitStackInput) WebKitStack {
39 ctx := input.Ctx
40 cfg := input.Config
41 dataDir := input.DataDir
42 cacheDir := input.CacheDir
43 themeManager := input.ThemeManager
44 colorResolver := input.ColorResolver
45 logger := input.Logger
46 // Detect hardware for performance profile scaling
47 hwSurveyor := env.NewHardwareSurveyor()
48 hwInfo := hwSurveyor.Survey(ctx)
49 logger.Info().
50 Int("cpu_cores", hwInfo.CPUCores).
51 Int("cpu_threads", hwInfo.CPUThreads).
52 Uint64("ram_mb", hwInfo.TotalRAM/(1024*1024)).
53 Str("gpu_vendor", string(hwInfo.GPUVendor)).
54 Uint64("vram_mb", hwInfo.VRAM/(1024*1024)).
55 Msg("hardware survey completed")
56
57 // Resolve performance profile to get actual settings
58 perfSettings := config.ResolvePerformanceProfile(&cfg.Performance, &hwInfo)
59 logger.Info().
60 Str("profile", string(cfg.Performance.Profile)).
61 Int("skia_cpu_threads", perfSettings.SkiaCPUPaintingThreads).
62 Int("skia_gpu_threads", perfSettings.SkiaGPUPaintingThreads).
63 Int("webview_pool_prewarm", perfSettings.WebViewPoolPrewarmCount).
64 Msg("resolved performance profile")
65
66 // Configure rendering environment (must happen before WebKit/GTK init)
67 configureRenderingEnvironment(ctx, cfg, &perfSettings, logger)
68
69 // Build WebKitContext options with memory pressure settings
70 wkOpts := port.WebKitContextOptions{
71 DataDir: dataDir,
72 CacheDir: cacheDir,
73 }
74
75 // Configure web process memory pressure if any setting is configured
76 if hasWebProcessMemoryConfig(&perfSettings) {
77 wkOpts.WebProcessMemory = &port.MemoryPressureConfig{
78 MemoryLimitMB: perfSettings.WebProcessMemoryLimitMB,
79 PollIntervalSec: perfSettings.WebProcessMemoryPollIntervalSec,
80 ConservativeThreshold: perfSettings.WebProcessMemoryConservativeThreshold,
81 StrictThreshold: perfSettings.WebProcessMemoryStrictThreshold,
82 }
83 }
84
85 // Configure network process memory pressure if any setting is configured
86 if hasNetworkProcessMemoryConfig(&perfSettings) {
87 wkOpts.NetworkProcessMemory = &port.MemoryPressureConfig{
88 MemoryLimitMB: perfSettings.NetworkProcessMemoryLimitMB,
89 PollIntervalSec: perfSettings.NetworkProcessMemoryPollIntervalSec,
90 ConservativeThreshold: perfSettings.NetworkProcessMemoryConservativeThreshold,
91 StrictThreshold: perfSettings.NetworkProcessMemoryStrictThreshold,
92 }
93 }
94
95 wkCtx, err := webkit.NewWebKitContextWithOptions(ctx, wkOpts)
96 if err != nil {
97 logger.Fatal().Err(err).Msg("failed to initialize WebKit context")
98 }
99
100 filterStoreDir := filepath.Join(dataDir, "filters", "store")
101 filterJSONDir := filepath.Join(dataDir, "filters", "json")
102 filterManager, err := filtering.NewManager(filtering.ManagerConfig{
103 StoreDir: filterStoreDir,
104 JSONDir: filterJSONDir,
105 Enabled: cfg.ContentFiltering.Enabled,
106 AutoUpdate: cfg.ContentFiltering.AutoUpdate,
107 })
108 if err != nil {
109 logger.Warn().Err(err).Msg("failed to create filter manager, continuing without content filtering")
110 } else {
111 if err := filterManager.Initialize(ctx); err != nil {
112 logger.Warn().Err(err).Msg("failed to initialize filters, will load async")
113 }
114 }
115
116 schemeHandler := webkit.NewDumbSchemeHandler(ctx)
117 schemeHandler.SetAssets(assets.WebUIAssets)
118 schemeHandler.RegisterWithContext(wkCtx)
119
120 settings := webkit.NewSettingsManager(ctx, cfg)
121 injector := webkit.NewContentInjector(colorResolver)
122
123 // Set up auto-copy on selection config getter
124 injector.SetAutoCopyConfigGetter(func() bool {
125 return config.Get().Clipboard.AutoCopyOnSelection
126 })
127
128 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
129 themeCSSText := themeManager.GetWebUIThemeCSS()
130 if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil {
131 logger.Warn().Err(err).Msg("failed to prepare WebUI theme CSS")
132 }
133
134 messageRouter := webkit.NewMessageRouter(ctx)
135 poolCfg := webkit.DefaultPoolConfig()
136 // Override prewarm count from resolved profile settings
137 if perfSettings.WebViewPoolPrewarmCount > 0 {
138 poolCfg.PrewarmCount = perfSettings.WebViewPoolPrewarmCount
139 }
140 pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter)
141 // Ensure prewarmed WebViews pick up the theme background color.
142 bgR, bgG, bgB, bgA := themeManager.GetBackgroundRGBA()
143 pool.SetBackgroundColor(bgR, bgG, bgB, bgA)
144
145 if filterManager != nil {
146 pool.SetFilterApplier(filterManager)
147 }
148
149 // Pre-create ONE WebView synchronously for instant first navigation.
150 // This is the heaviest operation but happens before window is shown,
151 // so users perceive it as part of the normal "loading" phase.
152 if err := pool.PrewarmFirst(ctx); err != nil {
153 logger.Warn().Err(err).Msg("failed to prewarm first webview, first tab may be slower")
154 }
155
156 return WebKitStack{
157 Context: wkCtx,
158 Settings: settings,
159 Injector: injector,
160 MessageRouter: messageRouter,
161 Pool: pool,
162 FilterManager: filterManager,
163 }
164}
165
166// hasWebProcessMemoryConfig returns true if any web process memory setting is configured.
167func hasWebProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
168 return p.WebProcessMemoryLimitMB > 0 ||
169 p.WebProcessMemoryPollIntervalSec > 0 ||
170 p.WebProcessMemoryConservativeThreshold > 0 ||
171 p.WebProcessMemoryStrictThreshold > 0
172}
173
174// hasNetworkProcessMemoryConfig returns true if any network process memory setting is configured.
175func hasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
176 return p.NetworkProcessMemoryLimitMB > 0 ||
177 p.NetworkProcessMemoryPollIntervalSec > 0 ||
178 p.NetworkProcessMemoryConservativeThreshold > 0 ||
179 p.NetworkProcessMemoryStrictThreshold > 0
180}
181
182// configureRenderingEnvironment sets up the rendering environment before WebKit/GTK initialization.
183// Environment variables must be set before GTK/WebKit/GStreamer initializes.
184func configureRenderingEnvironment(
185 ctx context.Context,
186 cfg *config.Config,
187 perfSettings *config.ResolvedPerformanceSettings,
188 logger zerolog.Logger,
189) {
190 renderEnv := env.NewManager()
191 gpuVendor := renderEnv.DetectGPUVendor(ctx)
192 renderSettings := port.RenderingEnvSettings{
193 // GStreamer settings
194 ForceVSync: cfg.Media.ForceVSync,
195 GLRenderingMode: string(cfg.Media.GLRenderingMode),
196 GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel,
197
198 // WebKit compositor settings
199 DisableDMABufRenderer: cfg.Rendering.DisableDMABufRenderer,
200 ForceCompositingMode: cfg.Rendering.ForceCompositingMode,
201 DisableCompositingMode: cfg.Rendering.DisableCompositingMode,
202
203 // GTK/GSK settings
204 GSKRenderer: string(cfg.Rendering.GSKRenderer),
205 DisableMipmaps: cfg.Rendering.DisableMipmaps,
206 PreferGL: cfg.Rendering.PreferGL,
207
208 // Debug settings
209 ShowFPS: cfg.Rendering.ShowFPS,
210 SampleMemory: cfg.Rendering.SampleMemory,
211 DebugFrames: cfg.Rendering.DebugFrames,
212
213 // Skia rendering thread settings (from resolved profile)
214 SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
215 SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
216 SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
217 }
218 if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
219 logger.Warn().Err(err).Msg("failed to apply rendering environment")
220 }
221 logger.Info().
222 Str("gpu", string(gpuVendor)).
223 Interface("vars", renderEnv.GetAppliedVars()).
224 Msg("rendering environment configured")
225}