detached 89e934d

refactor(config): consolidate engine migration metadata

commit
89e934d3c61261570396e663d08e6a3f273baa7d
author
brice · · 2026-06-25 12:02 UTC
parent
f4ac668
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +416 -228
M docs/config/index.md +101 -101
@@ -248,38 +248,67 @@ When Noctalia rewrites the watched file, Dumber reapplies the same resolved them
248248
249249 ## Rendering, UI Scale & Zoom
250250
251+CEF is the default browser engine. WebKitGTK remains available as a fallback via `engine.type = "webkit"`; `engine.webkit.*` settings only affect that fallback engine.
252+
251253 | Key | Type | Default | Valid Values | Description |
252254 |-----|------|---------|--------------|-------------|
253-| `rendering.mode` | string | `"gpu"` | `auto`, `gpu`, `cpu` | WebKit hardware acceleration policy |
254-| `rendering.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit DMA-BUF renderer (may fix flicker on Wayland; slower) |
255-| `rendering.force_compositing_mode` | bool | `false` | - | Force WebKit compositing mode (`WEBKIT_FORCE_COMPOSITING_MODE`) |
256-| `rendering.disable_compositing_mode` | bool | `false` | - | Disable WebKit compositing mode (`WEBKIT_DISABLE_COMPOSITING_MODE`) |
257-| `rendering.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | GTK renderer selection (`GSK_RENDERER`) |
258-| `rendering.disable_mipmaps` | bool | `false` | - | Disable GTK mipmaps (`GSK_GPU_DISABLE=mipmap`) |
259-| `rendering.prefer_gl` | bool | `false` | - | Prefer OpenGL over GLES (`GDK_DEBUG=gl-prefer-gl`) |
260-| `rendering.draw_compositing_indicators` | bool | `false` | - | Draw WebKit compositing indicators (debug) |
261-| `rendering.show_fps` | bool | `false` | - | Show WebKit FPS counter (`WEBKIT_SHOW_FPS`) |
262-| `rendering.sample_memory` | bool | `false` | - | Enable WebKit memory sampling (`WEBKIT_SAMPLE_MEMORY`) |
263-| `rendering.debug_frames` | bool | `false` | - | Enable GTK frame timing debug (`GDK_DEBUG=frames`) |
264-| `engine.type` | string | `"cef"` | `cef`, `webkit` | Browser engine. WebKit remains available as a fallback. |
255+| `engine.type` | string | `"cef"` | `cef`, `webkit` | Browser engine selection; WebKitGTK is the fallback option |
265256 | `engine.cef.render_stack` | string | `"vulkan"` | `vulkan`, `egl` | CEF GPU render stack |
266-| `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | - | Enable adaptive CEF OSR FPS polling (only applies when `windowless_frame_rate = 0`) |
267-| `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled, otherwise CEF default |
268-| `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 | Hard cap for adaptive CEF OSR FPS; 0 falls back to the built-in default cap selected by Dumber/CEF |
269-| `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 | Mouse wheel scroll sensitivity multiplier |
270-| `engine.cef.input.scroll_precise_multiplier` | float | `2.5` | > 0 | Precise/surface scroll sensitivity multiplier for touchpads and high-resolution wheels |
271-| `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier |
272-| `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier; touchpad vertical speed uses this together with `scroll_precise_multiplier` |
273-| `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |
257+| `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | - | Enable adaptive CEF OSR FPS polling when `windowless_frame_rate = 0` |
258+| `engine.cef.windowless_frame_rate` | int32 | `0` | `>= 0` | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled |
259+| `engine.cef.windowless_frame_rate_max` | int32 | `240` | `>= 0` | Hard cap for adaptive CEF OSR FPS; 0 uses the built-in cap |
260+| `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | `> 0` | Mouse wheel scroll sensitivity multiplier |
261+| `engine.cef.input.scroll_precise_multiplier` | float | `2.5` | `> 0` | Precise/surface scroll sensitivity multiplier for touchpads and high-resolution wheels |
262+| `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | `> 0` | Horizontal scroll sensitivity multiplier |
263+| `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | `> 0` | Vertical scroll sensitivity multiplier; combines with `scroll_precise_multiplier` for touchpads |
264+| `engine.cef.input.scroll_max_delta` | int32 | `0` | `>= 0` | Maximum absolute scroll delta after scaling; 0 disables clamping |
274265 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |
275-| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 | Minimum accumulated horizontal swipe delta required for navigation |
276-| `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes; does not affect vertical scroll speed |
277-| `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |
266+| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | `> 0` | Minimum accumulated horizontal swipe delta required for navigation |
267+| `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | `> 0` | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |
268+| `engine.webkit.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | WebKit fallback GTK renderer selection (`GSK_RENDERER`) |
269+| `engine.webkit.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit fallback DMA-BUF renderer |
270+| `engine.webkit.force_compositing_mode` | bool | `false` | - | Force WebKit fallback compositing mode (`WEBKIT_FORCE_COMPOSITING_MODE`) |
271+| `engine.webkit.disable_compositing_mode` | bool | `false` | - | Disable WebKit fallback compositing mode (`WEBKIT_DISABLE_COMPOSITING_MODE`) |
272+| `engine.webkit.disable_mipmaps` | bool | `false` | - | Disable GTK mipmaps for the WebKit fallback (`GSK_GPU_DISABLE=mipmap`) |
273+| `engine.webkit.prefer_gl` | bool | `false` | - | Prefer OpenGL over GLES for the WebKit fallback (`GDK_DEBUG=gl-prefer-gl`) |
274+| `engine.webkit.draw_compositing_indicators` | bool | `false` | - | Draw WebKit fallback compositing indicators (debug) |
275+| `engine.webkit.show_fps` | bool | `false` | - | Show WebKit fallback FPS counter (`WEBKIT_SHOW_FPS`) |
276+| `engine.webkit.sample_memory` | bool | `false` | - | Enable WebKit fallback memory sampling (`WEBKIT_SAMPLE_MEMORY`) |
277+| `engine.webkit.debug_frames` | bool | `false` | - | Enable GTK frame timing debug for the WebKit fallback (`GDK_DEBUG=frames`) |
278+| `engine.webkit.force_vsync` | bool | `false` | - | Force VSync for WebKit fallback video playback |
279+| `engine.webkit.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | WebKit fallback GStreamer/OpenGL API selection |
280+| `engine.webkit.gstreamer_debug_level` | int | `0` | `0-5` | WebKit fallback GStreamer debug verbosity |
281+| `default_ui_scale` | float | `1.0` | `> 0` | GTK widget UI scale (1.0=100%, 2.0=200%) |
282+| `default_webpage_zoom` | float | `1.2` | `> 0` | Default page zoom (1.0=100%, 1.2=120%) |
278283
279284 `engine.cef.input.scroll_precise_multiplier` controls touchpad/high-resolution wheel scroll speed, and `engine.cef.input.scroll_vertical_multiplier` applies an additional vertical-only scale. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
280285
281286 `engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `200.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.
282-| `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |
287+
288+### Legacy key migration
289+
290+Existing configs using older keys are migrated to the current engine shape. New configs should use the canonical keys above.
291+
292+| Legacy key | Current key |
293+|------------|-------------|
294+| `privacy.cookie_policy` | `engine.cookie_policy` |
295+| `privacy.itp_enabled` | `engine.webkit.itp_enabled` |
296+| `rendering.disable_dmabuf_renderer` | `engine.webkit.disable_dmabuf_renderer` |
297+| `rendering.force_compositing_mode` | `engine.webkit.force_compositing_mode` |
298+| `rendering.disable_compositing_mode` | `engine.webkit.disable_compositing_mode` |
299+| `rendering.gsk_renderer` | `engine.webkit.gsk_renderer` |
300+| `rendering.disable_mipmaps` | `engine.webkit.disable_mipmaps` |
301+| `rendering.prefer_gl` | `engine.webkit.prefer_gl` |
302+| `rendering.draw_compositing_indicators` | `engine.webkit.draw_compositing_indicators` |
303+| `rendering.show_fps` | `engine.webkit.show_fps` |
304+| `rendering.sample_memory` | `engine.webkit.sample_memory` |
305+| `rendering.debug_frames` | `engine.webkit.debug_frames` |
306+| `media.force_vsync` | `engine.webkit.force_vsync` |
307+| `media.gl_rendering_mode` | `engine.webkit.gl_rendering_mode` |
308+| `media.gstreamer_debug_level` | `engine.webkit.gstreamer_debug_level` |
309+| `runtime.prefix` | `engine.webkit.prefix` |
310+| `performance.*` | `engine.profile`, `engine.pool_prewarm_count`, `engine.zoom_cache_size`, or matching `engine.webkit.*` custom tuning keys |
311+| `rendering.mode` | Dropped; no current setting |
283312
284313 ## Workspace Configuration
285314
@@ -548,21 +577,14 @@ max_exited_session_age_days = 7 # Delete sessions older than 7 days on startup
548577 | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode |
549578 | `media.prefer_av1` | bool | `false` | - | Prefer AV1 codec when available |
550579 | `media.show_diagnostics` | bool | `false` | - | Show media diagnostics warnings at startup |
551-| `media.force_vsync` | bool | `false` | - | Force VSync for video playback (may help with tearing) |
552-| `media.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | OpenGL API selection for video rendering |
553-| `media.gstreamer_debug_level` | int | `0` | `0-5` | GStreamer debug verbosity (0=off) |
580+
581+WebKit fallback GStreamer tuning is configured under `engine.webkit.force_vsync`, `engine.webkit.gl_rendering_mode`, and `engine.webkit.gstreamer_debug_level`.
554582
555583 **Hardware decoding modes:**
556584 - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000
557585 - `force`: Hardware only - fails if unavailable
558586 - `disable`: Software only - higher CPU usage
559587
560-**GL rendering modes:**
561-- `auto` (default): Let GStreamer choose the best OpenGL API
562-- `gles2`: Force GLES2 (better for some mobile/embedded GPUs)
563-- `gl3`: Force OpenGL 3.x desktop
564-- `none`: Disable GL-based rendering
565-
566588 **GPU auto-detection:**
567589 Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal VA-API driver settings:
568590 - **AMD**: Uses `radeonsi` driver
@@ -575,21 +597,15 @@ Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal
575597 hardware_decoding = "auto" # HW preferred, SW fallback
576598 prefer_av1 = false # Let site choose codec
577599 show_diagnostics = false # Keep off for daily use
578-force_vsync = false # Let compositor handle VSync
579-gl_rendering_mode = "auto" # GStreamer picks best GL API
580-gstreamer_debug_level = 0 # Increase to 3-5 for debugging
581600 ```
582601
583-**Diagnostic mode toggle path:**
584-
602+**WebKit fallback GStreamer diagnostics:**
585603 ```toml
586-# ~/.config/dumber/config.toml
587-[media]
588-show_diagnostics = true
604+[engine.webkit]
589605 gstreamer_debug_level = 3
590606 ```
591607
592-Set `show_diagnostics = false` and `gstreamer_debug_level = 0` to return to normal mode.
608+Set `media.show_diagnostics = false` and `engine.webkit.gstreamer_debug_level = 0` to return to normal mode.
593609
594610 **Diagnostics CLI:**
595611 ```bash
@@ -601,9 +617,10 @@ dumber doctor --media
601617
602618 | Key | Type | Default | Description |
603619 |-----|------|---------|-------------|
604-| `runtime.prefix` | string | `""` | Optional runtime prefix (e.g., `/opt/webkitgtk`) used to locate newer GTK/WebKitGTK installs |
620+| `engine.cef.cef_dir` | string | `""` | Optional CEF runtime directory containing `libcef.so` and resources |
621+| `engine.webkit.prefix` | string | `""` | Optional WebKitGTK fallback runtime prefix (e.g., `/opt/webkitgtk`) |
605622
606-If set, dumber prepends prefix-derived paths to:
623+`engine.cef.cef_dir` overrides CEF runtime discovery. For the WebKitGTK fallback, `engine.webkit.prefix` prepends prefix-derived paths to:
607624 - `PKG_CONFIG_PATH` (for version checks)
608625 - `LD_LIBRARY_PATH` (for runtime library loading)
609626 - `GI_TYPELIB_PATH` (GObject introspection)
@@ -611,7 +628,10 @@ If set, dumber prepends prefix-derived paths to:
611628
612629 **Example:**
613630 ```toml
614-[runtime]
631+[engine.cef]
632+cef_dir = "/opt/cef"
633+
634+[engine.webkit]
615635 prefix = "/opt/webkitgtk"
616636 ```
617637
@@ -670,89 +690,69 @@ dumber config migrate --yes
670690
671691 ## Performance Profiles
672692
673-Performance profiles provide preset configurations for WebKitGTK tuning. These settings affect Skia rendering threads, memory pressure handling, and WebView pool behavior.
693+Performance profiles tune engine resource behavior. CEF is the default engine; the `engine.webkit.*` custom fields below apply only when using the WebKitGTK fallback.
674694
675695 > **Note:** Performance settings are applied at browser startup. Changes require a restart to take effect.
676696
677697 | Key | Type | Default | Valid Values | Description |
678698 |-----|------|---------|--------------|-------------|
679-| `performance.profile` | string | `"default"` | `default`, `lite`, `max`, `custom` | Performance profile selection |
699+| `engine.profile` | string | `"default"` | `default`, `lite`, `balanced`, `max`, `custom` | Performance profile selection |
700+| `engine.pool_prewarm_count` | int | `4` | `>= 0` | WebViews to pre-create at startup |
701+| `engine.zoom_cache_size` | int | `256` | `>= 0` | Domain zoom levels to cache |
680702
681703 ### Profiles
682704
683705 | Profile | Description | Use Case |
684706 |---------|-------------|----------|
685-| `default` | No tuning, uses WebKit defaults | Compatibility baseline, troubleshooting |
707+| `default` | No tuning, uses engine defaults | Compatibility baseline, troubleshooting |
686708 | `lite` | Reduced resource usage | Low-RAM systems (< 4GB), battery saving |
709+| `balanced` | Moderate tuning | General use |
687710 | `max` | Maximum responsiveness | Heavy pages (GitHub PRs, complex SPAs), high-end systems |
688711 | `custom` | Manual control over all settings | Advanced users who want fine-grained control |
689712
690-### Profile Settings Matrix
691-
692-| Setting | default | lite | max |
693-|---------|---------|------|-----|
694-| Skia CPU threads | unset | 2 | `NumCPU()/2` (min 4) |
695-| Skia GPU threads | unset | unset | scales with VRAM |
696-| Web process memory (MB) | unset | 768 | unset |
697-| Network process memory (MB) | unset | 384 | unset |
698-| Conservative threshold | unset | 0.25 | unset |
699-| Strict threshold | unset | 0.4 | unset |
700-| WebView pool prewarm | 4 | 2 | scales with RAM |
701-
702713 **Example:**
703714 ```toml
704-[performance]
705-profile = "default" # WebKit baseline default
706-
707-# For WebKit baseline behavior:
708-# profile = "default"
709-
710-# For heavy pages (GitHub PRs, complex web apps):
711-# profile = "max"
712-
713-# For manual control:
714-# profile = "custom"
715+[engine]
716+profile = "default"
717+pool_prewarm_count = 4
718+zoom_cache_size = 256
715719 ```
716720
717-### Custom Profile Settings
721+### WebKit fallback custom profile settings
718722
719-When `profile = "custom"`, you can configure individual tuning options:
723+When `engine.profile = "custom"` and `engine.type = "webkit"`, you can configure individual WebKit tuning options:
720724
721725 | Key | Type | Default | Description |
722726 |-----|------|---------|-------------|
723-| `performance.skia_cpu_painting_threads` | int | `0` | Skia CPU rendering threads (0=unset) |
724-| `performance.skia_gpu_painting_threads` | int | `-1` | Skia GPU rendering threads (-1=unset, 0=disable) |
725-| `performance.skia_enable_cpu_rendering` | bool | `false` | Force CPU rendering |
726-| `performance.web_process_memory_limit_mb` | int | `0` | Web process memory limit in MB (0=unset) |
727-| `performance.web_process_memory_poll_interval_sec` | float | `0` | Memory check interval (0=WebKit default: 30s) |
728-| `performance.web_process_memory_conservative_threshold` | float | `0` | Conservative cleanup threshold (0=unset) |
729-| `performance.web_process_memory_strict_threshold` | float | `0` | Strict cleanup threshold (0=unset) |
730-| `performance.network_process_memory_limit_mb` | int | `0` | Network process memory limit in MB |
731-| `performance.network_process_memory_poll_interval_sec` | float | `0` | Network memory check interval |
732-| `performance.network_process_memory_conservative_threshold` | float | `0` | Network conservative threshold |
733-| `performance.network_process_memory_strict_threshold` | float | `0` | Network strict threshold |
734-| `performance.webview_pool_prewarm_count` | int | `4` | WebViews to pre-create at startup |
735-| `performance.zoom_cache_size` | int | `256` | Domain zoom levels to cache |
736-
737-**Custom profile example:**
727+| `engine.webkit.skia_cpu_painting_threads` | int | `0` | Skia CPU rendering threads (0=unset) |
728+| `engine.webkit.skia_gpu_painting_threads` | int | `-1` | Skia GPU rendering threads (-1=unset, 0=disable) |
729+| `engine.webkit.skia_enable_cpu_rendering` | bool | `false` | Force CPU rendering |
730+| `engine.webkit.web_process_memory_limit_mb` | int | `0` | Web process memory limit in MB (0=unset) |
731+| `engine.webkit.web_process_memory_poll_interval_sec` | float | `0` | Memory check interval (0=WebKit default: 30s) |
732+| `engine.webkit.web_process_memory_conservative_threshold` | float | `0` | Conservative cleanup threshold (0=unset) |
733+| `engine.webkit.web_process_memory_strict_threshold` | float | `0` | Strict cleanup threshold (0=unset) |
734+| `engine.webkit.network_process_memory_limit_mb` | int | `0` | Network process memory limit in MB |
735+| `engine.webkit.network_process_memory_poll_interval_sec` | float | `0` | Network memory check interval |
736+| `engine.webkit.network_process_memory_conservative_threshold` | float | `0` | Network conservative threshold |
737+| `engine.webkit.network_process_memory_strict_threshold` | float | `0` | Network strict threshold |
738+
739+**Custom WebKit fallback example:**
738740 ```toml
739-[performance]
741+[engine]
742+type = "webkit"
740743 profile = "custom"
744+pool_prewarm_count = 6
745+zoom_cache_size = 256
741746
742-# Skia threading - tune for your CPU
747+[engine.webkit]
743748 skia_cpu_painting_threads = 4
744749 skia_gpu_painting_threads = 2
745-
746-# Web process memory pressure
747750 web_process_memory_limit_mb = 1024
748751 web_process_memory_conservative_threshold = 0.4
749752 web_process_memory_strict_threshold = 0.6
750-
751-# WebView pool
752-webview_pool_prewarm_count = 6
753753 ```
754754
755-> **Important:** Individual tuning fields are ignored unless `profile = "custom"`. Setting individual fields with any other profile will produce a validation warning.
755+> **Important:** Individual `engine.webkit.*` tuning fields are ignored unless `engine.profile = "custom"`.
756756
757757 ## Downloads
758758
@@ -779,14 +779,14 @@ All config values can be overridden via environment variables with the prefix `D
779779 # Database
780780 DUMBER_DATABASE_PATH=/custom/path/db.sqlite
781781
782-# Rendering (examples)
782+# Engine/rendering (examples)
783+DUMBER_ENGINE_TYPE=cef
783784 # CEF defaults to the GPU-first Vulkan DMABUF stack.
784785 # Use EGL/OpenGL for driver compatibility:
785786 # DUMBER_ENGINE_CEF_RENDER_STACK=egl
786-DUMBER_RENDERING_MODE=cpu
787-# WebKit-specific rendering overrides:
788-# DUMBER_RENDERING_DISABLE_DMABUF_RENDERER=true
789-# DUMBER_RENDERING_GSK_RENDERER=opengl
787+# WebKit fallback rendering overrides:
788+# DUMBER_ENGINE_WEBKIT_DISABLE_DMABUF_RENDERER=true
789+# DUMBER_ENGINE_WEBKIT_GSK_RENDERER=opengl
790790 DUMBER_DEFAULT_WEBPAGE_ZOOM=1.5
791791
792792 # Logging
@@ -248,38 +248,67 @@ When Noctalia rewrites the watched file, Dumber reapplies the same resolved them
248 248
249 ## Rendering, UI Scale & Zoom249 ## Rendering, UI Scale & Zoom
250 250
251+CEF is the default browser engine. WebKitGTK remains available as a fallback via `engine.type = "webkit"`; `engine.webkit.*` settings only affect that fallback engine.
252+
251 | Key | Type | Default | Valid Values | Description |253 | Key | Type | Default | Valid Values | Description |
252 |-----|------|---------|--------------|-------------|254 |-----|------|---------|--------------|-------------|
253-| `rendering.mode` | string | `"gpu"` | `auto`, `gpu`, `cpu` | WebKit hardware acceleration policy |255+| `engine.type` | string | `"cef"` | `cef`, `webkit` | Browser engine selection; WebKitGTK is the fallback option |
254-| `rendering.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit DMA-BUF renderer (may fix flicker on Wayland; slower) |
255-| `rendering.force_compositing_mode` | bool | `false` | - | Force WebKit compositing mode (`WEBKIT_FORCE_COMPOSITING_MODE`) |
256-| `rendering.disable_compositing_mode` | bool | `false` | - | Disable WebKit compositing mode (`WEBKIT_DISABLE_COMPOSITING_MODE`) |
257-| `rendering.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | GTK renderer selection (`GSK_RENDERER`) |
258-| `rendering.disable_mipmaps` | bool | `false` | - | Disable GTK mipmaps (`GSK_GPU_DISABLE=mipmap`) |
259-| `rendering.prefer_gl` | bool | `false` | - | Prefer OpenGL over GLES (`GDK_DEBUG=gl-prefer-gl`) |
260-| `rendering.draw_compositing_indicators` | bool | `false` | - | Draw WebKit compositing indicators (debug) |
261-| `rendering.show_fps` | bool | `false` | - | Show WebKit FPS counter (`WEBKIT_SHOW_FPS`) |
262-| `rendering.sample_memory` | bool | `false` | - | Enable WebKit memory sampling (`WEBKIT_SAMPLE_MEMORY`) |
263-| `rendering.debug_frames` | bool | `false` | - | Enable GTK frame timing debug (`GDK_DEBUG=frames`) |
264-| `engine.type` | string | `"cef"` | `cef`, `webkit` | Browser engine. WebKit remains available as a fallback. |
265 | `engine.cef.render_stack` | string | `"vulkan"` | `vulkan`, `egl` | CEF GPU render stack |256 | `engine.cef.render_stack` | string | `"vulkan"` | `vulkan`, `egl` | CEF GPU render stack |
266-| `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | - | Enable adaptive CEF OSR FPS polling (only applies when `windowless_frame_rate = 0`) |257+| `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | - | Enable adaptive CEF OSR FPS polling when `windowless_frame_rate = 0` |
267-| `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled, otherwise CEF default |258+| `engine.cef.windowless_frame_rate` | int32 | `0` | `>= 0` | Explicit CEF OSR FPS cap; 0 uses adaptive mode if enabled |
268-| `engine.cef.windowless_frame_rate_max` | int32 | `240` | >= 0 | Hard cap for adaptive CEF OSR FPS; 0 falls back to the built-in default cap selected by Dumber/CEF |259+| `engine.cef.windowless_frame_rate_max` | int32 | `240` | `>= 0` | Hard cap for adaptive CEF OSR FPS; 0 uses the built-in cap |
269-| `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | > 0 | Mouse wheel scroll sensitivity multiplier |260+| `engine.cef.input.scroll_wheel_multiplier` | float | `1.0` | `> 0` | Mouse wheel scroll sensitivity multiplier |
270-| `engine.cef.input.scroll_precise_multiplier` | float | `2.5` | > 0 | Precise/surface scroll sensitivity multiplier for touchpads and high-resolution wheels |261+| `engine.cef.input.scroll_precise_multiplier` | float | `2.5` | `> 0` | Precise/surface scroll sensitivity multiplier for touchpads and high-resolution wheels |
271-| `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | > 0 | Horizontal scroll sensitivity multiplier |262+| `engine.cef.input.scroll_horizontal_multiplier` | float | `1.0` | `> 0` | Horizontal scroll sensitivity multiplier |
272-| `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | > 0 | Vertical scroll sensitivity multiplier; touchpad vertical speed uses this together with `scroll_precise_multiplier` |263+| `engine.cef.input.scroll_vertical_multiplier` | float | `1.0` | `> 0` | Vertical scroll sensitivity multiplier; combines with `scroll_precise_multiplier` for touchpads |
273-| `engine.cef.input.scroll_max_delta` | int32 | `0` | >= 0 | Maximum absolute scroll delta after scaling; 0 disables clamping |264+| `engine.cef.input.scroll_max_delta` | int32 | `0` | `>= 0` | Maximum absolute scroll delta after scaling; 0 disables clamping |
274 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |265 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | - | Enable two-finger touchpad swipe back/forward navigation |
275-| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 | Minimum accumulated horizontal swipe delta required for navigation |266+| `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | `> 0` | Minimum accumulated horizontal swipe delta required for navigation |
276-| `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes; does not affect vertical scroll speed |267+| `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | `> 0` | Maximum vertical-to-horizontal delta ratio allowed for navigation swipes |
277-| `default_ui_scale` | float | `1.0` | > 0 | GTK widget UI scale (1.0=100%, 2.0=200%) |268+| `engine.webkit.gsk_renderer` | string | `"auto"` | `auto`, `opengl`, `vulkan`, `cairo` | WebKit fallback GTK renderer selection (`GSK_RENDERER`) |
269+| `engine.webkit.disable_dmabuf_renderer` | bool | `false` | - | Disable WebKit fallback DMA-BUF renderer |
270+| `engine.webkit.force_compositing_mode` | bool | `false` | - | Force WebKit fallback compositing mode (`WEBKIT_FORCE_COMPOSITING_MODE`) |
271+| `engine.webkit.disable_compositing_mode` | bool | `false` | - | Disable WebKit fallback compositing mode (`WEBKIT_DISABLE_COMPOSITING_MODE`) |
272+| `engine.webkit.disable_mipmaps` | bool | `false` | - | Disable GTK mipmaps for the WebKit fallback (`GSK_GPU_DISABLE=mipmap`) |
273+| `engine.webkit.prefer_gl` | bool | `false` | - | Prefer OpenGL over GLES for the WebKit fallback (`GDK_DEBUG=gl-prefer-gl`) |
274+| `engine.webkit.draw_compositing_indicators` | bool | `false` | - | Draw WebKit fallback compositing indicators (debug) |
275+| `engine.webkit.show_fps` | bool | `false` | - | Show WebKit fallback FPS counter (`WEBKIT_SHOW_FPS`) |
276+| `engine.webkit.sample_memory` | bool | `false` | - | Enable WebKit fallback memory sampling (`WEBKIT_SAMPLE_MEMORY`) |
277+| `engine.webkit.debug_frames` | bool | `false` | - | Enable GTK frame timing debug for the WebKit fallback (`GDK_DEBUG=frames`) |
278+| `engine.webkit.force_vsync` | bool | `false` | - | Force VSync for WebKit fallback video playback |
279+| `engine.webkit.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | WebKit fallback GStreamer/OpenGL API selection |
280+| `engine.webkit.gstreamer_debug_level` | int | `0` | `0-5` | WebKit fallback GStreamer debug verbosity |
281+| `default_ui_scale` | float | `1.0` | `> 0` | GTK widget UI scale (1.0=100%, 2.0=200%) |
282+| `default_webpage_zoom` | float | `1.2` | `> 0` | Default page zoom (1.0=100%, 1.2=120%) |
278 283
279 `engine.cef.input.scroll_precise_multiplier` controls touchpad/high-resolution wheel scroll speed, and `engine.cef.input.scroll_vertical_multiplier` applies an additional vertical-only scale. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.284 `engine.cef.input.scroll_precise_multiplier` controls touchpad/high-resolution wheel scroll speed, and `engine.cef.input.scroll_vertical_multiplier` applies an additional vertical-only scale. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
280 285
281 `engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `200.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.286 `engine.cef.input.touchpad_navigation_min_delta` uses raw GTK touchpad surface units for back/forward gestures. The default `200.0` matches WebKit-style commit distance to reduce accidental navigation; raise or lower it in `config.toml` to tune gesture sensitivity.
282-| `default_webpage_zoom` | float | `1.2` | > 0 | Default page zoom (1.0=100%, 1.2=120%) |287+
288+### Legacy key migration
289+
290+Existing configs using older keys are migrated to the current engine shape. New configs should use the canonical keys above.
291+
292+| Legacy key | Current key |
293+|------------|-------------|
294+| `privacy.cookie_policy` | `engine.cookie_policy` |
295+| `privacy.itp_enabled` | `engine.webkit.itp_enabled` |
296+| `rendering.disable_dmabuf_renderer` | `engine.webkit.disable_dmabuf_renderer` |
297+| `rendering.force_compositing_mode` | `engine.webkit.force_compositing_mode` |
298+| `rendering.disable_compositing_mode` | `engine.webkit.disable_compositing_mode` |
299+| `rendering.gsk_renderer` | `engine.webkit.gsk_renderer` |
300+| `rendering.disable_mipmaps` | `engine.webkit.disable_mipmaps` |
301+| `rendering.prefer_gl` | `engine.webkit.prefer_gl` |
302+| `rendering.draw_compositing_indicators` | `engine.webkit.draw_compositing_indicators` |
303+| `rendering.show_fps` | `engine.webkit.show_fps` |
304+| `rendering.sample_memory` | `engine.webkit.sample_memory` |
305+| `rendering.debug_frames` | `engine.webkit.debug_frames` |
306+| `media.force_vsync` | `engine.webkit.force_vsync` |
307+| `media.gl_rendering_mode` | `engine.webkit.gl_rendering_mode` |
308+| `media.gstreamer_debug_level` | `engine.webkit.gstreamer_debug_level` |
309+| `runtime.prefix` | `engine.webkit.prefix` |
310+| `performance.*` | `engine.profile`, `engine.pool_prewarm_count`, `engine.zoom_cache_size`, or matching `engine.webkit.*` custom tuning keys |
311+| `rendering.mode` | Dropped; no current setting |
283 312
284 ## Workspace Configuration313 ## Workspace Configuration
285 314
@@ -548,21 +577,14 @@ max_exited_session_age_days = 7 # Delete sessions older than 7 days on startup
548 | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode |577 | `media.hardware_decoding` | string | `"auto"` | `auto`, `force`, `disable` | Hardware video decoding mode |
549 | `media.prefer_av1` | bool | `false` | - | Prefer AV1 codec when available |578 | `media.prefer_av1` | bool | `false` | - | Prefer AV1 codec when available |
550 | `media.show_diagnostics` | bool | `false` | - | Show media diagnostics warnings at startup |579 | `media.show_diagnostics` | bool | `false` | - | Show media diagnostics warnings at startup |
551-| `media.force_vsync` | bool | `false` | - | Force VSync for video playback (may help with tearing) |580+
552-| `media.gl_rendering_mode` | string | `"auto"` | `auto`, `gles2`, `gl3`, `none` | OpenGL API selection for video rendering |581+WebKit fallback GStreamer tuning is configured under `engine.webkit.force_vsync`, `engine.webkit.gl_rendering_mode`, and `engine.webkit.gstreamer_debug_level`.
553-| `media.gstreamer_debug_level` | int | `0` | `0-5` | GStreamer debug verbosity (0=off) |
554 582
555 **Hardware decoding modes:**583 **Hardware decoding modes:**
556 - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000584 - `auto` (recommended): Hardware preferred with software fallback - fixes Twitch Error #4000
557 - `force`: Hardware only - fails if unavailable585 - `force`: Hardware only - fails if unavailable
558 - `disable`: Software only - higher CPU usage586 - `disable`: Software only - higher CPU usage
559 587
560-**GL rendering modes:**
561-- `auto` (default): Let GStreamer choose the best OpenGL API
562-- `gles2`: Force GLES2 (better for some mobile/embedded GPUs)
563-- `gl3`: Force OpenGL 3.x desktop
564-- `none`: Disable GL-based rendering
565-
566 **GPU auto-detection:**588 **GPU auto-detection:**
567 Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal VA-API driver settings:589 Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal VA-API driver settings:
568 - **AMD**: Uses `radeonsi` driver590 - **AMD**: Uses `radeonsi` driver
@@ -575,21 +597,15 @@ Dumber automatically detects your GPU vendor (AMD/Intel/NVIDIA) and sets optimal
575 hardware_decoding = "auto" # HW preferred, SW fallback597 hardware_decoding = "auto" # HW preferred, SW fallback
576 prefer_av1 = false # Let site choose codec598 prefer_av1 = false # Let site choose codec
577 show_diagnostics = false # Keep off for daily use599 show_diagnostics = false # Keep off for daily use
578-force_vsync = false # Let compositor handle VSync
579-gl_rendering_mode = "auto" # GStreamer picks best GL API
580-gstreamer_debug_level = 0 # Increase to 3-5 for debugging
581 ```600 ```
582 601
583-**Diagnostic mode toggle path:**602+**WebKit fallback GStreamer diagnostics:**
584-
585 ```toml603 ```toml
586-# ~/.config/dumber/config.toml604+[engine.webkit]
587-[media]
588-show_diagnostics = true
589 gstreamer_debug_level = 3605 gstreamer_debug_level = 3
590 ```606 ```
591 607
592-Set `show_diagnostics = false` and `gstreamer_debug_level = 0` to return to normal mode.608+Set `media.show_diagnostics = false` and `engine.webkit.gstreamer_debug_level = 0` to return to normal mode.
593 609
594 **Diagnostics CLI:**610 **Diagnostics CLI:**
595 ```bash611 ```bash
@@ -601,9 +617,10 @@ dumber doctor --media
601 617
602 | Key | Type | Default | Description |618 | Key | Type | Default | Description |
603 |-----|------|---------|-------------|619 |-----|------|---------|-------------|
604-| `runtime.prefix` | string | `""` | Optional runtime prefix (e.g., `/opt/webkitgtk`) used to locate newer GTK/WebKitGTK installs |620+| `engine.cef.cef_dir` | string | `""` | Optional CEF runtime directory containing `libcef.so` and resources |
621+| `engine.webkit.prefix` | string | `""` | Optional WebKitGTK fallback runtime prefix (e.g., `/opt/webkitgtk`) |
605 622
606-If set, dumber prepends prefix-derived paths to:623+`engine.cef.cef_dir` overrides CEF runtime discovery. For the WebKitGTK fallback, `engine.webkit.prefix` prepends prefix-derived paths to:
607 - `PKG_CONFIG_PATH` (for version checks)624 - `PKG_CONFIG_PATH` (for version checks)
608 - `LD_LIBRARY_PATH` (for runtime library loading)625 - `LD_LIBRARY_PATH` (for runtime library loading)
609 - `GI_TYPELIB_PATH` (GObject introspection)626 - `GI_TYPELIB_PATH` (GObject introspection)
@@ -611,7 +628,10 @@ If set, dumber prepends prefix-derived paths to:
611 628
612 **Example:**629 **Example:**
613 ```toml630 ```toml
614-[runtime]631+[engine.cef]
632+cef_dir = "/opt/cef"
633+
634+[engine.webkit]
615 prefix = "/opt/webkitgtk"635 prefix = "/opt/webkitgtk"
616 ```636 ```
617 637
@@ -670,89 +690,69 @@ dumber config migrate --yes
670 690
671 ## Performance Profiles691 ## Performance Profiles
672 692
673-Performance profiles provide preset configurations for WebKitGTK tuning. These settings affect Skia rendering threads, memory pressure handling, and WebView pool behavior.693+Performance profiles tune engine resource behavior. CEF is the default engine; the `engine.webkit.*` custom fields below apply only when using the WebKitGTK fallback.
674 694
675 > **Note:** Performance settings are applied at browser startup. Changes require a restart to take effect.695 > **Note:** Performance settings are applied at browser startup. Changes require a restart to take effect.
676 696
677 | Key | Type | Default | Valid Values | Description |697 | Key | Type | Default | Valid Values | Description |
678 |-----|------|---------|--------------|-------------|698 |-----|------|---------|--------------|-------------|
679-| `performance.profile` | string | `"default"` | `default`, `lite`, `max`, `custom` | Performance profile selection |699+| `engine.profile` | string | `"default"` | `default`, `lite`, `balanced`, `max`, `custom` | Performance profile selection |
700+| `engine.pool_prewarm_count` | int | `4` | `>= 0` | WebViews to pre-create at startup |
701+| `engine.zoom_cache_size` | int | `256` | `>= 0` | Domain zoom levels to cache |
680 702
681 ### Profiles703 ### Profiles
682 704
683 | Profile | Description | Use Case |705 | Profile | Description | Use Case |
684 |---------|-------------|----------|706 |---------|-------------|----------|
685-| `default` | No tuning, uses WebKit defaults | Compatibility baseline, troubleshooting |707+| `default` | No tuning, uses engine defaults | Compatibility baseline, troubleshooting |
686 | `lite` | Reduced resource usage | Low-RAM systems (< 4GB), battery saving |708 | `lite` | Reduced resource usage | Low-RAM systems (< 4GB), battery saving |
709+| `balanced` | Moderate tuning | General use |
687 | `max` | Maximum responsiveness | Heavy pages (GitHub PRs, complex SPAs), high-end systems |710 | `max` | Maximum responsiveness | Heavy pages (GitHub PRs, complex SPAs), high-end systems |
688 | `custom` | Manual control over all settings | Advanced users who want fine-grained control |711 | `custom` | Manual control over all settings | Advanced users who want fine-grained control |
689 712
690-### Profile Settings Matrix
691-
692-| Setting | default | lite | max |
693-|---------|---------|------|-----|
694-| Skia CPU threads | unset | 2 | `NumCPU()/2` (min 4) |
695-| Skia GPU threads | unset | unset | scales with VRAM |
696-| Web process memory (MB) | unset | 768 | unset |
697-| Network process memory (MB) | unset | 384 | unset |
698-| Conservative threshold | unset | 0.25 | unset |
699-| Strict threshold | unset | 0.4 | unset |
700-| WebView pool prewarm | 4 | 2 | scales with RAM |
701-
702 **Example:**713 **Example:**
703 ```toml714 ```toml
704-[performance]715+[engine]
705-profile = "default" # WebKit baseline default716+profile = "default"
706-717+pool_prewarm_count = 4
707-# For WebKit baseline behavior:718+zoom_cache_size = 256
708-# profile = "default"
709-
710-# For heavy pages (GitHub PRs, complex web apps):
711-# profile = "max"
712-
713-# For manual control:
714-# profile = "custom"
715 ```719 ```
716 720
717-### Custom Profile Settings721+### WebKit fallback custom profile settings
718 722
719-When `profile = "custom"`, you can configure individual tuning options:723+When `engine.profile = "custom"` and `engine.type = "webkit"`, you can configure individual WebKit tuning options:
720 724
721 | Key | Type | Default | Description |725 | Key | Type | Default | Description |
722 |-----|------|---------|-------------|726 |-----|------|---------|-------------|
723-| `performance.skia_cpu_painting_threads` | int | `0` | Skia CPU rendering threads (0=unset) |727+| `engine.webkit.skia_cpu_painting_threads` | int | `0` | Skia CPU rendering threads (0=unset) |
724-| `performance.skia_gpu_painting_threads` | int | `-1` | Skia GPU rendering threads (-1=unset, 0=disable) |728+| `engine.webkit.skia_gpu_painting_threads` | int | `-1` | Skia GPU rendering threads (-1=unset, 0=disable) |
725-| `performance.skia_enable_cpu_rendering` | bool | `false` | Force CPU rendering |729+| `engine.webkit.skia_enable_cpu_rendering` | bool | `false` | Force CPU rendering |
726-| `performance.web_process_memory_limit_mb` | int | `0` | Web process memory limit in MB (0=unset) |730+| `engine.webkit.web_process_memory_limit_mb` | int | `0` | Web process memory limit in MB (0=unset) |
727-| `performance.web_process_memory_poll_interval_sec` | float | `0` | Memory check interval (0=WebKit default: 30s) |731+| `engine.webkit.web_process_memory_poll_interval_sec` | float | `0` | Memory check interval (0=WebKit default: 30s) |
728-| `performance.web_process_memory_conservative_threshold` | float | `0` | Conservative cleanup threshold (0=unset) |732+| `engine.webkit.web_process_memory_conservative_threshold` | float | `0` | Conservative cleanup threshold (0=unset) |
729-| `performance.web_process_memory_strict_threshold` | float | `0` | Strict cleanup threshold (0=unset) |733+| `engine.webkit.web_process_memory_strict_threshold` | float | `0` | Strict cleanup threshold (0=unset) |
730-| `performance.network_process_memory_limit_mb` | int | `0` | Network process memory limit in MB |734+| `engine.webkit.network_process_memory_limit_mb` | int | `0` | Network process memory limit in MB |
731-| `performance.network_process_memory_poll_interval_sec` | float | `0` | Network memory check interval |735+| `engine.webkit.network_process_memory_poll_interval_sec` | float | `0` | Network memory check interval |
732-| `performance.network_process_memory_conservative_threshold` | float | `0` | Network conservative threshold |736+| `engine.webkit.network_process_memory_conservative_threshold` | float | `0` | Network conservative threshold |
733-| `performance.network_process_memory_strict_threshold` | float | `0` | Network strict threshold |737+| `engine.webkit.network_process_memory_strict_threshold` | float | `0` | Network strict threshold |
734-| `performance.webview_pool_prewarm_count` | int | `4` | WebViews to pre-create at startup |738+
735-| `performance.zoom_cache_size` | int | `256` | Domain zoom levels to cache |739+**Custom WebKit fallback example:**
736-
737-**Custom profile example:**
738 ```toml740 ```toml
739-[performance]741+[engine]
742+type = "webkit"
740 profile = "custom"743 profile = "custom"
744+pool_prewarm_count = 6
745+zoom_cache_size = 256
741 746
742-# Skia threading - tune for your CPU747+[engine.webkit]
743 skia_cpu_painting_threads = 4748 skia_cpu_painting_threads = 4
744 skia_gpu_painting_threads = 2749 skia_gpu_painting_threads = 2
745-
746-# Web process memory pressure
747 web_process_memory_limit_mb = 1024750 web_process_memory_limit_mb = 1024
748 web_process_memory_conservative_threshold = 0.4751 web_process_memory_conservative_threshold = 0.4
749 web_process_memory_strict_threshold = 0.6752 web_process_memory_strict_threshold = 0.6
750-
751-# WebView pool
752-webview_pool_prewarm_count = 6
753 ```753 ```
754 754
755-> **Important:** Individual tuning fields are ignored unless `profile = "custom"`. Setting individual fields with any other profile will produce a validation warning.755+> **Important:** Individual `engine.webkit.*` tuning fields are ignored unless `engine.profile = "custom"`.
756 756
757 ## Downloads757 ## Downloads
758 758
@@ -779,14 +779,14 @@ All config values can be overridden via environment variables with the prefix `D
779 # Database779 # Database
780 DUMBER_DATABASE_PATH=/custom/path/db.sqlite780 DUMBER_DATABASE_PATH=/custom/path/db.sqlite
781 781
782-# Rendering (examples)782+# Engine/rendering (examples)
783+DUMBER_ENGINE_TYPE=cef
783 # CEF defaults to the GPU-first Vulkan DMABUF stack.784 # CEF defaults to the GPU-first Vulkan DMABUF stack.
784 # Use EGL/OpenGL for driver compatibility:785 # Use EGL/OpenGL for driver compatibility:
785 # DUMBER_ENGINE_CEF_RENDER_STACK=egl786 # DUMBER_ENGINE_CEF_RENDER_STACK=egl
786-DUMBER_RENDERING_MODE=cpu787+# WebKit fallback rendering overrides:
787-# WebKit-specific rendering overrides:788+# DUMBER_ENGINE_WEBKIT_DISABLE_DMABUF_RENDERER=true
788-# DUMBER_RENDERING_DISABLE_DMABUF_RENDERER=true789+# DUMBER_ENGINE_WEBKIT_GSK_RENDERER=opengl
789-# DUMBER_RENDERING_GSK_RENDERER=opengl
790 DUMBER_DEFAULT_WEBPAGE_ZOOM=1.5790 DUMBER_DEFAULT_WEBPAGE_ZOOM=1.5
791 791
792 # Logging792 # Logging
M docs/reference/configuration.md +55 -30
@@ -52,18 +52,7 @@
5252 | `appearance.dark_palette.accent` | string | `#a8a8a8` | |
5353 | `appearance.dark_palette.border` | string | `#363636` | |
5454 | `debug.enable_devtools` | bool | `true` | |
55-| `rendering.mode` | string | `gpu` | `auto`, `gpu`, `cpu` |
56-| `rendering.disable_dmabuf_renderer` | bool | `false` | |
57-| `rendering.force_compositing_mode` | bool | `false` | |
58-| `rendering.disable_compositing_mode` | bool | `false` | |
59-| `rendering.gsk_renderer` | string | `auto` | `auto`, `opengl`, `vulkan`, `cairo` |
60-| `rendering.disable_mipmaps` | bool | `false` | |
61-| `rendering.prefer_gl` | bool | `false` | |
62-| `rendering.draw_compositing_indicators` | bool | `false` | |
63-| `rendering.show_fps` | bool | `false` | |
64-| `rendering.sample_memory` | bool | `false` | |
65-| `rendering.debug_frames` | bool | `false` | |
66-| `engine.type` | string | `cef` | `cef`, `webkit` |
55+| `engine.type` | string | `cef` | `cef`, `webkit` (CEF default; WebKitGTK fallback) |
6756 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |
6857 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |
6958 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
@@ -76,6 +65,19 @@
7665 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |
7766 | `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 |
7867 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |
68+| `engine.webkit.gsk_renderer` | string | `auto` | `auto`, `opengl`, `vulkan`, `cairo` (WebKit fallback only) |
69+| `engine.webkit.disable_dmabuf_renderer` | bool | `false` | WebKit fallback only |
70+| `engine.webkit.force_compositing_mode` | bool | `false` | WebKit fallback only |
71+| `engine.webkit.disable_compositing_mode` | bool | `false` | WebKit fallback only |
72+| `engine.webkit.disable_mipmaps` | bool | `false` | WebKit fallback only |
73+| `engine.webkit.prefer_gl` | bool | `false` | WebKit fallback only |
74+| `engine.webkit.draw_compositing_indicators` | bool | `false` | WebKit fallback only |
75+| `engine.webkit.show_fps` | bool | `false` | WebKit fallback only |
76+| `engine.webkit.sample_memory` | bool | `false` | WebKit fallback only |
77+| `engine.webkit.debug_frames` | bool | `false` | WebKit fallback only |
78+| `engine.webkit.force_vsync` | bool | `false` | WebKit fallback GStreamer only |
79+| `engine.webkit.gl_rendering_mode` | string | `auto` | `auto`, `gles2`, `gl3`, `none` (WebKit fallback only) |
80+| `engine.webkit.gstreamer_debug_level` | int | `0` | `0-5` (WebKit fallback only) |
7981 | `default_ui_scale` | float | `1.0` | > 0 |
8082
8183 Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
@@ -124,28 +126,51 @@ Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise
124126 | `media.hardware_decoding` | string | `auto` | `auto`, `force`, `disable` |
125127 | `media.prefer_av1` | bool | `false` | |
126128 | `media.show_diagnostics` | bool | `false` | |
127-| `media.force_vsync` | bool | `false` | |
128-| `media.gl_rendering_mode` | string | `auto` | `auto`, `gles2`, `gl3`, `none` |
129-| `media.gstreamer_debug_level` | int | `0` | 0-5 |
130-| `runtime.prefix` | string | `` | |
129+| `engine.cef.cef_dir` | string | `` | CEF runtime directory |
130+| `engine.webkit.prefix` | string | `` | WebKitGTK fallback runtime prefix |
131131 | `clipboard.auto_copy_on_selection` | bool | `true` | |
132132 | `content_filtering.enabled` | bool | `true` | |
133133 | `content_filtering.auto_update` | bool | `true` | |
134134 | `update.enable_on_startup` | bool | `true` | |
135135 | `update.auto_download` | bool | `false` | |
136136 | `update.notify_on_new_settings` | bool | `true` | |
137-| `performance.profile` | string | `default` | `default`, `lite`, `max`, `custom` |
138-| `performance.skia_cpu_painting_threads` | int | `0` | |
139-| `performance.skia_gpu_painting_threads` | int | `-1` | |
140-| `performance.skia_enable_cpu_rendering` | bool | `false` | |
141-| `performance.web_process_memory_limit_mb` | int | `0` | |
142-| `performance.web_process_memory_poll_interval_sec` | float | `0` | |
143-| `performance.web_process_memory_conservative_threshold` | float | `0` | |
144-| `performance.web_process_memory_strict_threshold` | float | `0` | |
145-| `performance.network_process_memory_limit_mb` | int | `0` | |
146-| `performance.network_process_memory_poll_interval_sec` | float | `0` | |
147-| `performance.network_process_memory_conservative_threshold` | float | `0` | |
148-| `performance.network_process_memory_strict_threshold` | float | `0` | |
149-| `performance.webview_pool_prewarm_count` | int | `4` | |
150-| `performance.zoom_cache_size` | int | `256` | |
137+| `engine.profile` | string | `default` | `default`, `lite`, `balanced`, `max`, `custom` |
138+| `engine.webkit.skia_cpu_painting_threads` | int | `0` | >= 0 (WebKit fallback custom profile only) |
139+| `engine.webkit.skia_gpu_painting_threads` | int | `-1` | >= -1 (WebKit fallback custom profile only) |
140+| `engine.webkit.skia_enable_cpu_rendering` | bool | `false` | WebKit fallback custom profile only |
141+| `engine.webkit.web_process_memory_limit_mb` | int | `0` | >= 0 (WebKit fallback custom profile only) |
142+| `engine.webkit.web_process_memory_poll_interval_sec` | float | `0` | >= 0 (WebKit fallback custom profile only) |
143+| `engine.webkit.web_process_memory_conservative_threshold` | float | `0` | 0-1 (WebKit fallback custom profile only) |
144+| `engine.webkit.web_process_memory_strict_threshold` | float | `0` | 0-1 (WebKit fallback custom profile only) |
145+| `engine.webkit.network_process_memory_limit_mb` | int | `0` | >= 0 (WebKit fallback custom profile only) |
146+| `engine.webkit.network_process_memory_poll_interval_sec` | float | `0` | >= 0 (WebKit fallback custom profile only) |
147+| `engine.webkit.network_process_memory_conservative_threshold` | float | `0` | 0-1 (WebKit fallback custom profile only) |
148+| `engine.webkit.network_process_memory_strict_threshold` | float | `0` | 0-1 (WebKit fallback custom profile only) |
149+| `engine.pool_prewarm_count` | int | `4` | >= 0 |
150+| `engine.zoom_cache_size` | int | `256` | >= 0 |
151151 | `downloads.path` | string | `` | |
152+
153+CEF is the default engine. WebKitGTK is a fallback selected with `engine.type = "webkit"`; `engine.webkit.*` keys are WebKit-specific.
154+
155+Legacy key migration:
156+
157+| Legacy key | Current key |
158+|------------|-------------|
159+| `privacy.cookie_policy` | `engine.cookie_policy` |
160+| `privacy.itp_enabled` | `engine.webkit.itp_enabled` |
161+| `rendering.disable_dmabuf_renderer` | `engine.webkit.disable_dmabuf_renderer` |
162+| `rendering.force_compositing_mode` | `engine.webkit.force_compositing_mode` |
163+| `rendering.disable_compositing_mode` | `engine.webkit.disable_compositing_mode` |
164+| `rendering.gsk_renderer` | `engine.webkit.gsk_renderer` |
165+| `rendering.disable_mipmaps` | `engine.webkit.disable_mipmaps` |
166+| `rendering.prefer_gl` | `engine.webkit.prefer_gl` |
167+| `rendering.draw_compositing_indicators` | `engine.webkit.draw_compositing_indicators` |
168+| `rendering.show_fps` | `engine.webkit.show_fps` |
169+| `rendering.sample_memory` | `engine.webkit.sample_memory` |
170+| `rendering.debug_frames` | `engine.webkit.debug_frames` |
171+| `media.force_vsync` | `engine.webkit.force_vsync` |
172+| `media.gl_rendering_mode` | `engine.webkit.gl_rendering_mode` |
173+| `media.gstreamer_debug_level` | `engine.webkit.gstreamer_debug_level` |
174+| `runtime.prefix` | `engine.webkit.prefix` |
175+| `performance.*` | `engine.profile`, `engine.pool_prewarm_count`, `engine.zoom_cache_size`, or matching `engine.webkit.*` custom tuning keys |
176+| `rendering.mode` | Dropped; no current setting |
@@ -52,18 +52,7 @@
52 | `appearance.dark_palette.accent` | string | `#a8a8a8` | |52 | `appearance.dark_palette.accent` | string | `#a8a8a8` | |
53 | `appearance.dark_palette.border` | string | `#363636` | |53 | `appearance.dark_palette.border` | string | `#363636` | |
54 | `debug.enable_devtools` | bool | `true` | |54 | `debug.enable_devtools` | bool | `true` | |
55-| `rendering.mode` | string | `gpu` | `auto`, `gpu`, `cpu` |55+| `engine.type` | string | `cef` | `cef`, `webkit` (CEF default; WebKitGTK fallback) |
56-| `rendering.disable_dmabuf_renderer` | bool | `false` | |
57-| `rendering.force_compositing_mode` | bool | `false` | |
58-| `rendering.disable_compositing_mode` | bool | `false` | |
59-| `rendering.gsk_renderer` | string | `auto` | `auto`, `opengl`, `vulkan`, `cairo` |
60-| `rendering.disable_mipmaps` | bool | `false` | |
61-| `rendering.prefer_gl` | bool | `false` | |
62-| `rendering.draw_compositing_indicators` | bool | `false` | |
63-| `rendering.show_fps` | bool | `false` | |
64-| `rendering.sample_memory` | bool | `false` | |
65-| `rendering.debug_frames` | bool | `false` | |
66-| `engine.type` | string | `cef` | `cef`, `webkit` |
67 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |56 | `engine.cef.render_stack` | string | `vulkan` | `vulkan`, `egl` |
68 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |57 | `engine.cef.adaptive_windowless_frame_rate` | bool | `true` | |
69 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |58 | `engine.cef.windowless_frame_rate` | int32 | `0` | >= 0 |
@@ -76,6 +65,19 @@
76 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |65 | `engine.cef.input.touchpad_navigation_enabled` | bool | `true` | |
77 | `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 |66 | `engine.cef.input.touchpad_navigation_min_delta` | float | `200.0` | > 0 |
78 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |67 | `engine.cef.input.touchpad_navigation_max_vertical_ratio` | float | `0.5` | > 0 |
68+| `engine.webkit.gsk_renderer` | string | `auto` | `auto`, `opengl`, `vulkan`, `cairo` (WebKit fallback only) |
69+| `engine.webkit.disable_dmabuf_renderer` | bool | `false` | WebKit fallback only |
70+| `engine.webkit.force_compositing_mode` | bool | `false` | WebKit fallback only |
71+| `engine.webkit.disable_compositing_mode` | bool | `false` | WebKit fallback only |
72+| `engine.webkit.disable_mipmaps` | bool | `false` | WebKit fallback only |
73+| `engine.webkit.prefer_gl` | bool | `false` | WebKit fallback only |
74+| `engine.webkit.draw_compositing_indicators` | bool | `false` | WebKit fallback only |
75+| `engine.webkit.show_fps` | bool | `false` | WebKit fallback only |
76+| `engine.webkit.sample_memory` | bool | `false` | WebKit fallback only |
77+| `engine.webkit.debug_frames` | bool | `false` | WebKit fallback only |
78+| `engine.webkit.force_vsync` | bool | `false` | WebKit fallback GStreamer only |
79+| `engine.webkit.gl_rendering_mode` | string | `auto` | `auto`, `gles2`, `gl3`, `none` (WebKit fallback only) |
80+| `engine.webkit.gstreamer_debug_level` | int | `0` | `0-5` (WebKit fallback only) |
79 | `default_ui_scale` | float | `1.0` | > 0 |81 | `default_ui_scale` | float | `1.0` | > 0 |
80 82
81 Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.83 Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise_multiplier` and the additional axis-specific `engine.cef.input.scroll_vertical_multiplier`. `engine.cef.input.touchpad_navigation_max_vertical_ratio` only filters horizontal back/forward swipe recognition; it does not tune vertical scroll speed.
@@ -124,28 +126,51 @@ Touchpad vertical scroll speed is controlled by `engine.cef.input.scroll_precise
124 | `media.hardware_decoding` | string | `auto` | `auto`, `force`, `disable` |126 | `media.hardware_decoding` | string | `auto` | `auto`, `force`, `disable` |
125 | `media.prefer_av1` | bool | `false` | |127 | `media.prefer_av1` | bool | `false` | |
126 | `media.show_diagnostics` | bool | `false` | |128 | `media.show_diagnostics` | bool | `false` | |
127-| `media.force_vsync` | bool | `false` | |129+| `engine.cef.cef_dir` | string | `` | CEF runtime directory |
128-| `media.gl_rendering_mode` | string | `auto` | `auto`, `gles2`, `gl3`, `none` |130+| `engine.webkit.prefix` | string | `` | WebKitGTK fallback runtime prefix |
129-| `media.gstreamer_debug_level` | int | `0` | 0-5 |
130-| `runtime.prefix` | string | `` | |
131 | `clipboard.auto_copy_on_selection` | bool | `true` | |131 | `clipboard.auto_copy_on_selection` | bool | `true` | |
132 | `content_filtering.enabled` | bool | `true` | |132 | `content_filtering.enabled` | bool | `true` | |
133 | `content_filtering.auto_update` | bool | `true` | |133 | `content_filtering.auto_update` | bool | `true` | |
134 | `update.enable_on_startup` | bool | `true` | |134 | `update.enable_on_startup` | bool | `true` | |
135 | `update.auto_download` | bool | `false` | |135 | `update.auto_download` | bool | `false` | |
136 | `update.notify_on_new_settings` | bool | `true` | |136 | `update.notify_on_new_settings` | bool | `true` | |
137-| `performance.profile` | string | `default` | `default`, `lite`, `max`, `custom` |137+| `engine.profile` | string | `default` | `default`, `lite`, `balanced`, `max`, `custom` |
138-| `performance.skia_cpu_painting_threads` | int | `0` | |138+| `engine.webkit.skia_cpu_painting_threads` | int | `0` | >= 0 (WebKit fallback custom profile only) |
139-| `performance.skia_gpu_painting_threads` | int | `-1` | |139+| `engine.webkit.skia_gpu_painting_threads` | int | `-1` | >= -1 (WebKit fallback custom profile only) |
140-| `performance.skia_enable_cpu_rendering` | bool | `false` | |140+| `engine.webkit.skia_enable_cpu_rendering` | bool | `false` | WebKit fallback custom profile only |
141-| `performance.web_process_memory_limit_mb` | int | `0` | |141+| `engine.webkit.web_process_memory_limit_mb` | int | `0` | >= 0 (WebKit fallback custom profile only) |
142-| `performance.web_process_memory_poll_interval_sec` | float | `0` | |142+| `engine.webkit.web_process_memory_poll_interval_sec` | float | `0` | >= 0 (WebKit fallback custom profile only) |
143-| `performance.web_process_memory_conservative_threshold` | float | `0` | |143+| `engine.webkit.web_process_memory_conservative_threshold` | float | `0` | 0-1 (WebKit fallback custom profile only) |
144-| `performance.web_process_memory_strict_threshold` | float | `0` | |144+| `engine.webkit.web_process_memory_strict_threshold` | float | `0` | 0-1 (WebKit fallback custom profile only) |
145-| `performance.network_process_memory_limit_mb` | int | `0` | |145+| `engine.webkit.network_process_memory_limit_mb` | int | `0` | >= 0 (WebKit fallback custom profile only) |
146-| `performance.network_process_memory_poll_interval_sec` | float | `0` | |146+| `engine.webkit.network_process_memory_poll_interval_sec` | float | `0` | >= 0 (WebKit fallback custom profile only) |
147-| `performance.network_process_memory_conservative_threshold` | float | `0` | |147+| `engine.webkit.network_process_memory_conservative_threshold` | float | `0` | 0-1 (WebKit fallback custom profile only) |
148-| `performance.network_process_memory_strict_threshold` | float | `0` | |148+| `engine.webkit.network_process_memory_strict_threshold` | float | `0` | 0-1 (WebKit fallback custom profile only) |
149-| `performance.webview_pool_prewarm_count` | int | `4` | |149+| `engine.pool_prewarm_count` | int | `4` | >= 0 |
150-| `performance.zoom_cache_size` | int | `256` | |150+| `engine.zoom_cache_size` | int | `256` | >= 0 |
151 | `downloads.path` | string | `` | |151 | `downloads.path` | string | `` | |
152+
153+CEF is the default engine. WebKitGTK is a fallback selected with `engine.type = "webkit"`; `engine.webkit.*` keys are WebKit-specific.
154+
155+Legacy key migration:
156+
157+| Legacy key | Current key |
158+|------------|-------------|
159+| `privacy.cookie_policy` | `engine.cookie_policy` |
160+| `privacy.itp_enabled` | `engine.webkit.itp_enabled` |
161+| `rendering.disable_dmabuf_renderer` | `engine.webkit.disable_dmabuf_renderer` |
162+| `rendering.force_compositing_mode` | `engine.webkit.force_compositing_mode` |
163+| `rendering.disable_compositing_mode` | `engine.webkit.disable_compositing_mode` |
164+| `rendering.gsk_renderer` | `engine.webkit.gsk_renderer` |
165+| `rendering.disable_mipmaps` | `engine.webkit.disable_mipmaps` |
166+| `rendering.prefer_gl` | `engine.webkit.prefer_gl` |
167+| `rendering.draw_compositing_indicators` | `engine.webkit.draw_compositing_indicators` |
168+| `rendering.show_fps` | `engine.webkit.show_fps` |
169+| `rendering.sample_memory` | `engine.webkit.sample_memory` |
170+| `rendering.debug_frames` | `engine.webkit.debug_frames` |
171+| `media.force_vsync` | `engine.webkit.force_vsync` |
172+| `media.gl_rendering_mode` | `engine.webkit.gl_rendering_mode` |
173+| `media.gstreamer_debug_level` | `engine.webkit.gstreamer_debug_level` |
174+| `runtime.prefix` | `engine.webkit.prefix` |
175+| `performance.*` | `engine.profile`, `engine.pool_prewarm_count`, `engine.zoom_cache_size`, or matching `engine.webkit.*` custom tuning keys |
176+| `rendering.mode` | Dropped; no current setting |
M internal/infrastructure/config/loader.go +11 -9
@@ -311,22 +311,24 @@ func bindBrowsingContextEnvAliases(v *viper.Viper) error {
311311
312312 // checkLegacyFormat detects old config format and returns an error directing user to migrate.
313313 func (m *Manager) checkLegacyFormat() error {
314- // Check if old sections exist by looking for known keys.
315314 // IsSet works for legacy keys because they do not have defaults, while
316315 // engine.type has a default and must be checked with InConfig.
317- hasOldSections := m.viper.IsSet("rendering.mode") ||
318- m.viper.IsSet("rendering.disable_dmabuf_renderer") ||
319- m.viper.IsSet("performance.profile") ||
320- m.viper.IsSet("privacy.cookie_policy") ||
321- m.viper.IsSet("runtime.prefix")
316+ hasOldSections := false
317+ for _, alias := range legacyEngineAliases {
318+ if m.viper.IsSet(alias.legacyKey()) {
319+ hasOldSections = true
320+ break
321+ }
322+ }
322323
323324 hasEngineSection := m.viper.InConfig("engine.type")
324325
325326 if hasOldSections && !hasEngineSection {
326327 return fmt.Errorf(
327- "config format outdated: [rendering], [performance], [privacy] sections " +
328- "have moved to [engine]/[engine.webkit] — " +
329- "run \"dumber config migrate\" to update your config file",
328+ "config format outdated: legacy engine keys in [rendering], [performance], " +
329+ "[privacy], [runtime], or migrated [media] fields have moved to " +
330+ "[engine]/[engine.webkit] — run \"dumber config migrate\" " +
331+ "to update your config file",
330332 )
331333 }
332334 return nil
@@ -311,22 +311,24 @@ func bindBrowsingContextEnvAliases(v *viper.Viper) error {
311 311
312 // checkLegacyFormat detects old config format and returns an error directing user to migrate.312 // checkLegacyFormat detects old config format and returns an error directing user to migrate.
313 func (m *Manager) checkLegacyFormat() error {313 func (m *Manager) checkLegacyFormat() error {
314- // Check if old sections exist by looking for known keys.
315 // IsSet works for legacy keys because they do not have defaults, while314 // IsSet works for legacy keys because they do not have defaults, while
316 // engine.type has a default and must be checked with InConfig.315 // engine.type has a default and must be checked with InConfig.
317- hasOldSections := m.viper.IsSet("rendering.mode") ||316+ hasOldSections := false
318- m.viper.IsSet("rendering.disable_dmabuf_renderer") ||317+ for _, alias := range legacyEngineAliases {
319- m.viper.IsSet("performance.profile") ||318+ if m.viper.IsSet(alias.legacyKey()) {
320- m.viper.IsSet("privacy.cookie_policy") ||319+ hasOldSections = true
321- m.viper.IsSet("runtime.prefix")320+ break
321+ }
322+ }
322 323
323 hasEngineSection := m.viper.InConfig("engine.type")324 hasEngineSection := m.viper.InConfig("engine.type")
324 325
325 if hasOldSections && !hasEngineSection {326 if hasOldSections && !hasEngineSection {
326 return fmt.Errorf(327 return fmt.Errorf(
327- "config format outdated: [rendering], [performance], [privacy] sections " +328+ "config format outdated: legacy engine keys in [rendering], [performance], " +
328- "have moved to [engine]/[engine.webkit] — " +329+ "[privacy], [runtime], or migrated [media] fields have moved to " +
329- "run \"dumber config migrate\" to update your config file",330+ "[engine]/[engine.webkit] — run \"dumber config migrate\" " +
331+ "to update your config file",
330 )332 )
331 }333 }
332 return nil334 return nil
M internal/infrastructure/config/loader_legacy_test.go +44 -0
@@ -78,6 +78,50 @@ func TestCheckLegacyFormat_RuntimePrefix_ReturnsError(t *testing.T) {
7878 require.Contains(t, err.Error(), "dumber config migrate")
7979 }
8080
81+func TestCheckLegacyFormat_MetadataBackedAliasesReturnError(t *testing.T) {
82+ for _, alias := range legacyEngineAliases {
83+ key := alias.legacyKey()
84+ t.Run(key, func(t *testing.T) {
85+ m := &Manager{viper: viper.New()}
86+ m.viper.Set(key, sampleLegacyAliasValue(alias))
87+
88+ err := m.checkLegacyFormat()
89+
90+ require.Error(t, err)
91+ require.Contains(t, err.Error(), "dumber config migrate")
92+ })
93+ }
94+}
95+
96+func sampleLegacyAliasValue(alias legacyEngineAlias) any {
97+ switch alias.legacyKey() {
98+ case "performance.profile":
99+ return "balanced"
100+ case "privacy.cookie_policy":
101+ return "always"
102+ case "rendering.mode":
103+ return "gpu"
104+ case "rendering.gsk_renderer", "media.gl_rendering_mode":
105+ return "auto"
106+ case "runtime.prefix":
107+ return "/usr"
108+ }
109+
110+ switch alias.legacyField {
111+ case "zoom_cache_size", "webview_pool_prewarm_count",
112+ "skia_cpu_painting_threads", "skia_gpu_painting_threads",
113+ "web_process_memory_limit_mb", "network_process_memory_limit_mb",
114+ "gstreamer_debug_level":
115+ return 1
116+ case "web_process_memory_poll_interval_sec", "network_process_memory_poll_interval_sec",
117+ "web_process_memory_conservative_threshold", "web_process_memory_strict_threshold",
118+ "network_process_memory_conservative_threshold", "network_process_memory_strict_threshold":
119+ return 0.5
120+ default:
121+ return true
122+ }
123+}
124+
81125 func TestTransformLegacyConfig_DefaultAdaptiveDoesNotBlockLegacyCEFMigration(t *testing.T) {
82126 dir := t.TempDir()
83127 configPath := filepath.Join(dir, "config.toml")
@@ -78,6 +78,50 @@ func TestCheckLegacyFormat_RuntimePrefix_ReturnsError(t *testing.T) {
78 require.Contains(t, err.Error(), "dumber config migrate")78 require.Contains(t, err.Error(), "dumber config migrate")
79 }79 }
80 80
81+func TestCheckLegacyFormat_MetadataBackedAliasesReturnError(t *testing.T) {
82+ for _, alias := range legacyEngineAliases {
83+ key := alias.legacyKey()
84+ t.Run(key, func(t *testing.T) {
85+ m := &Manager{viper: viper.New()}
86+ m.viper.Set(key, sampleLegacyAliasValue(alias))
87+
88+ err := m.checkLegacyFormat()
89+
90+ require.Error(t, err)
91+ require.Contains(t, err.Error(), "dumber config migrate")
92+ })
93+ }
94+}
95+
96+func sampleLegacyAliasValue(alias legacyEngineAlias) any {
97+ switch alias.legacyKey() {
98+ case "performance.profile":
99+ return "balanced"
100+ case "privacy.cookie_policy":
101+ return "always"
102+ case "rendering.mode":
103+ return "gpu"
104+ case "rendering.gsk_renderer", "media.gl_rendering_mode":
105+ return "auto"
106+ case "runtime.prefix":
107+ return "/usr"
108+ }
109+
110+ switch alias.legacyField {
111+ case "zoom_cache_size", "webview_pool_prewarm_count",
112+ "skia_cpu_painting_threads", "skia_gpu_painting_threads",
113+ "web_process_memory_limit_mb", "network_process_memory_limit_mb",
114+ "gstreamer_debug_level":
115+ return 1
116+ case "web_process_memory_poll_interval_sec", "network_process_memory_poll_interval_sec",
117+ "web_process_memory_conservative_threshold", "web_process_memory_strict_threshold",
118+ "network_process_memory_conservative_threshold", "network_process_memory_strict_threshold":
119+ return 0.5
120+ default:
121+ return true
122+ }
123+}
124+
81 func TestTransformLegacyConfig_DefaultAdaptiveDoesNotBlockLegacyCEFMigration(t *testing.T) {125 func TestTransformLegacyConfig_DefaultAdaptiveDoesNotBlockLegacyCEFMigration(t *testing.T) {
82 dir := t.TempDir()126 dir := t.TempDir()
83 configPath := filepath.Join(dir, "config.toml")127 configPath := filepath.Join(dir, "config.toml")
M internal/infrastructure/config/migrate_engine.go +134 -88
@@ -10,79 +10,127 @@ import (
1010 // configFilePermissions is the file permission used when writing migrated config files.
1111 const configFilePermissions fs.FileMode = 0o644
1212
13-// engineMigrationMapping defines old key -> new key mappings for the engine migration.
14-// Keys with empty new key ("") are dropped (not migrated).
15-// The source section is the first part of the old key.
16-type engineFieldMapping struct {
17- oldSection string
18- oldField string
19- newSection string // "engine" or "engine.webkit"
20- newField string // empty means drop
13+// legacyEngineAlias defines the single metadata source for old engine config keys.
14+// Dropped aliases are detected as legacy input but are not written to a new target.
15+type legacyEngineAlias struct {
16+ legacySection string
17+ legacyField string
18+ targetSection string // "engine" or "engine.webkit"; empty when dropped
19+ targetField string // empty when dropped
20+ dropped bool
2121 }
2222
23-// engineUniversalMappings are fields that go into [engine] (not [engine.webkit]).
24-var engineUniversalMappings = []engineFieldMapping{
23+func (a legacyEngineAlias) legacyKey() string {
24+ return a.legacySection + "." + a.legacyField
25+}
26+
27+const (
28+ engineTargetSection = "engine"
29+ engineWebKitTargetSection = "engine.webkit"
30+)
31+
32+func legacyEngineTarget(legacySection, legacyField, targetSection, targetField string) legacyEngineAlias {
33+ return legacyEngineAlias{
34+ legacySection: legacySection,
35+ legacyField: legacyField,
36+ targetSection: targetSection,
37+ targetField: targetField,
38+ }
39+}
40+
41+func legacyEngineSameTarget(legacySection, legacyField, targetSection string) legacyEngineAlias {
42+ return legacyEngineTarget(legacySection, legacyField, targetSection, legacyField)
43+}
44+
45+func droppedLegacyEngineAlias(legacySection, legacyField string) legacyEngineAlias {
46+ return legacyEngineAlias{legacySection: legacySection, legacyField: legacyField, dropped: true}
47+}
48+
49+// legacyEngineAliases is the canonical list of old-to-new engine config aliases.
50+var legacyEngineAliases = []legacyEngineAlias{
2551 // [performance] -> [engine]
26- {oldSection: "performance", oldField: "profile", newSection: "engine", newField: "profile"},
27- {oldSection: "performance", oldField: "zoom_cache_size", newSection: "engine", newField: "zoom_cache_size"},
28- {oldSection: "performance", oldField: "webview_pool_prewarm_count", newSection: "engine", newField: "pool_prewarm_count"},
52+ legacyEngineSameTarget("performance", "profile", engineTargetSection),
53+ legacyEngineSameTarget("performance", "zoom_cache_size", engineTargetSection),
54+ legacyEngineTarget("performance", "webview_pool_prewarm_count", engineTargetSection, "pool_prewarm_count"),
2955 // [privacy] -> [engine]
30- {oldSection: "privacy", oldField: "cookie_policy", newSection: "engine", newField: "cookie_policy"},
31-}
56+ legacyEngineSameTarget("privacy", "cookie_policy", engineTargetSection),
3257
33-// engineWebkitMappings are fields that go into [engine.webkit].
34-var engineWebkitMappings = []engineFieldMapping{
3558 // [privacy] -> [engine.webkit]
36- {oldSection: "privacy", oldField: "itp_enabled", newSection: "engine.webkit", newField: "itp_enabled"},
59+ legacyEngineSameTarget("privacy", "itp_enabled", engineWebKitTargetSection),
3760
3861 // [rendering] -> [engine.webkit] (rendering.mode is dropped)
39- {oldSection: "rendering", oldField: "disable_dmabuf_renderer", newSection: "engine.webkit", newField: "disable_dmabuf_renderer"},
40- {oldSection: "rendering", oldField: "force_compositing_mode", newSection: "engine.webkit", newField: "force_compositing_mode"},
41- {oldSection: "rendering", oldField: "disable_compositing_mode", newSection: "engine.webkit", newField: "disable_compositing_mode"},
42- {oldSection: "rendering", oldField: "gsk_renderer", newSection: "engine.webkit", newField: "gsk_renderer"},
43- {oldSection: "rendering", oldField: "disable_mipmaps", newSection: "engine.webkit", newField: "disable_mipmaps"},
44- {oldSection: "rendering", oldField: "prefer_gl", newSection: "engine.webkit", newField: "prefer_gl"},
45- {oldSection: "rendering", oldField: "draw_compositing_indicators", newSection: "engine.webkit", newField: "draw_compositing_indicators"},
46- {oldSection: "rendering", oldField: "show_fps", newSection: "engine.webkit", newField: "show_fps"},
47- {oldSection: "rendering", oldField: "sample_memory", newSection: "engine.webkit", newField: "sample_memory"},
48- {oldSection: "rendering", oldField: "debug_frames", newSection: "engine.webkit", newField: "debug_frames"},
62+ droppedLegacyEngineAlias("rendering", "mode"),
63+ legacyEngineSameTarget("rendering", "disable_dmabuf_renderer", engineWebKitTargetSection),
64+ legacyEngineSameTarget("rendering", "force_compositing_mode", engineWebKitTargetSection),
65+ legacyEngineSameTarget("rendering", "disable_compositing_mode", engineWebKitTargetSection),
66+ legacyEngineSameTarget("rendering", "gsk_renderer", engineWebKitTargetSection),
67+ legacyEngineSameTarget("rendering", "disable_mipmaps", engineWebKitTargetSection),
68+ legacyEngineSameTarget("rendering", "prefer_gl", engineWebKitTargetSection),
69+ legacyEngineSameTarget("rendering", "draw_compositing_indicators", engineWebKitTargetSection),
70+ legacyEngineSameTarget("rendering", "show_fps", engineWebKitTargetSection),
71+ legacyEngineSameTarget("rendering", "sample_memory", engineWebKitTargetSection),
72+ legacyEngineSameTarget("rendering", "debug_frames", engineWebKitTargetSection),
4973
5074 // [performance] -> [engine.webkit] (webkit-specific) — field names are identical
51- {oldSection: "performance", oldField: "skia_cpu_painting_threads",
52- newSection: "engine.webkit", newField: "skia_cpu_painting_threads"},
53- {oldSection: "performance", oldField: "skia_gpu_painting_threads",
54- newSection: "engine.webkit", newField: "skia_gpu_painting_threads"},
55- {oldSection: "performance", oldField: "skia_enable_cpu_rendering",
56- newSection: "engine.webkit", newField: "skia_enable_cpu_rendering"},
57- {oldSection: "performance", oldField: "web_process_memory_limit_mb",
58- newSection: "engine.webkit", newField: "web_process_memory_limit_mb"},
59- {oldSection: "performance", oldField: "web_process_memory_poll_interval_sec",
60- newSection: "engine.webkit", newField: "web_process_memory_poll_interval_sec"},
61- {oldSection: "performance", oldField: "web_process_memory_conservative_threshold",
62- newSection: "engine.webkit", newField: "web_process_memory_conservative_threshold"},
63- {oldSection: "performance", oldField: "web_process_memory_strict_threshold",
64- newSection: "engine.webkit", newField: "web_process_memory_strict_threshold"},
65- {oldSection: "performance", oldField: "network_process_memory_limit_mb",
66- newSection: "engine.webkit", newField: "network_process_memory_limit_mb"},
67- {oldSection: "performance", oldField: "network_process_memory_poll_interval_sec",
68- newSection: "engine.webkit", newField: "network_process_memory_poll_interval_sec"},
69- {oldSection: "performance", oldField: "network_process_memory_conservative_threshold",
70- newSection: "engine.webkit", newField: "network_process_memory_conservative_threshold"},
71- {oldSection: "performance", oldField: "network_process_memory_strict_threshold",
72- newSection: "engine.webkit", newField: "network_process_memory_strict_threshold"},
75+ legacyEngineSameTarget("performance", "skia_cpu_painting_threads", engineWebKitTargetSection),
76+ legacyEngineSameTarget("performance", "skia_gpu_painting_threads", engineWebKitTargetSection),
77+ legacyEngineSameTarget("performance", "skia_enable_cpu_rendering", engineWebKitTargetSection),
78+ legacyEngineSameTarget("performance", "web_process_memory_limit_mb", engineWebKitTargetSection),
79+ legacyEngineSameTarget("performance", "web_process_memory_poll_interval_sec", engineWebKitTargetSection),
80+ legacyEngineSameTarget("performance", "web_process_memory_conservative_threshold", engineWebKitTargetSection),
81+ legacyEngineSameTarget("performance", "web_process_memory_strict_threshold", engineWebKitTargetSection),
82+ legacyEngineSameTarget("performance", "network_process_memory_limit_mb", engineWebKitTargetSection),
83+ legacyEngineSameTarget("performance", "network_process_memory_poll_interval_sec", engineWebKitTargetSection),
84+ legacyEngineSameTarget("performance", "network_process_memory_conservative_threshold", engineWebKitTargetSection),
85+ legacyEngineSameTarget("performance", "network_process_memory_strict_threshold", engineWebKitTargetSection),
7386
7487 // [media] GStreamer fields -> [engine.webkit]
75- {oldSection: "media", oldField: "force_vsync", newSection: "engine.webkit", newField: "force_vsync"},
76- {oldSection: "media", oldField: "gl_rendering_mode", newSection: "engine.webkit", newField: "gl_rendering_mode"},
77- {oldSection: "media", oldField: "gstreamer_debug_level", newSection: "engine.webkit", newField: "gstreamer_debug_level"},
88+ legacyEngineSameTarget("media", "force_vsync", engineWebKitTargetSection),
89+ legacyEngineSameTarget("media", "gl_rendering_mode", engineWebKitTargetSection),
90+ legacyEngineSameTarget("media", "gstreamer_debug_level", engineWebKitTargetSection),
7891
7992 // [runtime] -> [engine.webkit]
80- {oldSection: "runtime", oldField: "prefix", newSection: "engine.webkit", newField: "prefix"},
93+ legacyEngineSameTarget("runtime", "prefix", engineWebKitTargetSection),
94+}
95+
96+func legacyEngineAliasesForSection(section string) []legacyEngineAlias {
97+ aliases := make([]legacyEngineAlias, 0)
98+ for _, alias := range legacyEngineAliases {
99+ if alias.legacySection == section {
100+ aliases = append(aliases, alias)
101+ }
102+ }
103+ return aliases
104+}
105+
106+func legacyEngineSectionsToRemove() []string {
107+ sections := make([]string, 0)
108+ seen := make(map[string]struct{})
109+ for _, alias := range legacyEngineAliases {
110+ if alias.legacySection == "media" {
111+ continue
112+ }
113+ if _, ok := seen[alias.legacySection]; ok {
114+ continue
115+ }
116+ seen[alias.legacySection] = struct{}{}
117+ sections = append(sections, alias.legacySection)
118+ }
119+ return sections
81120 }
82121
83-// oldSectionsToRemove are the sections that are fully removed after migration
84-// (media is not here because it has fields that stay).
85-var oldSectionsToRemove = []string{"rendering", "performance", "privacy", "runtime"}
122+func hasLegacyEngineAlias(raw map[string]any, alias legacyEngineAlias) bool {
123+ sectionAny, exists := raw[alias.legacySection]
124+ if !exists {
125+ return false
126+ }
127+ sectionMap, ok := sectionAny.(map[string]any)
128+ if !ok {
129+ return false
130+ }
131+ _, exists = sectionMap[alias.legacyField]
132+ return exists
133+}
86134
87135 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].
88136 // Returns true if migration was performed, false if already migrated or no old sections found.
@@ -93,7 +141,7 @@ func MigrateToEngineConfig(configFile string) (bool, error) {
93141 }
94142
95143 if _, exists := raw["engine"]; exists {
96- for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {
144+ for _, section := range legacyEngineSectionsToRemove() {
97145 if _, hasLegacy := raw[section]; hasLegacy {
98146 return false, fmt.Errorf(
99147 "mixed old/new config: [engine] coexists with legacy [%s]; "+
@@ -102,16 +150,13 @@ func MigrateToEngineConfig(configFile string) (bool, error) {
102150 )
103151 }
104152 }
105- // Check for deprecated media keys that moved to [engine.webkit].
106- if mediaRaw, ok := raw["media"].(map[string]any); ok {
107- for _, key := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
108- if _, has := mediaRaw[key]; has {
109- return false, fmt.Errorf(
110- "mixed old/new config: [engine] coexists with deprecated "+
111- "media.%s; remove it from [media] (now in [engine.webkit])",
112- key,
113- )
114- }
153+ for _, alias := range legacyEngineAliasesForSection("media") {
154+ if hasLegacyEngineAlias(raw, alias) {
155+ return false, fmt.Errorf(
156+ "mixed old/new config: [engine] coexists with deprecated "+
157+ "media.%s; remove it from [media] (now in [engine.webkit])",
158+ alias.legacyField,
159+ )
115160 }
116161 }
117162 return false, nil // already migrated
@@ -150,18 +195,14 @@ func readRawTOML(path string) (map[string]any, error) {
150195
151196 // hasOldEngineSections checks if the raw config has old sections that need migration.
152197 func hasOldEngineSections(raw map[string]any) bool {
153- for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {
198+ for _, section := range legacyEngineSectionsToRemove() {
154199 if _, exists := raw[section]; exists {
155200 return true
156201 }
157202 }
158- if mediaAny, exists := raw["media"]; exists {
159- if media, ok := mediaAny.(map[string]any); ok {
160- for _, f := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
161- if _, has := media[f]; has {
162- return true
163- }
164- }
203+ for _, alias := range legacyEngineAliasesForSection("media") {
204+ if hasLegacyEngineAlias(raw, alias) {
205+ return true
165206 }
166207 }
167208 return false
@@ -172,14 +213,19 @@ func applyEngineMappings(raw map[string]any) {
172213 engineMap := make(map[string]any)
173214 webkitMap := make(map[string]any)
174215
175- for _, m := range engineUniversalMappings {
176- if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {
177- engineMap[m.newField] = val
216+ for _, alias := range legacyEngineAliases {
217+ if alias.dropped {
218+ continue
178219 }
179- }
180- for _, m := range engineWebkitMappings {
181- if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {
182- webkitMap[m.newField] = val
220+ val := getSectionField(raw, alias.legacySection, alias.legacyField)
221+ if val == nil {
222+ continue
223+ }
224+ switch alias.targetSection {
225+ case "engine":
226+ engineMap[alias.targetField] = val
227+ case "engine.webkit":
228+ webkitMap[alias.targetField] = val
183229 }
184230 }
185231
@@ -189,14 +235,14 @@ func applyEngineMappings(raw map[string]any) {
189235 }
190236 raw["engine"] = engineMap
191237
192- for _, section := range oldSectionsToRemove {
238+ for _, section := range legacyEngineSectionsToRemove() {
193239 delete(raw, section)
194240 }
195241
196242 stripMigratedMediaFields(raw)
197243 }
198244
199-// stripMigratedMediaFields removes GStreamer fields from [media] while keeping the rest.
245+// stripMigratedMediaFields removes migrated fields from [media] while keeping the rest.
200246 func stripMigratedMediaFields(raw map[string]any) {
201247 mediaAny, exists := raw["media"]
202248 if !exists {
@@ -206,9 +252,9 @@ func stripMigratedMediaFields(raw map[string]any) {
206252 if !ok {
207253 return
208254 }
209- delete(media, "force_vsync")
210- delete(media, "gl_rendering_mode")
211- delete(media, "gstreamer_debug_level")
255+ for _, alias := range legacyEngineAliasesForSection("media") {
256+ delete(media, alias.legacyField)
257+ }
212258 if len(media) == 0 {
213259 delete(raw, "media")
214260 }
@@ -10,79 +10,127 @@ import (
10 // configFilePermissions is the file permission used when writing migrated config files.10 // configFilePermissions is the file permission used when writing migrated config files.
11 const configFilePermissions fs.FileMode = 0o64411 const configFilePermissions fs.FileMode = 0o644
12 12
13-// engineMigrationMapping defines old key -> new key mappings for the engine migration.13+// legacyEngineAlias defines the single metadata source for old engine config keys.
14-// Keys with empty new key ("") are dropped (not migrated).14+// Dropped aliases are detected as legacy input but are not written to a new target.
15-// The source section is the first part of the old key.15+type legacyEngineAlias struct {
16-type engineFieldMapping struct {16+ legacySection string
17- oldSection string17+ legacyField string
18- oldField string18+ targetSection string // "engine" or "engine.webkit"; empty when dropped
19- newSection string // "engine" or "engine.webkit"19+ targetField string // empty when dropped
20- newField string // empty means drop20+ dropped bool
21 }21 }
22 22
23-// engineUniversalMappings are fields that go into [engine] (not [engine.webkit]).23+func (a legacyEngineAlias) legacyKey() string {
24-var engineUniversalMappings = []engineFieldMapping{24+ return a.legacySection + "." + a.legacyField
25+}
26+
27+const (
28+ engineTargetSection = "engine"
29+ engineWebKitTargetSection = "engine.webkit"
30+)
31+
32+func legacyEngineTarget(legacySection, legacyField, targetSection, targetField string) legacyEngineAlias {
33+ return legacyEngineAlias{
34+ legacySection: legacySection,
35+ legacyField: legacyField,
36+ targetSection: targetSection,
37+ targetField: targetField,
38+ }
39+}
40+
41+func legacyEngineSameTarget(legacySection, legacyField, targetSection string) legacyEngineAlias {
42+ return legacyEngineTarget(legacySection, legacyField, targetSection, legacyField)
43+}
44+
45+func droppedLegacyEngineAlias(legacySection, legacyField string) legacyEngineAlias {
46+ return legacyEngineAlias{legacySection: legacySection, legacyField: legacyField, dropped: true}
47+}
48+
49+// legacyEngineAliases is the canonical list of old-to-new engine config aliases.
50+var legacyEngineAliases = []legacyEngineAlias{
25 // [performance] -> [engine]51 // [performance] -> [engine]
26- {oldSection: "performance", oldField: "profile", newSection: "engine", newField: "profile"},52+ legacyEngineSameTarget("performance", "profile", engineTargetSection),
27- {oldSection: "performance", oldField: "zoom_cache_size", newSection: "engine", newField: "zoom_cache_size"},53+ legacyEngineSameTarget("performance", "zoom_cache_size", engineTargetSection),
28- {oldSection: "performance", oldField: "webview_pool_prewarm_count", newSection: "engine", newField: "pool_prewarm_count"},54+ legacyEngineTarget("performance", "webview_pool_prewarm_count", engineTargetSection, "pool_prewarm_count"),
29 // [privacy] -> [engine]55 // [privacy] -> [engine]
30- {oldSection: "privacy", oldField: "cookie_policy", newSection: "engine", newField: "cookie_policy"},56+ legacyEngineSameTarget("privacy", "cookie_policy", engineTargetSection),
31-}
32 57
33-// engineWebkitMappings are fields that go into [engine.webkit].
34-var engineWebkitMappings = []engineFieldMapping{
35 // [privacy] -> [engine.webkit]58 // [privacy] -> [engine.webkit]
36- {oldSection: "privacy", oldField: "itp_enabled", newSection: "engine.webkit", newField: "itp_enabled"},59+ legacyEngineSameTarget("privacy", "itp_enabled", engineWebKitTargetSection),
37 60
38 // [rendering] -> [engine.webkit] (rendering.mode is dropped)61 // [rendering] -> [engine.webkit] (rendering.mode is dropped)
39- {oldSection: "rendering", oldField: "disable_dmabuf_renderer", newSection: "engine.webkit", newField: "disable_dmabuf_renderer"},62+ droppedLegacyEngineAlias("rendering", "mode"),
40- {oldSection: "rendering", oldField: "force_compositing_mode", newSection: "engine.webkit", newField: "force_compositing_mode"},63+ legacyEngineSameTarget("rendering", "disable_dmabuf_renderer", engineWebKitTargetSection),
41- {oldSection: "rendering", oldField: "disable_compositing_mode", newSection: "engine.webkit", newField: "disable_compositing_mode"},64+ legacyEngineSameTarget("rendering", "force_compositing_mode", engineWebKitTargetSection),
42- {oldSection: "rendering", oldField: "gsk_renderer", newSection: "engine.webkit", newField: "gsk_renderer"},65+ legacyEngineSameTarget("rendering", "disable_compositing_mode", engineWebKitTargetSection),
43- {oldSection: "rendering", oldField: "disable_mipmaps", newSection: "engine.webkit", newField: "disable_mipmaps"},66+ legacyEngineSameTarget("rendering", "gsk_renderer", engineWebKitTargetSection),
44- {oldSection: "rendering", oldField: "prefer_gl", newSection: "engine.webkit", newField: "prefer_gl"},67+ legacyEngineSameTarget("rendering", "disable_mipmaps", engineWebKitTargetSection),
45- {oldSection: "rendering", oldField: "draw_compositing_indicators", newSection: "engine.webkit", newField: "draw_compositing_indicators"},68+ legacyEngineSameTarget("rendering", "prefer_gl", engineWebKitTargetSection),
46- {oldSection: "rendering", oldField: "show_fps", newSection: "engine.webkit", newField: "show_fps"},69+ legacyEngineSameTarget("rendering", "draw_compositing_indicators", engineWebKitTargetSection),
47- {oldSection: "rendering", oldField: "sample_memory", newSection: "engine.webkit", newField: "sample_memory"},70+ legacyEngineSameTarget("rendering", "show_fps", engineWebKitTargetSection),
48- {oldSection: "rendering", oldField: "debug_frames", newSection: "engine.webkit", newField: "debug_frames"},71+ legacyEngineSameTarget("rendering", "sample_memory", engineWebKitTargetSection),
72+ legacyEngineSameTarget("rendering", "debug_frames", engineWebKitTargetSection),
49 73
50 // [performance] -> [engine.webkit] (webkit-specific) — field names are identical74 // [performance] -> [engine.webkit] (webkit-specific) — field names are identical
51- {oldSection: "performance", oldField: "skia_cpu_painting_threads",75+ legacyEngineSameTarget("performance", "skia_cpu_painting_threads", engineWebKitTargetSection),
52- newSection: "engine.webkit", newField: "skia_cpu_painting_threads"},76+ legacyEngineSameTarget("performance", "skia_gpu_painting_threads", engineWebKitTargetSection),
53- {oldSection: "performance", oldField: "skia_gpu_painting_threads",77+ legacyEngineSameTarget("performance", "skia_enable_cpu_rendering", engineWebKitTargetSection),
54- newSection: "engine.webkit", newField: "skia_gpu_painting_threads"},78+ legacyEngineSameTarget("performance", "web_process_memory_limit_mb", engineWebKitTargetSection),
55- {oldSection: "performance", oldField: "skia_enable_cpu_rendering",79+ legacyEngineSameTarget("performance", "web_process_memory_poll_interval_sec", engineWebKitTargetSection),
56- newSection: "engine.webkit", newField: "skia_enable_cpu_rendering"},80+ legacyEngineSameTarget("performance", "web_process_memory_conservative_threshold", engineWebKitTargetSection),
57- {oldSection: "performance", oldField: "web_process_memory_limit_mb",81+ legacyEngineSameTarget("performance", "web_process_memory_strict_threshold", engineWebKitTargetSection),
58- newSection: "engine.webkit", newField: "web_process_memory_limit_mb"},82+ legacyEngineSameTarget("performance", "network_process_memory_limit_mb", engineWebKitTargetSection),
59- {oldSection: "performance", oldField: "web_process_memory_poll_interval_sec",83+ legacyEngineSameTarget("performance", "network_process_memory_poll_interval_sec", engineWebKitTargetSection),
60- newSection: "engine.webkit", newField: "web_process_memory_poll_interval_sec"},84+ legacyEngineSameTarget("performance", "network_process_memory_conservative_threshold", engineWebKitTargetSection),
61- {oldSection: "performance", oldField: "web_process_memory_conservative_threshold",85+ legacyEngineSameTarget("performance", "network_process_memory_strict_threshold", engineWebKitTargetSection),
62- newSection: "engine.webkit", newField: "web_process_memory_conservative_threshold"},
63- {oldSection: "performance", oldField: "web_process_memory_strict_threshold",
64- newSection: "engine.webkit", newField: "web_process_memory_strict_threshold"},
65- {oldSection: "performance", oldField: "network_process_memory_limit_mb",
66- newSection: "engine.webkit", newField: "network_process_memory_limit_mb"},
67- {oldSection: "performance", oldField: "network_process_memory_poll_interval_sec",
68- newSection: "engine.webkit", newField: "network_process_memory_poll_interval_sec"},
69- {oldSection: "performance", oldField: "network_process_memory_conservative_threshold",
70- newSection: "engine.webkit", newField: "network_process_memory_conservative_threshold"},
71- {oldSection: "performance", oldField: "network_process_memory_strict_threshold",
72- newSection: "engine.webkit", newField: "network_process_memory_strict_threshold"},
73 86
74 // [media] GStreamer fields -> [engine.webkit]87 // [media] GStreamer fields -> [engine.webkit]
75- {oldSection: "media", oldField: "force_vsync", newSection: "engine.webkit", newField: "force_vsync"},88+ legacyEngineSameTarget("media", "force_vsync", engineWebKitTargetSection),
76- {oldSection: "media", oldField: "gl_rendering_mode", newSection: "engine.webkit", newField: "gl_rendering_mode"},89+ legacyEngineSameTarget("media", "gl_rendering_mode", engineWebKitTargetSection),
77- {oldSection: "media", oldField: "gstreamer_debug_level", newSection: "engine.webkit", newField: "gstreamer_debug_level"},90+ legacyEngineSameTarget("media", "gstreamer_debug_level", engineWebKitTargetSection),
78 91
79 // [runtime] -> [engine.webkit]92 // [runtime] -> [engine.webkit]
80- {oldSection: "runtime", oldField: "prefix", newSection: "engine.webkit", newField: "prefix"},93+ legacyEngineSameTarget("runtime", "prefix", engineWebKitTargetSection),
94+}
95+
96+func legacyEngineAliasesForSection(section string) []legacyEngineAlias {
97+ aliases := make([]legacyEngineAlias, 0)
98+ for _, alias := range legacyEngineAliases {
99+ if alias.legacySection == section {
100+ aliases = append(aliases, alias)
101+ }
102+ }
103+ return aliases
104+}
105+
106+func legacyEngineSectionsToRemove() []string {
107+ sections := make([]string, 0)
108+ seen := make(map[string]struct{})
109+ for _, alias := range legacyEngineAliases {
110+ if alias.legacySection == "media" {
111+ continue
112+ }
113+ if _, ok := seen[alias.legacySection]; ok {
114+ continue
115+ }
116+ seen[alias.legacySection] = struct{}{}
117+ sections = append(sections, alias.legacySection)
118+ }
119+ return sections
81 }120 }
82 121
83-// oldSectionsToRemove are the sections that are fully removed after migration122+func hasLegacyEngineAlias(raw map[string]any, alias legacyEngineAlias) bool {
84-// (media is not here because it has fields that stay).123+ sectionAny, exists := raw[alias.legacySection]
85-var oldSectionsToRemove = []string{"rendering", "performance", "privacy", "runtime"}124+ if !exists {
125+ return false
126+ }
127+ sectionMap, ok := sectionAny.(map[string]any)
128+ if !ok {
129+ return false
130+ }
131+ _, exists = sectionMap[alias.legacyField]
132+ return exists
133+}
86 134
87 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].135 // MigrateToEngineConfig restructures old top-level config sections into [engine]/[engine.webkit].
88 // Returns true if migration was performed, false if already migrated or no old sections found.136 // Returns true if migration was performed, false if already migrated or no old sections found.
@@ -93,7 +141,7 @@ func MigrateToEngineConfig(configFile string) (bool, error) {
93 }141 }
94 142
95 if _, exists := raw["engine"]; exists {143 if _, exists := raw["engine"]; exists {
96- for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {144+ for _, section := range legacyEngineSectionsToRemove() {
97 if _, hasLegacy := raw[section]; hasLegacy {145 if _, hasLegacy := raw[section]; hasLegacy {
98 return false, fmt.Errorf(146 return false, fmt.Errorf(
99 "mixed old/new config: [engine] coexists with legacy [%s]; "+147 "mixed old/new config: [engine] coexists with legacy [%s]; "+
@@ -102,16 +150,13 @@ func MigrateToEngineConfig(configFile string) (bool, error) {
102 )150 )
103 }151 }
104 }152 }
105- // Check for deprecated media keys that moved to [engine.webkit].153+ for _, alias := range legacyEngineAliasesForSection("media") {
106- if mediaRaw, ok := raw["media"].(map[string]any); ok {154+ if hasLegacyEngineAlias(raw, alias) {
107- for _, key := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {155+ return false, fmt.Errorf(
108- if _, has := mediaRaw[key]; has {156+ "mixed old/new config: [engine] coexists with deprecated "+
109- return false, fmt.Errorf(157+ "media.%s; remove it from [media] (now in [engine.webkit])",
110- "mixed old/new config: [engine] coexists with deprecated "+158+ alias.legacyField,
111- "media.%s; remove it from [media] (now in [engine.webkit])",159+ )
112- key,
113- )
114- }
115 }160 }
116 }161 }
117 return false, nil // already migrated162 return false, nil // already migrated
@@ -150,18 +195,14 @@ func readRawTOML(path string) (map[string]any, error) {
150 195
151 // hasOldEngineSections checks if the raw config has old sections that need migration.196 // hasOldEngineSections checks if the raw config has old sections that need migration.
152 func hasOldEngineSections(raw map[string]any) bool {197 func hasOldEngineSections(raw map[string]any) bool {
153- for _, section := range []string{"rendering", "performance", "privacy", "runtime"} {198+ for _, section := range legacyEngineSectionsToRemove() {
154 if _, exists := raw[section]; exists {199 if _, exists := raw[section]; exists {
155 return true200 return true
156 }201 }
157 }202 }
158- if mediaAny, exists := raw["media"]; exists {203+ for _, alias := range legacyEngineAliasesForSection("media") {
159- if media, ok := mediaAny.(map[string]any); ok {204+ if hasLegacyEngineAlias(raw, alias) {
160- for _, f := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {205+ return true
161- if _, has := media[f]; has {
162- return true
163- }
164- }
165 }206 }
166 }207 }
167 return false208 return false
@@ -172,14 +213,19 @@ func applyEngineMappings(raw map[string]any) {
172 engineMap := make(map[string]any)213 engineMap := make(map[string]any)
173 webkitMap := make(map[string]any)214 webkitMap := make(map[string]any)
174 215
175- for _, m := range engineUniversalMappings {216+ for _, alias := range legacyEngineAliases {
176- if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {217+ if alias.dropped {
177- engineMap[m.newField] = val218+ continue
178 }219 }
179- }220+ val := getSectionField(raw, alias.legacySection, alias.legacyField)
180- for _, m := range engineWebkitMappings {221+ if val == nil {
181- if val := getSectionField(raw, m.oldSection, m.oldField); val != nil {222+ continue
182- webkitMap[m.newField] = val223+ }
224+ switch alias.targetSection {
225+ case "engine":
226+ engineMap[alias.targetField] = val
227+ case "engine.webkit":
228+ webkitMap[alias.targetField] = val
183 }229 }
184 }230 }
185 231
@@ -189,14 +235,14 @@ func applyEngineMappings(raw map[string]any) {
189 }235 }
190 raw["engine"] = engineMap236 raw["engine"] = engineMap
191 237
192- for _, section := range oldSectionsToRemove {238+ for _, section := range legacyEngineSectionsToRemove() {
193 delete(raw, section)239 delete(raw, section)
194 }240 }
195 241
196 stripMigratedMediaFields(raw)242 stripMigratedMediaFields(raw)
197 }243 }
198 244
199-// stripMigratedMediaFields removes GStreamer fields from [media] while keeping the rest.245+// stripMigratedMediaFields removes migrated fields from [media] while keeping the rest.
200 func stripMigratedMediaFields(raw map[string]any) {246 func stripMigratedMediaFields(raw map[string]any) {
201 mediaAny, exists := raw["media"]247 mediaAny, exists := raw["media"]
202 if !exists {248 if !exists {
@@ -206,9 +252,9 @@ func stripMigratedMediaFields(raw map[string]any) {
206 if !ok {252 if !ok {
207 return253 return
208 }254 }
209- delete(media, "force_vsync")255+ for _, alias := range legacyEngineAliasesForSection("media") {
210- delete(media, "gl_rendering_mode")256+ delete(media, alias.legacyField)
211- delete(media, "gstreamer_debug_level")257+ }
212 if len(media) == 0 {258 if len(media) == 0 {
213 delete(raw, "media")259 delete(raw, "media")
214 }260 }
M internal/infrastructure/config/migrate_engine_test.go +71 -0
@@ -253,6 +253,77 @@ disable_dmabuf_renderer = false
253253 assert.Equal(t, int64(3), webkit["gstreamer_debug_level"])
254254 }
255255
256+func TestLegacyEngineAliasesMetadataCoverage(t *testing.T) {
257+ require.NotEmpty(t, legacyEngineAliases)
258+
259+ aliases := make(map[string]legacyEngineAlias)
260+ for _, alias := range legacyEngineAliases {
261+ key := alias.legacyKey()
262+ require.NotEmpty(t, alias.legacySection, "%s should have a legacy section", key)
263+ require.NotEmpty(t, alias.legacyField, "%s should have a legacy field", key)
264+ require.NotContains(t, aliases, key, "duplicate metadata for %s", key)
265+ aliases[key] = alias
266+
267+ if alias.dropped {
268+ assert.Empty(t, alias.targetSection, "%s is dropped and should not have a target section", key)
269+ assert.Empty(t, alias.targetField, "%s is dropped and should not have a target field", key)
270+ continue
271+ }
272+
273+ require.Contains(t, []string{"engine", "engine.webkit"}, alias.targetSection, "%s should target a known engine section", key)
274+ require.NotEmpty(t, alias.targetField, "%s should have a canonical target field", key)
275+ }
276+
277+ for _, key := range []string{
278+ "media.force_vsync",
279+ "media.gl_rendering_mode",
280+ "media.gstreamer_debug_level",
281+ "runtime.prefix",
282+ "performance.profile",
283+ "privacy.cookie_policy",
284+ "privacy.itp_enabled",
285+ "rendering.disable_dmabuf_renderer",
286+ } {
287+ alias, ok := aliases[key]
288+ require.True(t, ok, "missing metadata for %s", key)
289+ require.False(t, alias.dropped, "%s should have a canonical target", key)
290+ }
291+
292+ renderingMode, ok := aliases["rendering.mode"]
293+ require.True(t, ok, "missing dropped metadata for rendering.mode")
294+ assert.True(t, renderingMode.dropped)
295+}
296+
297+func TestHasOldEngineSections_UsesMetadataForMigratedMediaKeys(t *testing.T) {
298+ for _, field := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
299+ raw := map[string]any{"media": map[string]any{field: true}}
300+ assert.True(t, hasOldEngineSections(raw), "media.%s should trigger engine migration", field)
301+ }
302+
303+ raw := map[string]any{"media": map[string]any{"hardware_decoding": true}}
304+ assert.False(t, hasOldEngineSections(raw), "non-migrated media keys should not trigger engine migration")
305+}
306+
307+func TestMigrateToEngineConfig_MixedEngineAndMigratedMediaKey(t *testing.T) {
308+ tmpDir := t.TempDir()
309+ configFile := filepath.Join(tmpDir, "config.toml")
310+
311+ content := `
312+[engine]
313+type = "webkit"
314+
315+[media]
316+gl_rendering_mode = "auto"
317+`
318+ err := os.WriteFile(configFile, []byte(content), 0o644)
319+ require.NoError(t, err)
320+
321+ migrated, err := MigrateToEngineConfig(configFile)
322+ require.Error(t, err)
323+ assert.False(t, migrated)
324+ assert.Contains(t, err.Error(), "media.gl_rendering_mode")
325+}
326+
256327 func TestMigrateToEngineConfig_EngineTypeDefault(t *testing.T) {
257328 tmpDir := t.TempDir()
258329 configFile := filepath.Join(tmpDir, "config.toml")
@@ -253,6 +253,77 @@ disable_dmabuf_renderer = false
253 assert.Equal(t, int64(3), webkit["gstreamer_debug_level"])253 assert.Equal(t, int64(3), webkit["gstreamer_debug_level"])
254 }254 }
255 255
256+func TestLegacyEngineAliasesMetadataCoverage(t *testing.T) {
257+ require.NotEmpty(t, legacyEngineAliases)
258+
259+ aliases := make(map[string]legacyEngineAlias)
260+ for _, alias := range legacyEngineAliases {
261+ key := alias.legacyKey()
262+ require.NotEmpty(t, alias.legacySection, "%s should have a legacy section", key)
263+ require.NotEmpty(t, alias.legacyField, "%s should have a legacy field", key)
264+ require.NotContains(t, aliases, key, "duplicate metadata for %s", key)
265+ aliases[key] = alias
266+
267+ if alias.dropped {
268+ assert.Empty(t, alias.targetSection, "%s is dropped and should not have a target section", key)
269+ assert.Empty(t, alias.targetField, "%s is dropped and should not have a target field", key)
270+ continue
271+ }
272+
273+ require.Contains(t, []string{"engine", "engine.webkit"}, alias.targetSection, "%s should target a known engine section", key)
274+ require.NotEmpty(t, alias.targetField, "%s should have a canonical target field", key)
275+ }
276+
277+ for _, key := range []string{
278+ "media.force_vsync",
279+ "media.gl_rendering_mode",
280+ "media.gstreamer_debug_level",
281+ "runtime.prefix",
282+ "performance.profile",
283+ "privacy.cookie_policy",
284+ "privacy.itp_enabled",
285+ "rendering.disable_dmabuf_renderer",
286+ } {
287+ alias, ok := aliases[key]
288+ require.True(t, ok, "missing metadata for %s", key)
289+ require.False(t, alias.dropped, "%s should have a canonical target", key)
290+ }
291+
292+ renderingMode, ok := aliases["rendering.mode"]
293+ require.True(t, ok, "missing dropped metadata for rendering.mode")
294+ assert.True(t, renderingMode.dropped)
295+}
296+
297+func TestHasOldEngineSections_UsesMetadataForMigratedMediaKeys(t *testing.T) {
298+ for _, field := range []string{"force_vsync", "gl_rendering_mode", "gstreamer_debug_level"} {
299+ raw := map[string]any{"media": map[string]any{field: true}}
300+ assert.True(t, hasOldEngineSections(raw), "media.%s should trigger engine migration", field)
301+ }
302+
303+ raw := map[string]any{"media": map[string]any{"hardware_decoding": true}}
304+ assert.False(t, hasOldEngineSections(raw), "non-migrated media keys should not trigger engine migration")
305+}
306+
307+func TestMigrateToEngineConfig_MixedEngineAndMigratedMediaKey(t *testing.T) {
308+ tmpDir := t.TempDir()
309+ configFile := filepath.Join(tmpDir, "config.toml")
310+
311+ content := `
312+[engine]
313+type = "webkit"
314+
315+[media]
316+gl_rendering_mode = "auto"
317+`
318+ err := os.WriteFile(configFile, []byte(content), 0o644)
319+ require.NoError(t, err)
320+
321+ migrated, err := MigrateToEngineConfig(configFile)
322+ require.Error(t, err)
323+ assert.False(t, migrated)
324+ assert.Contains(t, err.Error(), "media.gl_rendering_mode")
325+}
326+
256 func TestMigrateToEngineConfig_EngineTypeDefault(t *testing.T) {327 func TestMigrateToEngineConfig_EngineTypeDefault(t *testing.T) {
257 tmpDir := t.TempDir()328 tmpDir := t.TempDir()
258 configFile := filepath.Join(tmpDir, "config.toml")329 configFile := filepath.Join(tmpDir, "config.toml")

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close