detached 24083da

#2 fix(env): scope automatic HDR to WoW executables

state
merged · 24083da · fast-forward
branch
fix/hdr-per-exe → main
opened
bnema ·
updated

Battle.net's CEF launcher crashes (int3 in DXVK d3d11) when DXGI reports HDR10. Automatic Wayland HDR now uses per-exe DXVK_CONFIG sections for WoW executables instead of a global DXVK_HDR=1.

CommitSubjectAuthorDateSignatureStatus
24083dafix(env): scope automatic HDR to WoW executablesbrice GPG Verified

Checks 0

no checks

4 files · +78 -15
no checks
M README.md +14 -6
@@ -54,17 +54,25 @@ bnetctl clean -a # Full purge (cache, prefix, desktop entry, data)
5454
5555 ## HDR on Wayland
5656
57-HDR is enabled automatically for native Wayland (`DXVK_HDR=1`). It requires
58-**Wine >= 11.19**, an HDR-capable compositor with HDR enabled, and an HDR-capable
59-monitor/game. An existing `DXVK_HDR` environment variable is respected, including
60-an empty value. To disable HDR explicitly (also overriding `DXVK_HDR=1`):
57+HDR is enabled automatically for native Wayland only for `WowClassic.exe`,
58+`Wow.exe`, `WowClassicT.exe`, and `WowT.exe`, using per-executable
59+`dxgi.enableHDR = True` sections in `DXVK_CONFIG`. This avoids exposing HDR to
60+Battle.net's Chromium/CEF launcher. It requires **Wine >= 11.19**, an HDR-capable
61+compositor with HDR enabled, and an HDR-capable monitor/game.
62+
63+An existing `DXVK_HDR` environment variable is respected, including an empty
64+value; when it is set, no automatic config is added. Otherwise, the WoW sections
65+are appended after any existing `DXVK_CONFIG` lines, preserving their scope.
66+To skip automatic HDR entries and unset inherited `DXVK_HDR` (user-provided
67+`DXVK_CONFIG` is left untouched):
6168
6269 ```bash
6370 bnetctl launch --no-hdr
6471 ```
6572
66-X11 mode does not set `DXVK_HDR`. Games started by Battle.net inherit its HDR
67-policy; enable HDR in the game's own settings too.
73+X11 mode leaves the user's HDR environment untouched. Games started by
74+Battle.net inherit its per-executable config; enable HDR in the game's own
75+settings too.
6876
6977 For Wayland `install` and `launch`, bnetctl prefers CachyOS Wine when it meets the
7078 minimum, otherwise qualifying `wine` on PATH. If neither qualifies, it retains
@@ -54,17 +54,25 @@ bnetctl clean -a # Full purge (cache, prefix, desktop entry, data)
54 54
55 ## HDR on Wayland55 ## HDR on Wayland
56 56
57-HDR is enabled automatically for native Wayland (`DXVK_HDR=1`). It requires57+HDR is enabled automatically for native Wayland only for `WowClassic.exe`,
58-**Wine >= 11.19**, an HDR-capable compositor with HDR enabled, and an HDR-capable58+`Wow.exe`, `WowClassicT.exe`, and `WowT.exe`, using per-executable
59-monitor/game. An existing `DXVK_HDR` environment variable is respected, including59+`dxgi.enableHDR = True` sections in `DXVK_CONFIG`. This avoids exposing HDR to
60-an empty value. To disable HDR explicitly (also overriding `DXVK_HDR=1`):60+Battle.net's Chromium/CEF launcher. It requires **Wine >= 11.19**, an HDR-capable
61+compositor with HDR enabled, and an HDR-capable monitor/game.
62+
63+An existing `DXVK_HDR` environment variable is respected, including an empty
64+value; when it is set, no automatic config is added. Otherwise, the WoW sections
65+are appended after any existing `DXVK_CONFIG` lines, preserving their scope.
66+To skip automatic HDR entries and unset inherited `DXVK_HDR` (user-provided
67+`DXVK_CONFIG` is left untouched):
61 68
62 ```bash69 ```bash
63 bnetctl launch --no-hdr70 bnetctl launch --no-hdr
64 ```71 ```
65 72
66-X11 mode does not set `DXVK_HDR`. Games started by Battle.net inherit its HDR73+X11 mode leaves the user's HDR environment untouched. Games started by
67-policy; enable HDR in the game's own settings too.74+Battle.net inherit its per-executable config; enable HDR in the game's own
75+settings too.
68 76
69 For Wayland `install` and `launch`, bnetctl prefers CachyOS Wine when it meets the77 For Wayland `install` and `launch`, bnetctl prefers CachyOS Wine when it meets the
70 minimum, otherwise qualifying `wine` on PATH. If neither qualifies, it retains78 minimum, otherwise qualifying `wine` on PATH. If neither qualifies, it retains
M cmd/launch.go +5 -1
@@ -53,7 +53,11 @@ func runLaunch(cmd *cobra.Command, args []string) error {
5353
5454 launcher := services.NewLaunchService(runtime, fs, cfg, func() *domain.WineEnv {
5555 enabled, reason := env.HDRStatus(driver, noHDR)
56- log.Info("hdr env", "enabled", enabled, "reason", reason)
56+ if reason == "wayland default" {
57+ log.Info("hdr env", "enabled", enabled, "reason", reason, "scope", "WoW executables")
58+ } else {
59+ log.Info("hdr env", "enabled", enabled, "reason", reason)
60+ }
5761 return env.BuildGPUEnvForDisplayHDR(driver, noHDR)
5862 }, log, env.BattleNetArgsForDisplay(driver)...)
5963
@@ -53,7 +53,11 @@ func runLaunch(cmd *cobra.Command, args []string) error {
53 53
54 launcher := services.NewLaunchService(runtime, fs, cfg, func() *domain.WineEnv {54 launcher := services.NewLaunchService(runtime, fs, cfg, func() *domain.WineEnv {
55 enabled, reason := env.HDRStatus(driver, noHDR)55 enabled, reason := env.HDRStatus(driver, noHDR)
56- log.Info("hdr env", "enabled", enabled, "reason", reason)56+ if reason == "wayland default" {
57+ log.Info("hdr env", "enabled", enabled, "reason", reason, "scope", "WoW executables")
58+ } else {
59+ log.Info("hdr env", "enabled", enabled, "reason", reason)
60+ }
57 return env.BuildGPUEnvForDisplayHDR(driver, noHDR)61 return env.BuildGPUEnvForDisplayHDR(driver, noHDR)
58 }, log, env.BattleNetArgsForDisplay(driver)...)62 }, log, env.BattleNetArgsForDisplay(driver)...)
59 63
M internal/adapters/env/gpu.go +10 -2
@@ -92,6 +92,10 @@ func BuildGPUEnvForDisplay(driver DisplayDriver) *domain.WineEnv {
9292 return BuildGPUEnvForDisplayHDR(driver, false)
9393 }
9494
95+// wowHDRConfig uses DXVK's semicolon-separated config lines. Each section
96+// matches an exact executable name, keeping HDR away from Battle.net's CEF.
97+const wowHDRConfig = "[WowClassic.exe];dxgi.enableHDR = True;[Wow.exe];dxgi.enableHDR = True;[WowClassicT.exe];dxgi.enableHDR = True;[WowT.exe];dxgi.enableHDR = True"
98+
9599 // BuildGPUEnvForDisplayHDR applies the launch HDR policy.
96100 func BuildGPUEnvForDisplayHDR(driver DisplayDriver, noHDR bool) *domain.WineEnv {
97101 env := &domain.WineEnv{
@@ -116,9 +120,13 @@ func BuildGPUEnvForDisplayHDR(driver DisplayDriver, noHDR bool) *domain.WineEnv
116120 // Without an X11 display, Wine selects its native Wayland driver.
117121 env.Unset = append(env.Unset, "DISPLAY")
118122 if noHDR {
119- env.Vars["DXVK_HDR"] = "0"
123+ env.Unset = append(env.Unset, "DXVK_HDR")
120124 } else if _, set := os.LookupEnv("DXVK_HDR"); !set {
121- env.Vars["DXVK_HDR"] = "1"
125+ config := os.Getenv("DXVK_CONFIG")
126+ if config != "" {
127+ config += ";"
128+ }
129+ env.Vars["DXVK_CONFIG"] = config + wowHDRConfig
122130 }
123131 }
124132
@@ -92,6 +92,10 @@ func BuildGPUEnvForDisplay(driver DisplayDriver) *domain.WineEnv {
92 return BuildGPUEnvForDisplayHDR(driver, false)92 return BuildGPUEnvForDisplayHDR(driver, false)
93 }93 }
94 94
95+// wowHDRConfig uses DXVK's semicolon-separated config lines. Each section
96+// matches an exact executable name, keeping HDR away from Battle.net's CEF.
97+const wowHDRConfig = "[WowClassic.exe];dxgi.enableHDR = True;[Wow.exe];dxgi.enableHDR = True;[WowClassicT.exe];dxgi.enableHDR = True;[WowT.exe];dxgi.enableHDR = True"
98+
95 // BuildGPUEnvForDisplayHDR applies the launch HDR policy.99 // BuildGPUEnvForDisplayHDR applies the launch HDR policy.
96 func BuildGPUEnvForDisplayHDR(driver DisplayDriver, noHDR bool) *domain.WineEnv {100 func BuildGPUEnvForDisplayHDR(driver DisplayDriver, noHDR bool) *domain.WineEnv {
97 env := &domain.WineEnv{101 env := &domain.WineEnv{
@@ -116,9 +120,13 @@ func BuildGPUEnvForDisplayHDR(driver DisplayDriver, noHDR bool) *domain.WineEnv
116 // Without an X11 display, Wine selects its native Wayland driver.120 // Without an X11 display, Wine selects its native Wayland driver.
117 env.Unset = append(env.Unset, "DISPLAY")121 env.Unset = append(env.Unset, "DISPLAY")
118 if noHDR {122 if noHDR {
119- env.Vars["DXVK_HDR"] = "0"123+ env.Unset = append(env.Unset, "DXVK_HDR")
120 } else if _, set := os.LookupEnv("DXVK_HDR"); !set {124 } else if _, set := os.LookupEnv("DXVK_HDR"); !set {
121- env.Vars["DXVK_HDR"] = "1"125+ config := os.Getenv("DXVK_CONFIG")
126+ if config != "" {
127+ config += ";"
128+ }
129+ env.Vars["DXVK_CONFIG"] = config + wowHDRConfig
122 }130 }
123 }131 }
124 132
M internal/adapters/env/hdr_test.go +49 -6
@@ -2,6 +2,7 @@ package env
22
33 import (
44 "os"
5+ "slices"
56 "testing"
67 )
78
@@ -14,11 +15,11 @@ func TestHDRPolicy(t *testing.T) {
1415 want, reason string
1516 enabled bool
1617 }{
17- {"default", DisplayDriverWayland, false, nil, "1", "wayland default", true},
18+ {"default", DisplayDriverWayland, false, nil, wowHDRConfig, "wayland default", true},
1819 {"user on", DisplayDriverWayland, false, str("1"), "", "user DXVK_HDR=1", true},
1920 {"user off", DisplayDriverWayland, false, str("0"), "", "user DXVK_HDR=0", false},
2021 {"user empty", DisplayDriverWayland, false, str(""), "", "user DXVK_HDR=", false},
21- {"opt out", DisplayDriverWayland, true, str("1"), "0", "--no-hdr", false},
22+ {"opt out", DisplayDriverWayland, true, str("1"), "", "--no-hdr", false},
2223 {"x11", DisplayDriverX11, false, nil, "", "x11", false},
2324 {"x11 inherited on", DisplayDriverX11, false, str("1"), "", "user env", true},
2425 {"x11 inherited off", DisplayDriverX11, false, str("0"), "", "user env", false},
@@ -26,6 +27,7 @@ func TestHDRPolicy(t *testing.T) {
2627 {"x11 ignores opt out", DisplayDriverX11, true, str("1"), "", "user env", true},
2728 } {
2829 t.Run(tt.name, func(t *testing.T) {
30+ t.Setenv("DXVK_CONFIG", "")
2931 t.Setenv("DXVK_HDR", "")
3032 if tt.user == nil {
3133 if err := os.Unsetenv("DXVK_HDR"); err != nil {
@@ -35,14 +37,20 @@ func TestHDRPolicy(t *testing.T) {
3537 t.Setenv("DXVK_HDR", *tt.user)
3638 }
3739 got := BuildGPUEnvForDisplayHDR(tt.driver, tt.noHDR)
38- if got.Vars["DXVK_HDR"] != tt.want {
39- t.Fatalf("env: %v", got.Vars)
40+ if _, ok := got.Vars["DXVK_HDR"]; ok {
41+ t.Fatal("unexpected global HDR assignment")
42+ }
43+ if got.Vars["DXVK_CONFIG"] != tt.want {
44+ t.Fatalf("config: %q, want %q", got.Vars["DXVK_CONFIG"], tt.want)
4045 }
4146 if tt.want == "" {
42- if _, ok := got.Vars["DXVK_HDR"]; ok {
43- t.Fatal("unexpected HDR assignment")
47+ if _, ok := got.Vars["DXVK_CONFIG"]; ok {
48+ t.Fatal("unexpected config assignment")
4449 }
4550 }
51+ if unset := slices.Contains(got.Unset, "DXVK_HDR"); unset != (tt.driver == DisplayDriverWayland && tt.noHDR) {
52+ t.Fatalf("unexpected HDR unset policy: %v", got.Unset)
53+ }
4654 enabled, reason := HDRStatus(tt.driver, tt.noHDR)
4755 if enabled != tt.enabled || reason != tt.reason {
4856 t.Fatalf("status: %v %q", enabled, reason)
@@ -51,3 +59,38 @@ func TestHDRPolicy(t *testing.T) {
5159 }
5260 }
5361 func str(s string) *string { return &s }
62+
63+func TestHDRPreservesUserConfig(t *testing.T) {
64+ const entries = "[WowClassic.exe];dxgi.enableHDR = True;[Wow.exe];dxgi.enableHDR = True;[WowClassicT.exe];dxgi.enableHDR = True;[WowT.exe];dxgi.enableHDR = True"
65+ for _, userConfig := range []string{"", "dxgi.maxFrameLatency = 1", "[Battle.net.exe];dxgi.enableHDR = False", "[Other.exe];dxgi.syncInterval = 0;"} {
66+ t.Run(userConfig, func(t *testing.T) {
67+ t.Setenv("DXVK_CONFIG", userConfig)
68+ t.Setenv("DXVK_HDR", "")
69+ if err := os.Unsetenv("DXVK_HDR"); err != nil {
70+ t.Fatal(err)
71+ }
72+ want := entries
73+ if userConfig != "" {
74+ want = userConfig + ";" + entries
75+ }
76+ got := BuildGPUEnvForDisplayHDR(DisplayDriverWayland, false)
77+ if got.Vars["DXVK_CONFIG"] != want {
78+ t.Fatalf("config: %q, want %q", got.Vars["DXVK_CONFIG"], want)
79+ }
80+ got = BuildGPUEnvForDisplayHDR(DisplayDriverWayland, true)
81+ if _, ok := got.Vars["DXVK_CONFIG"]; ok {
82+ t.Fatal("opt out must not add config without inherited DXVK_HDR")
83+ }
84+ for _, noHDR := range []bool{false, true} {
85+ t.Setenv("DXVK_HDR", "1")
86+ got = BuildGPUEnvForDisplayHDR(DisplayDriverWayland, noHDR)
87+ if _, ok := got.Vars["DXVK_CONFIG"]; ok {
88+ t.Fatal("must leave inherited config untouched")
89+ }
90+ if os.Getenv("DXVK_CONFIG") != userConfig {
91+ t.Fatal("changed user environment")
92+ }
93+ }
94+ })
95+ }
96+}
@@ -2,6 +2,7 @@ package env
2 2
3 import (3 import (
4 "os"4 "os"
5+ "slices"
5 "testing"6 "testing"
6 )7 )
7 8
@@ -14,11 +15,11 @@ func TestHDRPolicy(t *testing.T) {
14 want, reason string15 want, reason string
15 enabled bool16 enabled bool
16 }{17 }{
17- {"default", DisplayDriverWayland, false, nil, "1", "wayland default", true},18+ {"default", DisplayDriverWayland, false, nil, wowHDRConfig, "wayland default", true},
18 {"user on", DisplayDriverWayland, false, str("1"), "", "user DXVK_HDR=1", true},19 {"user on", DisplayDriverWayland, false, str("1"), "", "user DXVK_HDR=1", true},
19 {"user off", DisplayDriverWayland, false, str("0"), "", "user DXVK_HDR=0", false},20 {"user off", DisplayDriverWayland, false, str("0"), "", "user DXVK_HDR=0", false},
20 {"user empty", DisplayDriverWayland, false, str(""), "", "user DXVK_HDR=", false},21 {"user empty", DisplayDriverWayland, false, str(""), "", "user DXVK_HDR=", false},
21- {"opt out", DisplayDriverWayland, true, str("1"), "0", "--no-hdr", false},22+ {"opt out", DisplayDriverWayland, true, str("1"), "", "--no-hdr", false},
22 {"x11", DisplayDriverX11, false, nil, "", "x11", false},23 {"x11", DisplayDriverX11, false, nil, "", "x11", false},
23 {"x11 inherited on", DisplayDriverX11, false, str("1"), "", "user env", true},24 {"x11 inherited on", DisplayDriverX11, false, str("1"), "", "user env", true},
24 {"x11 inherited off", DisplayDriverX11, false, str("0"), "", "user env", false},25 {"x11 inherited off", DisplayDriverX11, false, str("0"), "", "user env", false},
@@ -26,6 +27,7 @@ func TestHDRPolicy(t *testing.T) {
26 {"x11 ignores opt out", DisplayDriverX11, true, str("1"), "", "user env", true},27 {"x11 ignores opt out", DisplayDriverX11, true, str("1"), "", "user env", true},
27 } {28 } {
28 t.Run(tt.name, func(t *testing.T) {29 t.Run(tt.name, func(t *testing.T) {
30+ t.Setenv("DXVK_CONFIG", "")
29 t.Setenv("DXVK_HDR", "")31 t.Setenv("DXVK_HDR", "")
30 if tt.user == nil {32 if tt.user == nil {
31 if err := os.Unsetenv("DXVK_HDR"); err != nil {33 if err := os.Unsetenv("DXVK_HDR"); err != nil {
@@ -35,14 +37,20 @@ func TestHDRPolicy(t *testing.T) {
35 t.Setenv("DXVK_HDR", *tt.user)37 t.Setenv("DXVK_HDR", *tt.user)
36 }38 }
37 got := BuildGPUEnvForDisplayHDR(tt.driver, tt.noHDR)39 got := BuildGPUEnvForDisplayHDR(tt.driver, tt.noHDR)
38- if got.Vars["DXVK_HDR"] != tt.want {40+ if _, ok := got.Vars["DXVK_HDR"]; ok {
39- t.Fatalf("env: %v", got.Vars)41+ t.Fatal("unexpected global HDR assignment")
42+ }
43+ if got.Vars["DXVK_CONFIG"] != tt.want {
44+ t.Fatalf("config: %q, want %q", got.Vars["DXVK_CONFIG"], tt.want)
40 }45 }
41 if tt.want == "" {46 if tt.want == "" {
42- if _, ok := got.Vars["DXVK_HDR"]; ok {47+ if _, ok := got.Vars["DXVK_CONFIG"]; ok {
43- t.Fatal("unexpected HDR assignment")48+ t.Fatal("unexpected config assignment")
44 }49 }
45 }50 }
51+ if unset := slices.Contains(got.Unset, "DXVK_HDR"); unset != (tt.driver == DisplayDriverWayland && tt.noHDR) {
52+ t.Fatalf("unexpected HDR unset policy: %v", got.Unset)
53+ }
46 enabled, reason := HDRStatus(tt.driver, tt.noHDR)54 enabled, reason := HDRStatus(tt.driver, tt.noHDR)
47 if enabled != tt.enabled || reason != tt.reason {55 if enabled != tt.enabled || reason != tt.reason {
48 t.Fatalf("status: %v %q", enabled, reason)56 t.Fatalf("status: %v %q", enabled, reason)
@@ -51,3 +59,38 @@ func TestHDRPolicy(t *testing.T) {
51 }59 }
52 }60 }
53 func str(s string) *string { return &s }61 func str(s string) *string { return &s }
62+
63+func TestHDRPreservesUserConfig(t *testing.T) {
64+ const entries = "[WowClassic.exe];dxgi.enableHDR = True;[Wow.exe];dxgi.enableHDR = True;[WowClassicT.exe];dxgi.enableHDR = True;[WowT.exe];dxgi.enableHDR = True"
65+ for _, userConfig := range []string{"", "dxgi.maxFrameLatency = 1", "[Battle.net.exe];dxgi.enableHDR = False", "[Other.exe];dxgi.syncInterval = 0;"} {
66+ t.Run(userConfig, func(t *testing.T) {
67+ t.Setenv("DXVK_CONFIG", userConfig)
68+ t.Setenv("DXVK_HDR", "")
69+ if err := os.Unsetenv("DXVK_HDR"); err != nil {
70+ t.Fatal(err)
71+ }
72+ want := entries
73+ if userConfig != "" {
74+ want = userConfig + ";" + entries
75+ }
76+ got := BuildGPUEnvForDisplayHDR(DisplayDriverWayland, false)
77+ if got.Vars["DXVK_CONFIG"] != want {
78+ t.Fatalf("config: %q, want %q", got.Vars["DXVK_CONFIG"], want)
79+ }
80+ got = BuildGPUEnvForDisplayHDR(DisplayDriverWayland, true)
81+ if _, ok := got.Vars["DXVK_CONFIG"]; ok {
82+ t.Fatal("opt out must not add config without inherited DXVK_HDR")
83+ }
84+ for _, noHDR := range []bool{false, true} {
85+ t.Setenv("DXVK_HDR", "1")
86+ got = BuildGPUEnvForDisplayHDR(DisplayDriverWayland, noHDR)
87+ if _, ok := got.Vars["DXVK_CONFIG"]; ok {
88+ t.Fatal("must leave inherited config untouched")
89+ }
90+ if os.Getenv("DXVK_CONFIG") != userConfig {
91+ t.Fatal("changed user environment")
92+ }
93+ }
94+ })
95+ }
96+}

Comments00 unresolved

    no comments yet.

    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. On pages with a side panel: into the 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; in a file, click a line number to select it, shift-click for a range
    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
    l
    merge request: the list of files (j k move, enter opens, esc back)
    M
    merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
    R
    merge request: mark the head read
    esc
    close a dialog, leave a field

    Branches 2

    Tags 2

    ↑↓ moveenter openesc close