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 prepareThemeUC := usecase.NewPrepareWebUIThemeUseCase(injector)
124 themeCSSText := themeManager.GetWebUIThemeCSS()
125 if err := prepareThemeUC.Execute(ctx, usecase.PrepareWebUIThemeInput{CSSVars: themeCSSText}); err != nil {
126 logger.Warn().Err(err).Msg("failed to prepare WebUI theme CSS")
127 }
128
129 messageRouter := webkit.NewMessageRouter(ctx)
130 poolCfg := webkit.DefaultPoolConfig()
131 // Override prewarm count from resolved profile settings
132 if perfSettings.WebViewPoolPrewarmCount > 0 {
133 poolCfg.PrewarmCount = perfSettings.WebViewPoolPrewarmCount
134 }
135 pool := webkit.NewWebViewPool(ctx, wkCtx, settings, poolCfg, injector, messageRouter)
136 // Ensure prewarmed WebViews pick up the theme background color.
137 bgR, bgG, bgB, bgA := themeManager.GetBackgroundRGBA()
138 pool.SetBackgroundColor(bgR, bgG, bgB, bgA)
139
140 if filterManager != nil {
141 pool.SetFilterApplier(filterManager)
142 }
143
144 // Pre-create ONE WebView synchronously for instant first navigation.
145 // This is the heaviest operation but happens before window is shown,
146 // so users perceive it as part of the normal "loading" phase.
147 if err := pool.PrewarmFirst(ctx); err != nil {
148 logger.Warn().Err(err).Msg("failed to prewarm first webview, first tab may be slower")
149 }
150
151 return WebKitStack{
152 Context: wkCtx,
153 Settings: settings,
154 Injector: injector,
155 MessageRouter: messageRouter,
156 Pool: pool,
157 FilterManager: filterManager,
158 }
159}
160
161// hasWebProcessMemoryConfig returns true if any web process memory setting is configured.
162func hasWebProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
163 return p.WebProcessMemoryLimitMB > 0 ||
164 p.WebProcessMemoryPollIntervalSec > 0 ||
165 p.WebProcessMemoryConservativeThreshold > 0 ||
166 p.WebProcessMemoryStrictThreshold > 0
167}
168
169// hasNetworkProcessMemoryConfig returns true if any network process memory setting is configured.
170func hasNetworkProcessMemoryConfig(p *config.ResolvedPerformanceSettings) bool {
171 return p.NetworkProcessMemoryLimitMB > 0 ||
172 p.NetworkProcessMemoryPollIntervalSec > 0 ||
173 p.NetworkProcessMemoryConservativeThreshold > 0 ||
174 p.NetworkProcessMemoryStrictThreshold > 0
175}
176
177// configureRenderingEnvironment sets up the rendering environment before WebKit/GTK initialization.
178// Environment variables must be set before GTK/WebKit/GStreamer initializes.
179func configureRenderingEnvironment(
180 ctx context.Context,
181 cfg *config.Config,
182 perfSettings *config.ResolvedPerformanceSettings,
183 logger zerolog.Logger,
184) {
185 renderEnv := env.NewManager()
186 gpuVendor := renderEnv.DetectGPUVendor(ctx)
187 renderSettings := port.RenderingEnvSettings{
188 // GStreamer settings
189 ForceVSync: cfg.Media.ForceVSync,
190 GLRenderingMode: string(cfg.Media.GLRenderingMode),
191 GStreamerDebugLevel: cfg.Media.GStreamerDebugLevel,
192 VideoBufferSizeMB: cfg.Media.VideoBufferSizeMB,
193 QueueBufferTimeSec: cfg.Media.QueueBufferTimeSec,
194
195 // WebKit compositor settings
196 DisableDMABufRenderer: cfg.Rendering.DisableDMABufRenderer,
197 ForceCompositingMode: cfg.Rendering.ForceCompositingMode,
198 DisableCompositingMode: cfg.Rendering.DisableCompositingMode,
199
200 // GTK/GSK settings
201 GSKRenderer: string(cfg.Rendering.GSKRenderer),
202 DisableMipmaps: cfg.Rendering.DisableMipmaps,
203 PreferGL: cfg.Rendering.PreferGL,
204
205 // Debug settings
206 ShowFPS: cfg.Rendering.ShowFPS,
207 SampleMemory: cfg.Rendering.SampleMemory,
208 DebugFrames: cfg.Rendering.DebugFrames,
209
210 // Skia rendering thread settings (from resolved profile)
211 SkiaCPUPaintingThreads: perfSettings.SkiaCPUPaintingThreads,
212 SkiaGPUPaintingThreads: perfSettings.SkiaGPUPaintingThreads,
213 SkiaEnableCPURendering: perfSettings.SkiaEnableCPURendering,
214 }
215 if err := renderEnv.ApplyEnvironment(ctx, renderSettings); err != nil {
216 logger.Warn().Err(err).Msg("failed to apply rendering environment")
217 }
218 logger.Info().
219 Str("gpu", string(gpuVendor)).
220 Interface("vars", renderEnv.GetAppliedVars()).
221 Msg("rendering environment configured")
222}