From 2c6763f01da0a5c8f3a59fd44680e47b5f8d3f4f Mon Sep 17 00:00:00 2001 From: Luke Boucher Date: Wed, 24 Jun 2026 19:50:53 +0100 Subject: [PATCH 1/6] Add XLIB imports needed for WM_CLASS interactions. --- src/BizHawk.Common/LSB/XlibImports.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/BizHawk.Common/LSB/XlibImports.cs b/src/BizHawk.Common/LSB/XlibImports.cs index 23cdcbb297f..45952a601ca 100644 --- a/src/BizHawk.Common/LSB/XlibImports.cs +++ b/src/BizHawk.Common/LSB/XlibImports.cs @@ -715,5 +715,27 @@ public struct XkbDescRec [DllImport(XLIB)] [return: MarshalAs(UnmanagedType.SysUInt)] public static extern Keysym XkbKeycodeToKeysym(IntPtr display, uint keycode, int group, int level); + + [DllImport(XLIB)] + public static extern IntPtr XAllocClassHint(); + + [DllImport(XLIB)] + public static extern Status XSetClassHint(IntPtr display, IntPtr window, IntPtr classHint); + + [DllImport(XLIB)] + public static extern Status XGetClassHint(IntPtr display, IntPtr window, out IntPtr classHint); + + public struct XClassHint + { + public IntPtr res_name; + public IntPtr res_class; + } + + [DllImport(XLIB)] + public static extern Status XQueryTree(IntPtr display, IntPtr window, out IntPtr rootReturn, out IntPtr parentReturn, out IntPtr[] childrenReturn, out int childCount); + + [DllImport(XLIB)] + public static extern Status XFetchName(IntPtr display, IntPtr window, out string returnedName); + } } From 95028e82357a603ee20be1d67b419c5df504ee1c Mon Sep 17 00:00:00 2001 From: Luke Boucher Date: Wed, 24 Jun 2026 21:10:59 +0100 Subject: [PATCH 2/6] Add WM_CLASS and call it once from the main loop after rendering --- src/BizHawk.Client.EmuHawk/MainForm.cs | 9 ++++++++ src/BizHawk.Common/OSTailoredCode.cs | 31 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 13b08dd0b01..4d9d1c639cc 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -914,6 +914,8 @@ private void CheckMayCloseAndCleanup(object/*?*/ closingSender, CancelEventArgs /// private bool _skipNextAltRelease = true; + private bool _runWMSetOnce = true; + public int ProgramRunLoop() { // needs to be done late, after the log console snaps on top @@ -1015,6 +1017,13 @@ public int ProgramRunLoop() Render(); StepRunLoop_Throttle(); + //Code to set WM_CLASS only need be run once, but must be run after the application has rendered at least once + if(_runWMSetOnce && OSTailoredCode.CurrentOS==OSTailoredCode.DistinctOS.Linux) + { + OSTailoredCode.SetWMClass(_x11Display,"BizHawk"); + _runWMSetOnce = false; + } + // HACK: RAIntegration might peek at memory during messages // we need this to allow memory access here, otherwise it will deadlock var raMemHack = (RA as RAIntegration)?.ThisIsTheRAMemHack(); diff --git a/src/BizHawk.Common/OSTailoredCode.cs b/src/BizHawk.Common/OSTailoredCode.cs index f5c4f2e2ea6..fad95279ef2 100644 --- a/src/BizHawk.Common/OSTailoredCode.cs +++ b/src/BizHawk.Common/OSTailoredCode.cs @@ -292,5 +292,36 @@ public static string SimpleSubshell(string cmd, string args, string noOutputMsg) if (stdout.EndOfStream) throw new Exception($"{noOutputMsg} ({cmd} wrote nothing to stdout)"); return stdout.ReadLine()!; } + + public static void SetWMClass(IntPtr x11Display, string className) + { + //Setup the ClassHint + var wmSet = new XlibImports.XClassHint{res_name = Marshal.StringToCoTaskMemAnsi("BizHawk"), res_class = Marshal.StringToCoTaskMemAnsi(className)}; + IntPtr point = XlibImports.XAllocClassHint(); + Marshal.StructureToPtr(wmSet, point, true); + + //To find the window we must query twice, once to get the array length and the second to populate + XlibImports.XQueryTree(x11Display, XlibImports.XDefaultRootWindow(x11Display), out _, out _, out _, out int windowCount); + IntPtr[] windowList = new IntPtr[windowCount]; + XlibImports.XQueryTree(x11Display, XlibImports.XDefaultRootWindow(x11Display), out _, out _, out windowList, out windowCount); + for(int windowIterator = 0; windowIterator < windowCount; windowIterator++) + { + XlibImports.XFetchName(x11Display, windowList[windowIterator], out string name); + if(name?.Contains("BizHawk") == true) + { + //A bizhawk window found: + //Interogate if a WM_CLASS is already set + XlibImports.XGetClassHint(x11Display, windowList[windowIterator], out var classHint); + if(classHint == IntPtr.Zero) + { + //Assign+Retrieve - doesn't seem to set properly without retrieving it after) + XlibImports.XSetClassHint(x11Display, windowList[windowIterator], point); + XlibImports.XGetClassHint(x11Display, windowList[windowIterator],out _); + } + //Reset classHint after checking, doesn't seem to overwrite otherwise. + classHint = IntPtr.Zero; + } + } + } } } From 5171202b9cbb3b3dccec2b8265e5de2c8b30fe9a Mon Sep 17 00:00:00 2001 From: Luke Boucher Date: Wed, 24 Jun 2026 21:49:31 +0100 Subject: [PATCH 3/6] Add support for passing WM_CLASS via CLI --- src/BizHawk.Client.Common/ArgParser.cs | 9 ++++++++- src/BizHawk.Client.Common/ParsedCLIFlags.cs | 6 +++++- src/BizHawk.Client.EmuHawk/MainForm.cs | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/BizHawk.Client.Common/ArgParser.cs b/src/BizHawk.Client.Common/ArgParser.cs index c7e8393ac56..b4709035fac 100644 --- a/src/BizHawk.Client.Common/ArgParser.cs +++ b/src/BizHawk.Client.Common/ArgParser.cs @@ -117,6 +117,11 @@ public static class ArgParser Description = "pairs in the format `k1:v1;k2:v2` (mind your shell escape sequences); if the value is `true`/`false` it's interpreted as a boolean, if it's a valid 32-bit signed integer e.g. `-1234` it's interpreted as such, if it's a valid 32-bit float e.g. `12.34` it's interpreted as such, else it's interpreted as a string", }; + private static readonly Option OptionWMClass = new ("--wmclass") + { + Description = "set a custom WM_CLASS for this Bizhawk initiation, Linux only." + }; + static ArgParser() { OptionAVDumpFrameList.Description = $"comma-separated list of integers, indices of frames which should be included in the A/V dump (encoding); implies `{OptionAVDumpEndAtFrame.Name}=` where `` is the highest frame listed"; @@ -162,6 +167,7 @@ private static RootCommand GetRootCommand() root.Add(/* --url-post */ OptionHTTPClientURIPOST); root.Add(/* --userdata */ OptionUserdataUnparsedPairs); root.Add(/* --version */ OptionQueryAppVersion); + root.Add(/* --wmclass */ OptionWMClass); return root; } @@ -268,7 +274,8 @@ private static void EnsureConsole() openExtToolDll: result.GetValue(OptionOpenExternalTool), socketProtocol: result.GetValue(OptionSocketServerUseUDP) ? ProtocolType.Udp : ProtocolType.Tcp, userdataUnparsedPairs: userdataUnparsedPairs, - cmdRom: result.GetValue(ArgumentRomFilePath) + cmdRom: result.GetValue(ArgumentRomFilePath), + wmClassName: result.GetValue(OptionWMClass) ); return null; } diff --git a/src/BizHawk.Client.Common/ParsedCLIFlags.cs b/src/BizHawk.Client.Common/ParsedCLIFlags.cs index 81dd4aa8b97..083bc8c01c2 100644 --- a/src/BizHawk.Client.Common/ParsedCLIFlags.cs +++ b/src/BizHawk.Client.Common/ParsedCLIFlags.cs @@ -51,6 +51,8 @@ public readonly struct ParsedCLIFlags public readonly string? cmdRom; + public readonly string? wmClassName; + public ParsedCLIFlags( int? cmdLoadSlot, string? cmdLoadState, @@ -73,7 +75,8 @@ public ParsedCLIFlags( string? openExtToolDll, ProtocolType socketProtocol, IReadOnlyList<(string Key, string Value)>? userdataUnparsedPairs, - string? cmdRom) + string? cmdRom, + string? wmClassName) { this.cmdLoadSlot = cmdLoadSlot; this.cmdLoadState = cmdLoadState; @@ -97,6 +100,7 @@ public ParsedCLIFlags( SocketProtocol = socketProtocol; UserdataUnparsedPairs = userdataUnparsedPairs; this.cmdRom = cmdRom; + this.wmClassName = wmClassName; } } } diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index 4d9d1c639cc..dec82606e17 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -1020,7 +1020,8 @@ public int ProgramRunLoop() //Code to set WM_CLASS only need be run once, but must be run after the application has rendered at least once if(_runWMSetOnce && OSTailoredCode.CurrentOS==OSTailoredCode.DistinctOS.Linux) { - OSTailoredCode.SetWMClass(_x11Display,"BizHawk"); + string wmclass = _argParser.wmClassName is not null ? _argParser.wmClassName : "BizHawk"; + OSTailoredCode.SetWMClass(_x11Display,wmclass); _runWMSetOnce = false; } From d9a09ebe20d833728e0186b4f6158f90048f634e Mon Sep 17 00:00:00 2001 From: Luke Boucher Date: Wed, 24 Jun 2026 21:58:20 +0100 Subject: [PATCH 4/6] Linting fixes --- src/BizHawk.Common/LSB/XlibImports.cs | 9 +++++---- src/BizHawk.Common/OSTailoredCode.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/BizHawk.Common/LSB/XlibImports.cs b/src/BizHawk.Common/LSB/XlibImports.cs index 45952a601ca..2f7590b337e 100644 --- a/src/BizHawk.Common/LSB/XlibImports.cs +++ b/src/BizHawk.Common/LSB/XlibImports.cs @@ -716,7 +716,7 @@ public struct XkbDescRec [return: MarshalAs(UnmanagedType.SysUInt)] public static extern Keysym XkbKeycodeToKeysym(IntPtr display, uint keycode, int group, int level); - [DllImport(XLIB)] + [DllImport(XLIB)] public static extern IntPtr XAllocClassHint(); [DllImport(XLIB)] @@ -727,15 +727,16 @@ public struct XkbDescRec public struct XClassHint { - public IntPtr res_name; + public IntPtr res_name; public IntPtr res_class; } [DllImport(XLIB)] public static extern Status XQueryTree(IntPtr display, IntPtr window, out IntPtr rootReturn, out IntPtr parentReturn, out IntPtr[] childrenReturn, out int childCount); - + [DllImport(XLIB)] public static extern Status XFetchName(IntPtr display, IntPtr window, out string returnedName); - + } + } diff --git a/src/BizHawk.Common/OSTailoredCode.cs b/src/BizHawk.Common/OSTailoredCode.cs index fad95279ef2..5705b042434 100644 --- a/src/BizHawk.Common/OSTailoredCode.cs +++ b/src/BizHawk.Common/OSTailoredCode.cs @@ -304,7 +304,7 @@ public static void SetWMClass(IntPtr x11Display, string className) XlibImports.XQueryTree(x11Display, XlibImports.XDefaultRootWindow(x11Display), out _, out _, out _, out int windowCount); IntPtr[] windowList = new IntPtr[windowCount]; XlibImports.XQueryTree(x11Display, XlibImports.XDefaultRootWindow(x11Display), out _, out _, out windowList, out windowCount); - for(int windowIterator = 0; windowIterator < windowCount; windowIterator++) + for(int windowIterator = 0; windowIterator < windowCount; windowIterator++) { XlibImports.XFetchName(x11Display, windowList[windowIterator], out string name); if(name?.Contains("BizHawk") == true) From 86ef7f85e87eb7adb6f1ada29f7b781df12ff609 Mon Sep 17 00:00:00 2001 From: Luke Boucher Date: Wed, 24 Jun 2026 22:06:01 +0100 Subject: [PATCH 5/6] Further linting fixes --- src/BizHawk.Client.Common/ArgParser.cs | 2 +- src/BizHawk.Common/LSB/XlibImports.cs | 5 +---- src/BizHawk.Common/OSTailoredCode.cs | 1 - 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/BizHawk.Client.Common/ArgParser.cs b/src/BizHawk.Client.Common/ArgParser.cs index b4709035fac..0eae84f733c 100644 --- a/src/BizHawk.Client.Common/ArgParser.cs +++ b/src/BizHawk.Client.Common/ArgParser.cs @@ -119,7 +119,7 @@ public static class ArgParser private static readonly Option OptionWMClass = new ("--wmclass") { - Description = "set a custom WM_CLASS for this Bizhawk initiation, Linux only." + Description = "set a custom WM_CLASS for this Bizhawk initiation, Linux only.", }; static ArgParser() diff --git a/src/BizHawk.Common/LSB/XlibImports.cs b/src/BizHawk.Common/LSB/XlibImports.cs index 2f7590b337e..edd4a6433b9 100644 --- a/src/BizHawk.Common/LSB/XlibImports.cs +++ b/src/BizHawk.Common/LSB/XlibImports.cs @@ -715,7 +715,7 @@ public struct XkbDescRec [DllImport(XLIB)] [return: MarshalAs(UnmanagedType.SysUInt)] public static extern Keysym XkbKeycodeToKeysym(IntPtr display, uint keycode, int group, int level); - + [DllImport(XLIB)] public static extern IntPtr XAllocClassHint(); @@ -733,10 +733,7 @@ public struct XClassHint [DllImport(XLIB)] public static extern Status XQueryTree(IntPtr display, IntPtr window, out IntPtr rootReturn, out IntPtr parentReturn, out IntPtr[] childrenReturn, out int childCount); - [DllImport(XLIB)] public static extern Status XFetchName(IntPtr display, IntPtr window, out string returnedName); - } - } diff --git a/src/BizHawk.Common/OSTailoredCode.cs b/src/BizHawk.Common/OSTailoredCode.cs index 5705b042434..a21a30d4da3 100644 --- a/src/BizHawk.Common/OSTailoredCode.cs +++ b/src/BizHawk.Common/OSTailoredCode.cs @@ -309,7 +309,6 @@ public static void SetWMClass(IntPtr x11Display, string className) XlibImports.XFetchName(x11Display, windowList[windowIterator], out string name); if(name?.Contains("BizHawk") == true) { - //A bizhawk window found: //Interogate if a WM_CLASS is already set XlibImports.XGetClassHint(x11Display, windowList[windowIterator], out var classHint); if(classHint == IntPtr.Zero) From 8cb4922aae94ceaaded0fb6bd120446b1938276c Mon Sep 17 00:00:00 2001 From: Luke Boucher Date: Thu, 25 Jun 2026 11:00:20 +0100 Subject: [PATCH 6/6] Review changes --- src/BizHawk.Client.EmuHawk/MainForm.cs | 17 +++++------- src/BizHawk.Common/LSB/XlibImports.cs | 36 ++++++++++++++++++++++++++ src/BizHawk.Common/OSTailoredCode.cs | 30 --------------------- 3 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/BizHawk.Client.EmuHawk/MainForm.cs b/src/BizHawk.Client.EmuHawk/MainForm.cs index dec82606e17..22bf715fe8d 100644 --- a/src/BizHawk.Client.EmuHawk/MainForm.cs +++ b/src/BizHawk.Client.EmuHawk/MainForm.cs @@ -295,6 +295,13 @@ private void MainForm_Load(object sender, EventArgs e) else Console.WriteLine($"requested ext. tool dll {requestedExtToolDll} could not be loaded"); } + //Code to set WM_CLASS + if(OSTailoredCode.CurrentOS==OSTailoredCode.DistinctOS.Linux) + { + string wmclass = _argParser.wmClassName is not null ? _argParser.wmClassName : "BizHawk"; + XlibImports.SetWMClass(_x11Display, wmclass); + } + #if DEBUG AddDebugMenu(); #endif @@ -914,8 +921,6 @@ private void CheckMayCloseAndCleanup(object/*?*/ closingSender, CancelEventArgs /// private bool _skipNextAltRelease = true; - private bool _runWMSetOnce = true; - public int ProgramRunLoop() { // needs to be done late, after the log console snaps on top @@ -1017,14 +1022,6 @@ public int ProgramRunLoop() Render(); StepRunLoop_Throttle(); - //Code to set WM_CLASS only need be run once, but must be run after the application has rendered at least once - if(_runWMSetOnce && OSTailoredCode.CurrentOS==OSTailoredCode.DistinctOS.Linux) - { - string wmclass = _argParser.wmClassName is not null ? _argParser.wmClassName : "BizHawk"; - OSTailoredCode.SetWMClass(_x11Display,wmclass); - _runWMSetOnce = false; - } - // HACK: RAIntegration might peek at memory during messages // we need this to allow memory access here, otherwise it will deadlock var raMemHack = (RA as RAIntegration)?.ThisIsTheRAMemHack(); diff --git a/src/BizHawk.Common/LSB/XlibImports.cs b/src/BizHawk.Common/LSB/XlibImports.cs index edd4a6433b9..1df38aab24d 100644 --- a/src/BizHawk.Common/LSB/XlibImports.cs +++ b/src/BizHawk.Common/LSB/XlibImports.cs @@ -735,5 +735,41 @@ public struct XClassHint public static extern Status XQueryTree(IntPtr display, IntPtr window, out IntPtr rootReturn, out IntPtr parentReturn, out IntPtr[] childrenReturn, out int childCount); [DllImport(XLIB)] public static extern Status XFetchName(IntPtr display, IntPtr window, out string returnedName); + + public static void SetWMClass(IntPtr x11Display, string className) + { + //Establish reference to xDisplay + if(x11Display == IntPtr.Zero) + { + x11Display = XOpenDisplay(null); + } + //Setup the ClassHint + var wmSet = new XClassHint{res_name = Marshal.StringToCoTaskMemAnsi("BizHawk"), res_class = Marshal.StringToCoTaskMemAnsi(className)}; + IntPtr point = XAllocClassHint(); + Marshal.StructureToPtr(wmSet, point, true); + + //To find the window we must query twice, once to get the array length and the second to populate + XQueryTree(x11Display, XDefaultRootWindow(x11Display), out _, out _, out _, out int windowCount); + IntPtr[] windowList = new IntPtr[windowCount]; + XQueryTree(x11Display, XDefaultRootWindow(x11Display), out _, out _, out windowList, out windowCount); + for(int windowIterator = 0; windowIterator < windowCount; windowIterator++) + { + XFetchName(x11Display, windowList[windowIterator], out string name); + if(name?.Contains("BizHawk") == true) + { + //Interogate if a WM_CLASS is already set + XGetClassHint(x11Display, windowList[windowIterator], out var classHint); + if(classHint == IntPtr.Zero) + { + //Assign+Retrieve - doesn't seem to set properly without retrieving it after) + XSetClassHint(x11Display, windowList[windowIterator], point); + XGetClassHint(x11Display, windowList[windowIterator],out _); + } + //Reset classHint after checking, doesn't seem to overwrite otherwise. + classHint = IntPtr.Zero; + } + } + _ = XFlush(x11Display); + } } } diff --git a/src/BizHawk.Common/OSTailoredCode.cs b/src/BizHawk.Common/OSTailoredCode.cs index a21a30d4da3..f5c4f2e2ea6 100644 --- a/src/BizHawk.Common/OSTailoredCode.cs +++ b/src/BizHawk.Common/OSTailoredCode.cs @@ -292,35 +292,5 @@ public static string SimpleSubshell(string cmd, string args, string noOutputMsg) if (stdout.EndOfStream) throw new Exception($"{noOutputMsg} ({cmd} wrote nothing to stdout)"); return stdout.ReadLine()!; } - - public static void SetWMClass(IntPtr x11Display, string className) - { - //Setup the ClassHint - var wmSet = new XlibImports.XClassHint{res_name = Marshal.StringToCoTaskMemAnsi("BizHawk"), res_class = Marshal.StringToCoTaskMemAnsi(className)}; - IntPtr point = XlibImports.XAllocClassHint(); - Marshal.StructureToPtr(wmSet, point, true); - - //To find the window we must query twice, once to get the array length and the second to populate - XlibImports.XQueryTree(x11Display, XlibImports.XDefaultRootWindow(x11Display), out _, out _, out _, out int windowCount); - IntPtr[] windowList = new IntPtr[windowCount]; - XlibImports.XQueryTree(x11Display, XlibImports.XDefaultRootWindow(x11Display), out _, out _, out windowList, out windowCount); - for(int windowIterator = 0; windowIterator < windowCount; windowIterator++) - { - XlibImports.XFetchName(x11Display, windowList[windowIterator], out string name); - if(name?.Contains("BizHawk") == true) - { - //Interogate if a WM_CLASS is already set - XlibImports.XGetClassHint(x11Display, windowList[windowIterator], out var classHint); - if(classHint == IntPtr.Zero) - { - //Assign+Retrieve - doesn't seem to set properly without retrieving it after) - XlibImports.XSetClassHint(x11Display, windowList[windowIterator], point); - XlibImports.XGetClassHint(x11Display, windowList[windowIterator],out _); - } - //Reset classHint after checking, doesn't seem to overwrite otherwise. - classHint = IntPtr.Zero; - } - } - } } }