diff --git a/ClientPlugin/Compatibility/MySdlSplashScreen.cs b/ClientPlugin/Compatibility/MySdlSplashScreen.cs index 7f2fcd3..e0d984e 100644 --- a/ClientPlugin/Compatibility/MySdlSplashScreen.cs +++ b/ClientPlugin/Compatibility/MySdlSplashScreen.cs @@ -23,6 +23,8 @@ internal sealed class MySdlSplashScreen : IDisposable private const ulong SdlWindowAlwaysOnTop = 0x8000uL; + private const ulong SdlWindowHighPixelDensity = 0x2000uL; + // SDL_PIXELFORMAT_RGBA32 is the byte-array RGBA alias on little-endian platforms. private const uint SdlPixelFormatRgba32 = 0x16762004u; @@ -55,6 +57,9 @@ internal static void Show(string image, string gameIcon, Vector2 scale) /// internal static void Hide() { + if (!SdlRenderThread.IsInitialized) + return; + SdlRenderThread.Invoke(() => { s_current?.Dispose(); @@ -87,32 +92,88 @@ private MySdlSplashScreen(string image, string gameIcon, Vector2 scale) using Image sourceImage = Image.Load(path); int width = Math.Max(1, (int)MathF.Round(sourceImage.Width * scale.X)); int height = Math.Max(1, (int)MathF.Round(sourceImage.Height * scale.Y)); + float contentScale = 1f; + if (!SdlRenderThread.IsWayland) + { + contentScale = GetDisplayContentScale(GetPrimaryDisplay()); + if (contentScale <= 0f || !float.IsFinite(contentScale)) + contentScale = 1f; + width = Math.Max(1, (int)MathF.Round(width * contentScale)); + height = Math.Max(1, (int)MathF.Round(height * contentScale)); + } + + m_windowHandle = CreateWindow( + "Space Engineers", + width, + height, + SdlWindowBorderless + | SdlWindowAlwaysOnTop + | SdlWindowHidden + | SdlWindowHighPixelDensity + ); + if (m_windowHandle == IntPtr.Zero) + { + Console.WriteLine($"[LinuxCompat] SDL_CreateWindow failed: {GetErrorString()}"); + return; + } + + SetWindowAlwaysOnTop(m_windowHandle, true); + SdlIconHelper.Apply(m_windowHandle, gameIcon); + SetWindowPosition(m_windowHandle, SdlWindowPosCentered, SdlWindowPosCentered); + + if (SdlRenderThread.IsWayland) + { + if (!ShowWindow(m_windowHandle)) + { + Console.WriteLine($"[LinuxCompat] SDL_ShowWindow failed: {GetErrorString()}"); + return; + } + + if (!SyncWindow(m_windowHandle)) + { + Console.WriteLine($"[LinuxCompat] SDL_SyncWindow failed: {GetErrorString()}"); + return; + } + } + + if ( + !GetWindowSizeInPixels(m_windowHandle, out int pixelWidth, out int pixelHeight) + || pixelWidth <= 0 + || pixelHeight <= 0 + ) + { + pixelWidth = width; + pixelHeight = height; + } using Image splashImage = sourceImage.Clone(context => { - if (sourceImage.Width != width || sourceImage.Height != height) + if (sourceImage.Width != pixelWidth || sourceImage.Height != pixelHeight) { - context.Resize(width, height); + context.Resize(pixelWidth, pixelHeight); } }); - m_pixelData = new byte[width * height * 4]; + m_pixelData = new byte[pixelWidth * pixelHeight * 4]; Span destSpan = MemoryMarshal.Cast(m_pixelData.AsSpan()); - for (int y = 0; y < height; y++) + for (int y = 0; y < pixelHeight; y++) { Span row = splashImage.Frames[0].GetPixelRowSpan(y); - row.CopyTo(destSpan.Slice(y * width, width)); + row.CopyTo(destSpan.Slice(y * pixelWidth, pixelWidth)); } m_pixelDataHandle = GCHandle.Alloc(m_pixelData, GCHandleType.Pinned); - Console.WriteLine($"[LinuxCompat] Splash image loaded: {width}x{height}"); + Console.WriteLine( + $"[LinuxCompat] Splash image loaded: window={width}x{height} pixels={pixelWidth}x{pixelHeight} " + + $"contentScale={contentScale:F2}" + ); IntPtr surface = CreateSurfaceFrom( - width, - height, + pixelWidth, + pixelHeight, SdlPixelFormatRgba32, m_pixelDataHandle.AddrOfPinnedObject(), - width * 4 + pixelWidth * 4 ); if (surface == IntPtr.Zero) { @@ -125,30 +186,14 @@ private MySdlSplashScreen(string image, string gameIcon, Vector2 scale) try { - m_windowHandle = CreateWindow( - "Space Engineers", - width, - height, - SdlWindowBorderless | SdlWindowAlwaysOnTop | SdlWindowHidden - ); - if (m_windowHandle == IntPtr.Zero) - { - Console.WriteLine($"[LinuxCompat] SDL_CreateWindow failed: {GetErrorString()}"); - return; - } Console.WriteLine( $"[LinuxCompat] Splash window created: 0x{m_windowHandle.ToInt64():X}" ); - SetWindowAlwaysOnTop(m_windowHandle, true); - SdlIconHelper.Apply(m_windowHandle, gameIcon); - SetWindowPosition(m_windowHandle, SdlWindowPosCentered, SdlWindowPosCentered); IntPtr windowSurface = GetWindowSurface(m_windowHandle); Console.WriteLine( $"[LinuxCompat] Splash window surface: 0x{windowSurface.ToInt64():X}" ); - bool shown = ShowWindow(m_windowHandle); - Console.WriteLine($"[LinuxCompat] SDL_ShowWindow returned {shown}"); if (windowSurface == IntPtr.Zero) { Console.WriteLine( @@ -165,6 +210,19 @@ private MySdlSplashScreen(string image, string gameIcon, Vector2 scale) return; } + if (!SdlRenderThread.IsWayland) + { + bool shown = ShowWindow(m_windowHandle); + Console.WriteLine($"[LinuxCompat] SDL_ShowWindow returned {shown}"); + if (!shown) + { + Console.WriteLine( + $"[LinuxCompat] SDL_ShowWindow failed: {GetErrorString()}" + ); + return; + } + } + bool updated = UpdateWindowSurface(m_windowHandle); Console.WriteLine($"[LinuxCompat] SDL_UpdateWindowSurface returned {updated}"); if (!updated) @@ -230,17 +288,34 @@ public void Dispose() private static extern void DestroyWindow(IntPtr window); [DllImport(Lib, EntryPoint = "SDL_ShowWindow")] + [return: MarshalAs(UnmanagedType.I1)] private static extern bool ShowWindow(IntPtr window); + [DllImport(Lib, EntryPoint = "SDL_SyncWindow")] + [return: MarshalAs(UnmanagedType.I1)] + private static extern bool SyncWindow(IntPtr window); + [DllImport(Lib, EntryPoint = "SDL_SetWindowAlwaysOnTop")] + [return: MarshalAs(UnmanagedType.I1)] private static extern bool SetWindowAlwaysOnTop( IntPtr window, [MarshalAs(UnmanagedType.I1)] bool onTop ); [DllImport(Lib, EntryPoint = "SDL_SetWindowPosition")] + [return: MarshalAs(UnmanagedType.I1)] private static extern bool SetWindowPosition(IntPtr window, int x, int y); + [DllImport(Lib, EntryPoint = "SDL_GetWindowSizeInPixels")] + [return: MarshalAs(UnmanagedType.I1)] + private static extern bool GetWindowSizeInPixels(IntPtr window, out int width, out int height); + + [DllImport(Lib, EntryPoint = "SDL_GetPrimaryDisplay")] + private static extern uint GetPrimaryDisplay(); + + [DllImport(Lib, EntryPoint = "SDL_GetDisplayContentScale")] + private static extern float GetDisplayContentScale(uint displayId); + [DllImport(Lib, EntryPoint = "SDL_CreateSurfaceFrom")] private static extern IntPtr CreateSurfaceFrom( int width, @@ -254,6 +329,7 @@ int pitch private static extern IntPtr GetWindowSurface(IntPtr window); [DllImport(Lib, EntryPoint = "SDL_BlitSurface")] + [return: MarshalAs(UnmanagedType.I1)] private static extern bool BlitSurface( IntPtr source, IntPtr sourceRect, @@ -262,6 +338,7 @@ IntPtr destinationRect ); [DllImport(Lib, EntryPoint = "SDL_UpdateWindowSurface")] + [return: MarshalAs(UnmanagedType.I1)] private static extern bool UpdateWindowSurface(IntPtr window); [DllImport(Lib, EntryPoint = "SDL_DestroySurface")] diff --git a/ClientPlugin/Compatibility/PluginWindowConfig.cs b/ClientPlugin/Compatibility/PluginWindowConfig.cs index 6e16e43..5b7baf4 100644 --- a/ClientPlugin/Compatibility/PluginWindowConfig.cs +++ b/ClientPlugin/Compatibility/PluginWindowConfig.cs @@ -6,9 +6,10 @@ namespace ClientPlugin.Compatibility; // Stores Linux window geometry in SpaceEngineers.cfg through protected MyConfig accessors. internal static class PluginWindowConfig { - // Windowed size shares the game's render-resolution keys. - private const string KEY_WINDOWED_WIDTH = "ScreenWidth"; - private const string KEY_WINDOWED_HEIGHT = "ScreenHeight"; + // Windowed size is persisted in drawable pixels. Keep it separate from the + // game's current render resolution, which may describe fullscreen mode. + private const string KEY_WINDOWED_WIDTH = "LinuxCompat_WindowedWidth"; + private const string KEY_WINDOWED_HEIGHT = "LinuxCompat_WindowedHeight"; private const string KEY_WINDOWED_X = "LinuxCompat_WindowedX"; private const string KEY_WINDOWED_Y = "LinuxCompat_WindowedY"; diff --git a/ClientPlugin/Compatibility/SdlGameWindow.cs b/ClientPlugin/Compatibility/SdlGameWindow.cs index f159b6c..4b166d2 100644 --- a/ClientPlugin/Compatibility/SdlGameWindow.cs +++ b/ClientPlugin/Compatibility/SdlGameWindow.cs @@ -49,6 +49,7 @@ internal sealed class SdlGameWindow : IVRageWindow, IVRageInput, IVRageInput2 new Dictionary>(); private List m_bufferedChars = new List(); private readonly byte[] m_keyStates = new byte[32]; + private readonly uint m_windowId; private Vector2I m_clientSize = new Vector2I(1280, 720); private Vector2I m_clientSizePixels = new Vector2I(1280, 720); @@ -62,6 +63,7 @@ internal sealed class SdlGameWindow : IVRageWindow, IVRageInput, IVRageInput2 private bool m_mouseCapture; private bool m_showCursor = true; private bool m_mouseOutsideWindow; + private int m_manualCloseQueued; // Prevents drag-resize feedback from redundant SDL geometry changes. private MyWindowModeEnum? m_appliedWindowMode; @@ -74,10 +76,19 @@ internal sealed class SdlGameWindow : IVRageWindow, IVRageInput, IVRageInput2 private const int MIN_VALID_WINDOW_WIDTH = 480; private const int MIN_VALID_WINDOW_HEIGHT = 360; - private static bool IsValidWindowedSize(int w, int h) => + private static bool IsValidWindowedPixelSize(int w, int h) => w >= MIN_VALID_WINDOW_WIDTH && h >= MIN_VALID_WINDOW_HEIGHT; + private bool IsValidWindowedSize(int w, int h) + { + Vector2I pixels = WindowToPixelSize(new Vector2I(w, h)); + return IsValidWindowedPixelSize(pixels.X, pixels.Y); + } + // Debounce geometry saves; the render thread schedules and the game thread saves. + private readonly object m_configLock = new object(); + private Vector2I? m_pendingWindowedSizePixels; + private Vector2I? m_pendingWindowedPosition; private long m_configSaveScheduledAtTicks; private const int CONFIG_SAVE_DEBOUNCE_MS = 500; @@ -89,15 +100,51 @@ private void ScheduleConfigSave() ); } + private void QueueWindowedConfig(Vector2I? sizePixels, Vector2I? position) + { + if (!sizePixels.HasValue && !position.HasValue) + return; + lock (m_configLock) + { + if (sizePixels.HasValue) + m_pendingWindowedSizePixels = sizePixels; + if (position.HasValue) + m_pendingWindowedPosition = position; + } + ScheduleConfigSave(); + } + private void FlushPendingConfigSave(bool force = false) { long ticks = Volatile.Read(ref m_configSaveScheduledAtTicks); - if (ticks == 0) + if (ticks == 0 && !force) return; if (!force && DateTime.UtcNow.Ticks < ticks) return; Volatile.Write(ref m_configSaveScheduledAtTicks, 0); - PluginWindowConfig.Save(); + + if (ApplyPendingWindowConfig() || ticks != 0) + PluginWindowConfig.Save(); + } + + private bool ApplyPendingWindowConfig() + { + Vector2I? sizePixels; + Vector2I? position; + lock (m_configLock) + { + sizePixels = m_pendingWindowedSizePixels; + position = m_pendingWindowedPosition; + m_pendingWindowedSizePixels = null; + m_pendingWindowedPosition = null; + } + if (!sizePixels.HasValue && !position.HasValue) + return false; + if (sizePixels.HasValue) + PluginWindowConfig.SetWindowedSize(sizePixels.Value.X, sizePixels.Value.Y); + if (position.HasValue) + PluginWindowConfig.SetWindowedPosition(position.Value.X, position.Value.Y); + return true; } internal IntPtr Handle { get; private set; } @@ -110,31 +157,65 @@ private void FlushPendingConfigSave(bool force = false) // Physical drawable size used for the DXVK backbuffer on HiDPI displays. internal Vector2I ClientSizePixels => m_clientSizePixels; - // Applies a requested video-settings resize on the SDL thread. - internal void SetClientSize(int width, int height) + internal static Vector2I PixelsToPrimaryWindowSize(int width, int height) { - if (width <= 0 || height <= 0 || Handle == IntPtr.Zero) - return; - - SdlRenderThread.Dispatch(() => + return SdlRenderThread.Invoke(() => { - if (Handle == IntPtr.Zero) - return; - m_clientSize = new Vector2I(width, height); - SDL_SetWindowSize(Handle, width, height); - RefreshPixelSize(); + uint displayId = SDL_GetPrimaryDisplay(); + IntPtr modePtr = displayId == 0 ? IntPtr.Zero : SDL_GetDesktopDisplayMode(displayId); + float density = + modePtr == IntPtr.Zero + ? 1f + : Marshal.PtrToStructure(modePtr).PixelDensity; + return PixelsToWindowSize(width, height, density); }); } // SDL thread only. - private void RefreshPixelSize() + private void RefreshWindowMetrics() { if (Handle == IntPtr.Zero) return; + Vector2I logical = m_clientSize; + Vector2I pixels; + if (SDL_GetWindowSize(Handle, out int lw, out int lh) && lw > 0 && lh > 0) + logical = new Vector2I(lw, lh); if (SDL_GetWindowSizeInPixels(Handle, out int w, out int h) && w > 0 && h > 0) - m_clientSizePixels = new Vector2I(w, h); + pixels = new Vector2I(w, h); else - m_clientSizePixels = m_clientSize; + pixels = logical; + lock (m_bufferLock) + { + m_clientSize = logical; + m_clientSizePixels = pixels; + } + } + + private Vector2I PixelsToWindowSize(int width, int height) + { + float density = SDL_GetWindowPixelDensity(Handle); + return PixelsToWindowSize(width, height, density); + } + + private static Vector2I PixelsToWindowSize(int width, int height, float density) + { + if (density <= 0f || !float.IsFinite(density)) + density = 1f; + return new Vector2I( + Math.Max(1, (int)MathF.Round(width / density)), + Math.Max(1, (int)MathF.Round(height / density)) + ); + } + + private Vector2I WindowToPixelSize(Vector2I size) + { + float density = SDL_GetWindowPixelDensity(Handle); + if (density <= 0f || !float.IsFinite(density)) + density = 1f; + return new Vector2I( + Math.Max(1, (int)MathF.Round(size.X * density)), + Math.Max(1, (int)MathF.Round(size.Y * density)) + ); } public bool MouseCapture @@ -164,19 +245,26 @@ public Vector2 MousePosition { get { - if (m_mouseOutsideWindow) - return m_mousePosition; - return m_mousePosition.IsValid() - ? m_mousePosition - : new Vector2(m_clientSize.X * 0.5f, m_clientSize.Y * 0.5f); + lock (m_bufferLock) + { + if (m_mouseOutsideWindow) + return m_mousePosition; + return m_mousePosition.IsValid() + ? m_mousePosition + : new Vector2(m_clientSize.X * 0.5f, m_clientSize.Y * 0.5f); + } } set { - bool shouldWarp = - Math.Abs(m_mousePosition.X - value.X) > 0.5f - || Math.Abs(m_mousePosition.Y - value.Y) > 0.5f; - m_mouseOutsideWindow = false; - m_mousePosition = value; + bool shouldWarp; + lock (m_bufferLock) + { + shouldWarp = + Math.Abs(m_mousePosition.X - value.X) > 0.5f + || Math.Abs(m_mousePosition.Y - value.Y) > 0.5f; + m_mouseOutsideWindow = false; + m_mousePosition = value; + } if (shouldWarp && Handle != IntPtr.Zero) { float wx = value.X, @@ -254,22 +342,45 @@ private SdlGameWindow(string gameName, int width, int height, int? initialX, int if (Handle == IntPtr.Zero) throw new PlatformNotSupportedException("SDL3 window creation failed."); + m_windowId = SDL_GetWindowID(Handle); // Set _NET_WM_ICON before the window is mapped. SdlIconHelper.Apply(Handle, ResolveGameIcon()); // Apply saved geometry before the first map to avoid a visible jump. - if (initialX.HasValue && initialY.HasValue) + if (!SdlRenderThread.IsWayland && initialX.HasValue && initialY.HasValue) { SDL_SetWindowPosition(Handle, initialX.Value, initialY.Value); m_savedWindowedPosition = new Vector2I(initialX.Value, initialY.Value); } - if (IsValidWindowedSize(m_clientSize.X, m_clientSize.Y)) - m_savedWindowedSize = m_clientSize; - SDL_StartTextInput(Handle); UpdateMouseModeOnRenderThread(); - RefreshPixelSize(); + + // Wayland must configure the toplevel before DXVK attaches its first buffer. + if (SdlRenderThread.IsWayland) + { + SDL_ShowWindow(Handle); + SDL_SyncWindow(Handle); + + // The compositor assigns the output scale when the surface is mapped. + // Reapply persisted pixels using that output's actual density. + if ( + Sandbox.MySandboxGame.Config?.WindowMode == MyWindowModeEnum.Window + && PluginWindowConfig.TryGetWindowedSize(out int savedW, out int savedH) + ) + { + Vector2I restoredSize = PixelsToWindowSize(savedW, savedH); + SDL_SetWindowSize(Handle, restoredSize.X, restoredSize.Y); + SDL_SyncWindow(Handle); + } + + // Preserve SDL_WINDOW_HIDDEN until the game or Pulsar calls ShowAndFocus. + SDL_HideWindow(Handle); + SDL_SyncWindow(Handle); + } + RefreshWindowMetrics(); + if (IsValidWindowedSize(m_clientSize.X, m_clientSize.Y)) + m_savedWindowedSize = m_clientSize; // Receive events and one mouse snapshot per SDL loop. SdlRenderThread.EventHandler += HandleEvent; @@ -301,14 +412,20 @@ public void OnModeChanged(MyWindowModeEnum mode, int width, int height, Rectangl if (Handle == IntPtr.Zero) return; + BackbufferResizeRequest.BeginModeChange(); // Serialize mode changes with other SDL window operations. SdlRenderThread.Dispatch(() => { - if (Handle == IntPtr.Zero) - return; - ApplyModeChange(mode, width, height, desktopBounds); - // Reconcile the DXVK backbuffer on the next game tick. - BackbufferResizeRequest.Request(); + try + { + if (Handle == IntPtr.Zero) + return; + ApplyModeChange(mode, width, height, desktopBounds); + } + finally + { + BackbufferResizeRequest.CompleteModeChange(); + } }); } @@ -323,7 +440,8 @@ Rectangle desktopBounds if (displayBounds.Width <= 0 || displayBounds.Height <= 0) displayBounds = desktopBounds; - bool modeChanged = !m_appliedWindowMode.HasValue || m_appliedWindowMode.Value != mode; + bool initialMode = !m_appliedWindowMode.HasValue; + bool modeChanged = initialMode || m_appliedWindowMode.Value != mode; // Load saved windowed geometry before the first mode transition. if (!m_appliedWindowMode.HasValue && !m_savedWindowedSize.HasValue) @@ -345,14 +463,15 @@ Rectangle desktopBounds SDL_SetWindowFullscreen(Handle, false); SDL_SetWindowAlwaysOnTop(Handle, false); SDL_SetWindowBordered(Handle, true); - ApplyWindowedMode(width, height, displayBounds, modeChanged); + ApplyWindowedMode(width, height, displayBounds, modeChanged, initialMode); break; case MyWindowModeEnum.FullscreenWindow: - // XWayland requires SDL fullscreen for reliable borderless mode. - if (IsXWayland()) + // Wayland compositors, including XWayland, own borderless placement. + if (UsesWaylandCompositor()) { - ApplyFullscreenMode(displayBounds.Width, displayBounds.Height); + SDL_SetWindowFullscreenMode(Handle, IntPtr.Zero); + SDL_SetWindowFullscreen(Handle, true); break; } SDL_SetWindowFullscreenMode(Handle, IntPtr.Zero); @@ -361,8 +480,6 @@ Rectangle desktopBounds SDL_SetWindowBordered(Handle, false); SDL_SetWindowPosition(Handle, displayBounds.X, displayBounds.Y); SDL_SetWindowSize(Handle, displayBounds.Width, displayBounds.Height); - m_clientSize = new Vector2I(displayBounds.Width, displayBounds.Height); - RefreshPixelSize(); break; case MyWindowModeEnum.Fullscreen: @@ -371,60 +488,66 @@ Rectangle desktopBounds } m_appliedWindowMode = mode; + SDL_SyncWindow(Handle); + RefreshWindowMetrics(); } private void ApplyWindowedMode( int desiredWidth, int desiredHeight, Rectangle displayBounds, - bool modeChanged + bool modeChanged, + bool initialMode ) { + Vector2I desiredSize = PixelsToWindowSize(desiredWidth, desiredHeight); + // DXVK can report tiny swapchain bounds instead of desktop bounds. bool boundsOk = IsPlausibleDisplayBounds(displayBounds); - int targetW = desiredWidth; - int targetH = desiredHeight; + int targetW = desiredSize.X; + int targetH = desiredSize.Y; if (boundsOk) { - targetW = Math.Min(desiredWidth, displayBounds.Width); - targetH = Math.Min(desiredHeight, displayBounds.Height); + targetW = Math.Min(desiredSize.X, displayBounds.Width); + targetH = Math.Min(desiredSize.Y, displayBounds.Height); } if (targetW <= 0) - targetW = desiredWidth; + targetW = desiredSize.X; if (targetH <= 0) - targetH = desiredHeight; + targetH = desiredSize.Y; if (modeChanged) { - // Restore persisted geometry when entering windowed mode. - int w = m_savedWindowedSize?.X ?? targetW; - int h = m_savedWindowedSize?.Y ?? targetH; + // Startup restores manual geometry; later mode changes honor the + // resolution selected in the display settings. + int w = initialMode ? m_savedWindowedSize?.X ?? targetW : targetW; + int h = initialMode ? m_savedWindowedSize?.Y ?? targetH : targetH; if (boundsOk) { w = Math.Min(w, displayBounds.Width); h = Math.Min(h, displayBounds.Height); } - int x, - y; - if (m_savedWindowedPosition.HasValue) + int x = 0, + y = 0; + if (!SdlRenderThread.IsWayland && m_savedWindowedPosition.HasValue) { x = m_savedWindowedPosition.Value.X; y = m_savedWindowedPosition.Value.Y; } - else if (boundsOk) + else if (!SdlRenderThread.IsWayland && boundsOk) { x = displayBounds.X + (displayBounds.Width - w) / 2; y = displayBounds.Y + (displayBounds.Height - h) / 2; } - else + else if (!SdlRenderThread.IsWayland) { // Keep SDL's default position when no trusted bounds exist. SDL_GetWindowPosition(Handle, out x, out y); } - if (boundsOk) + if (!SdlRenderThread.IsWayland && boundsOk) ClampWindowToDisplay(displayBounds, ref x, ref y, ref w, ref h); Console.WriteLine( @@ -434,16 +557,20 @@ bool modeChanged ); SDL_SetWindowSize(Handle, w, h); - SDL_SetWindowPosition(Handle, x, y); - m_clientSize = new Vector2I(w, h); + if (!SdlRenderThread.IsWayland) + SDL_SetWindowPosition(Handle, x, y); m_savedWindowedSize = new Vector2I(w, h); - m_savedWindowedPosition = new Vector2I(x, y); + if (!SdlRenderThread.IsWayland) + m_savedWindowedPosition = new Vector2I(x, y); PersistSavedWindowedState(); } else if (targetW != m_clientSize.X || targetH != m_clientSize.Y) { // Preserve position for windowed resolution changes unless it no longer fits. - bool havePos = SDL_GetWindowPosition(Handle, out int curX, out int curY); + int curX = 0, + curY = 0; + bool havePos = + !SdlRenderThread.IsWayland && SDL_GetWindowPosition(Handle, out curX, out curY); int x = havePos ? curX : (boundsOk ? displayBounds.X + (displayBounds.Width - targetW) / 2 : 0); @@ -453,7 +580,7 @@ bool modeChanged int w = targetW; int h = targetH; - if (boundsOk) + if (!SdlRenderThread.IsWayland && boundsOk) ClampWindowToDisplay(displayBounds, ref x, ref y, ref w, ref h); Console.WriteLine( @@ -463,16 +590,14 @@ bool modeChanged ); SDL_SetWindowSize(Handle, w, h); - if (!havePos || x != curX || y != curY) + if (!SdlRenderThread.IsWayland && (!havePos || x != curX || y != curY)) SDL_SetWindowPosition(Handle, x, y); - m_clientSize = new Vector2I(w, h); m_savedWindowedSize = new Vector2I(w, h); - m_savedWindowedPosition = new Vector2I(x, y); + if (!SdlRenderThread.IsWayland) + m_savedWindowedPosition = new Vector2I(x, y); PersistSavedWindowedState(); } // Matching geometry came from the WM and must not be applied back to it. - - RefreshPixelSize(); } private static void ClampWindowToDisplay( @@ -505,35 +630,33 @@ private void CaptureCurrentWindowedState() return; if (SDL_GetWindowSize(Handle, out int w, out int h) && IsValidWindowedSize(w, h)) m_savedWindowedSize = new Vector2I(w, h); - if (SDL_GetWindowPosition(Handle, out int x, out int y)) + if (!SdlRenderThread.IsWayland && SDL_GetWindowPosition(Handle, out int x, out int y)) m_savedWindowedPosition = new Vector2I(x, y); PersistSavedWindowedState(); } private void PersistSavedWindowedState() { + Vector2I? pixels = null; if ( m_savedWindowedSize.HasValue && IsValidWindowedSize(m_savedWindowedSize.Value.X, m_savedWindowedSize.Value.Y) ) - PluginWindowConfig.SetWindowedSize( - m_savedWindowedSize.Value.X, - m_savedWindowedSize.Value.Y - ); - if (m_savedWindowedPosition.HasValue) - PluginWindowConfig.SetWindowedPosition( - m_savedWindowedPosition.Value.X, - m_savedWindowedPosition.Value.Y - ); + pixels = WindowToPixelSize(m_savedWindowedSize.Value); + QueueWindowedConfig(pixels, SdlRenderThread.IsWayland ? null : m_savedWindowedPosition); } private void LoadSavedWindowedState() { if ( - PluginWindowConfig.TryGetWindowedSize(out int w, out int h) && IsValidWindowedSize(w, h) + PluginWindowConfig.TryGetWindowedSize(out int w, out int h) + && IsValidWindowedPixelSize(w, h) + ) + m_savedWindowedSize = PixelsToWindowSize(w, h); + if ( + !SdlRenderThread.IsWayland + && PluginWindowConfig.TryGetWindowedPosition(out int x, out int y) ) - m_savedWindowedSize = new Vector2I(w, h); - if (PluginWindowConfig.TryGetWindowedPosition(out int x, out int y)) m_savedWindowedPosition = new Vector2I(x, y); } @@ -562,8 +685,6 @@ out SdlDisplayMode mode SDL_SetWindowFullscreenMode(Handle, (IntPtr)modePtr); } SDL_SetWindowFullscreen(Handle, true); - m_clientSize = new Vector2I(width, height); - RefreshPixelSize(); } private Rectangle GetWindowDisplayBounds() @@ -596,10 +717,11 @@ private Rectangle GetWindowDisplayBounds() // Reject DXGI swapchain bounds masquerading as desktop geometry. private static bool IsPlausibleDisplayBounds(Rectangle r) => r.Width >= 640 && r.Height >= 480; - private static bool IsXWayland() + private static bool UsesWaylandCompositor() { string sessionType = Environment.GetEnvironmentVariable("XDG_SESSION_TYPE"); - return string.Equals(sessionType, "wayland", StringComparison.OrdinalIgnoreCase); + return SdlRenderThread.IsWayland + || string.Equals(sessionType, "wayland", StringComparison.OrdinalIgnoreCase); } public void AddChar(char ch) @@ -644,14 +766,39 @@ public void CloseManually() private void HandleManualWindowCloseRequest() { - if (OnManualWindowCloseRequest != null && m_isVisible) + if (Interlocked.Exchange(ref m_manualCloseQueued, 1) != 0) + return; + + var game = Sandbox.MySandboxGame.Static; + if (game == null) { - OnManualWindowCloseRequest(); + Volatile.Write(ref m_manualCloseQueued, 0); + Hide(); + CloseManually(); return; } - Hide(); - CloseManually(); + game.Invoke( + () => + { + try + { + if (OnManualWindowCloseRequest != null && m_isVisible) + { + OnManualWindowCloseRequest(); + return; + } + + Hide(); + CloseManually(); + } + finally + { + Volatile.Write(ref m_manualCloseQueued, 0); + } + }, + "LinuxCompat window close" + ); } /// @@ -663,7 +810,6 @@ public void Exit() { m_isVisible = false; m_isActive = false; - // MySandboxGame.OnExit does not flush pending geometry changes. FlushPendingConfigSave(force: true); SdlRenderThread.Invoke(() => { @@ -681,7 +827,6 @@ public bool UpdateRenderThread() public void UpdateMainThread() { - // Keep config file I/O off the SDL event thread. FlushPendingConfigSave(); } @@ -806,11 +951,24 @@ private void UpdateMouseSnapshot() if (Handle == IntPtr.Zero) return; - uint buttonState = SDL_GetMouseState(out var mouseX, out var mouseY); SDL_GetRelativeMouseState(out var relX, out var relY); + if (SDL_GetMouseFocus() != Handle) + { + lock (m_bufferLock) + { + m_mouseButtonState = 0; + m_relativeDeltaXAccum = 0; + m_relativeDeltaYAccum = 0; + } + SetKeyState(MyKeys.LeftButton, false); + SetKeyState(MyKeys.RightButton, false); + SetKeyState(MyKeys.MiddleButton, false); + SetKeyState(MyKeys.ExtraButton1, false); + SetKeyState(MyKeys.ExtraButton2, false); + return; + } - if (SDL_GetWindowSize(Handle, out int curW, out int curH) && curW > 0 && curH > 0) - m_clientSize = new Vector2I(curW, curH); + uint buttonState = SDL_GetMouseState(out var mouseX, out var mouseY); lock (m_bufferLock) { @@ -849,6 +1007,9 @@ private void DestroyNativeWindow() /// private void HandleEvent(ref SdlRenderThread.SdlEvent sdlEvent) { + if (sdlEvent.Type != SDL_EVENT_QUIT && sdlEvent.Window.WindowId != m_windowId) + return; + switch (sdlEvent.Type) { case SDL_EVENT_QUIT: @@ -861,44 +1022,38 @@ private void HandleEvent(ref SdlRenderThread.SdlEvent sdlEvent) break; case SDL_EVENT_WINDOW_FOCUS_LOST: m_isActive = false; - m_mouseOutsideWindow = true; - m_mousePosition = -Vector2.One; break; case SDL_EVENT_WINDOW_MOUSE_ENTER: - m_mouseOutsideWindow = false; + lock (m_bufferLock) + { + m_mouseOutsideWindow = false; + } break; case SDL_EVENT_WINDOW_MOUSE_LEAVE: - m_mouseOutsideWindow = true; - m_mousePosition = -Vector2.One; + lock (m_bufferLock) + { + m_mouseOutsideWindow = true; + m_mousePosition = -Vector2.One; + } break; case SDL_EVENT_WINDOW_RESIZED: - m_clientSize = new Vector2I(sdlEvent.Window.Data1, sdlEvent.Window.Data2); - RefreshPixelSize(); + RefreshWindowMetrics(); BackbufferResizeRequest.Request(); - // Persist valid windowed geometry from either the WM or settings. - if ( - m_appliedWindowMode == MyWindowModeEnum.Window - && IsValidWindowedSize(m_clientSize.X, m_clientSize.Y) - ) - { - m_savedWindowedSize = m_clientSize; - PluginWindowConfig.SetWindowedSize(m_clientSize.X, m_clientSize.Y); - ScheduleConfigSave(); - } + PersistCurrentWindowedSize(); break; case SDL_EVENT_WINDOW_MOVED: - if (m_appliedWindowMode == MyWindowModeEnum.Window) + if (!SdlRenderThread.IsWayland && m_appliedWindowMode == MyWindowModeEnum.Window) { int px = sdlEvent.Window.Data1; int py = sdlEvent.Window.Data2; m_savedWindowedPosition = new Vector2I(px, py); - PluginWindowConfig.SetWindowedPosition(px, py); - ScheduleConfigSave(); + QueueWindowedConfig(null, m_savedWindowedPosition); } break; case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: - m_clientSizePixels = new Vector2I(sdlEvent.Window.Data1, sdlEvent.Window.Data2); + RefreshWindowMetrics(); BackbufferResizeRequest.Request(); + PersistCurrentWindowedSize(); break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: @@ -939,31 +1094,53 @@ private void HandleEvent(ref SdlRenderThread.SdlEvent sdlEvent) } } - // SDL may omit MOUSE_ENTER after focus returns. Restore the software cursor - // from global coordinates, or recenter it when the pointer is outside. + private void PersistCurrentWindowedSize() + { + if ( + m_appliedWindowMode != MyWindowModeEnum.Window + || !IsValidWindowedSize(m_clientSize.X, m_clientSize.Y) + ) + return; + + m_savedWindowedSize = m_clientSize; + QueueWindowedConfig(m_clientSizePixels, null); + } + + // SDL may omit MOUSE_ENTER after focus returns. Wayland cannot query global state. private void RecenterCursorIfOutsideWindow() { if (Handle == IntPtr.Zero) return; if (!m_showCursor) return; + lock (m_bufferLock) + { + if (!m_mouseOutsideWindow) + return; + } if (!SDL_GetWindowSize(Handle, out int w, out int h) || w <= 0 || h <= 0) return; - if (!SDL_GetWindowPosition(Handle, out int wx, out int wy)) - return; - SDL_GetGlobalMouseState(out float gx, out float gy); - bool inside = gx >= wx && gy >= wy && gx < wx + w && gy < wy + h; - if (inside) + if (!SdlRenderThread.IsWayland && SDL_GetWindowPosition(Handle, out int wx, out int wy)) { - m_mouseOutsideWindow = false; - m_mousePosition = new Vector2(gx - wx, gy - wy); - return; + SDL_GetGlobalMouseState(out float gx, out float gy); + if (gx >= wx && gy >= wy && gx < wx + w && gy < wy + h) + { + lock (m_bufferLock) + { + m_mouseOutsideWindow = false; + m_mousePosition = new Vector2(gx - wx, gy - wy); + } + return; + } } float cx = w * 0.5f; float cy = h * 0.5f; SDL_WarpMouseInWindow(Handle, cx, cy); - m_mouseOutsideWindow = false; - m_mousePosition = new Vector2(cx, cy); + lock (m_bufferLock) + { + m_mouseOutsideWindow = false; + m_mousePosition = new Vector2(cx, cy); + } } private void DispatchUpdateMouseMode() @@ -1141,10 +1318,17 @@ private struct SdlDisplayMode [DllImport(Lib, EntryPoint = "SDL_DestroyWindow")] private static extern void SDL_DestroyWindow(IntPtr window); + [DllImport(Lib, EntryPoint = "SDL_GetWindowID")] + private static extern uint SDL_GetWindowID(IntPtr window); + [DllImport(Lib, EntryPoint = "SDL_ShowWindow")] [return: MarshalAs(UnmanagedType.I1)] private static extern bool SDL_ShowWindow(IntPtr window); + [DllImport(Lib, EntryPoint = "SDL_SyncWindow")] + [return: MarshalAs(UnmanagedType.I1)] + private static extern bool SDL_SyncWindow(IntPtr window); + [DllImport(Lib, EntryPoint = "SDL_HideWindow")] [return: MarshalAs(UnmanagedType.I1)] private static extern bool SDL_HideWindow(IntPtr window); @@ -1203,6 +1387,9 @@ out SdlDisplayMode mode [DllImport(Lib, EntryPoint = "SDL_GetPrimaryDisplay")] private static extern uint SDL_GetPrimaryDisplay(); + [DllImport(Lib, EntryPoint = "SDL_GetDesktopDisplayMode")] + private static extern IntPtr SDL_GetDesktopDisplayMode(uint displayId); + [DllImport(Lib, EntryPoint = "SDL_GetDisplayBounds")] [return: MarshalAs(UnmanagedType.I1)] private static extern bool SDL_GetDisplayBounds(uint displayId, out SdlRect rect); @@ -1215,6 +1402,9 @@ out SdlDisplayMode mode [return: MarshalAs(UnmanagedType.I1)] private static extern bool SDL_GetWindowSizeInPixels(IntPtr window, out int w, out int h); + [DllImport(Lib, EntryPoint = "SDL_GetWindowPixelDensity")] + private static extern float SDL_GetWindowPixelDensity(IntPtr window); + [DllImport(Lib, EntryPoint = "SDL_StartTextInput")] [return: MarshalAs(UnmanagedType.I1)] private static extern bool SDL_StartTextInput(IntPtr window); @@ -1222,6 +1412,9 @@ out SdlDisplayMode mode [DllImport(Lib, EntryPoint = "SDL_GetMouseState")] private static extern uint SDL_GetMouseState(out float x, out float y); + [DllImport(Lib, EntryPoint = "SDL_GetMouseFocus")] + private static extern IntPtr SDL_GetMouseFocus(); + [DllImport(Lib, EntryPoint = "SDL_GetRelativeMouseState")] private static extern uint SDL_GetRelativeMouseState(out float x, out float y); diff --git a/ClientPlugin/Compatibility/SdlRenderThread.cs b/ClientPlugin/Compatibility/SdlRenderThread.cs index e166f80..b063135 100644 --- a/ClientPlugin/Compatibility/SdlRenderThread.cs +++ b/ClientPlugin/Compatibility/SdlRenderThread.cs @@ -9,7 +9,7 @@ namespace ClientPlugin.Compatibility; /// /// Owns SDL video, windows, event pumping, and clipboard access. All SDL video -/// calls use this thread to protect SDL's X11 connection state. +/// calls use this thread to protect the active window-system connection. /// internal static class SdlRenderThread { @@ -27,6 +27,7 @@ internal static class SdlRenderThread private static volatile bool s_running; private static volatile bool s_initOk; + private static readonly object s_startLock = new object(); private static readonly object s_queueLock = new object(); private static readonly Queue s_queue = new Queue(); private static readonly ManualResetEventSlim s_started = new ManualResetEventSlim(false); @@ -50,71 +51,53 @@ internal static class SdlRenderThread /// True once SDL_Init succeeded. internal static bool IsInitialized => s_initOk; + internal static bool IsWayland { get; private set; } + /// /// Starts SDL once and blocks until initialization completes. /// internal static void Start() { - if (s_thread != null) - return; - - var thread = new Thread(Run) + lock (s_startLock) { - // Keep the SDL context alive for the process lifetime. - IsBackground = false, - Name = "LinuxCompat-SDL", - }; - s_thread = thread; - thread.Start(); - - // s_started is signaled for both success and failure; timeout means - // native loading or SDL_Init is wedged. - if (!s_started.Wait(START_TIMEOUT_MS)) - { - Console.Error.WriteLine( - $"[LinuxCompat] SdlRenderThread.Start: SDL_Init did not complete within {START_TIMEOUT_MS / 1000} s. " - + "The render thread is wedged; killing the process to surface the failure." - ); - try - { - Console.Error.Flush(); - } - catch { } - // Runtime shutdown can block when a thread is stuck in native code. - try + if (s_thread != null) + return; + + var thread = new Thread(Run) { IsBackground = true, Name = "LinuxCompat-SDL" }; + s_thread = thread; + thread.Start(); + + // s_started is signaled for both success and failure; timeout means + // native loading or SDL_Init is wedged. + if (!s_started.Wait(START_TIMEOUT_MS)) { - Process.GetCurrentProcess().Kill(); + Console.Error.WriteLine( + $"[LinuxCompat] SdlRenderThread.Start: SDL_Init did not complete within {START_TIMEOUT_MS / 1000} s. " + + "The render thread is wedged; killing the process to surface the failure." + ); + try + { + Console.Error.Flush(); + } + catch { } + // Runtime shutdown can block when a thread is stuck in native code. + try + { + Process.GetCurrentProcess().Kill(); + } + catch { } + // Fallback if Process.Kill returns. + Environment.FailFast("SdlRenderThread SDL_Init timeout"); } - catch { } - // Fallback if Process.Kill returns. - Environment.FailFast("SdlRenderThread SDL_Init timeout"); } } - /// - /// Stops and joins the render thread. Safe from any thread. - /// - internal static void Stop() - { - if (s_thread == null) - return; - - s_running = false; - Dispatch(static () => { }); - - if (!IsCurrent) - s_thread.Join(); - - s_thread = null; - s_threadManagedId = 0; - } - /// /// Queues an action, or runs it inline when already on the render thread. /// internal static void Dispatch(Action action) { - if (action == null) + if (action == null || s_thread == null || !s_running) return; if (IsCurrent) @@ -145,6 +128,9 @@ internal static void Invoke(Action action) if (action == null) return; + if (s_thread == null || !s_running) + throw new InvalidOperationException("SDL render thread is not running."); + if (IsCurrent) { action(); @@ -195,9 +181,6 @@ private static void Run() { s_threadManagedId = Thread.CurrentThread.ManagedThreadId; - // DXVK uses the tested X11 path. Set SDL's environment before SDL_Init. - ForceX11VideoDriver(); - // Long world loads cannot answer _NET_WM_PING. Disable it before // window creation to avoid false "not responding" dialogs. SDL_SetHint("SDL_VIDEO_X11_NET_WM_PING", "0"); @@ -211,7 +194,11 @@ private static void Run() } else { - Console.WriteLine("[LinuxCompat] SdlRenderThread initialised SDL3 (video)"); + string videoDriver = Marshal.PtrToStringUTF8(SDL_GetCurrentVideoDriver()); + IsWayland = string.Equals(videoDriver, "wayland", StringComparison.OrdinalIgnoreCase); + Console.WriteLine( + $"[LinuxCompat] SdlRenderThread initialised SDL3 video driver: {videoDriver ?? "unknown"}" + ); SdlJoystick.Initialize(); } @@ -285,7 +272,7 @@ private static void Run() } } - // Process shutdown bypasses orderly SDL teardown. + // SDL is process-owned. Plugin disposal must not tear down a live window. } private static void DrainQueue() @@ -317,13 +304,6 @@ private static void DrainQueue() } } - private static void ForceX11VideoDriver() - { - IntPtr env = SDL_GetEnvironment(); - if (env != IntPtr.Zero) - SDL_SetEnvironmentVariable(env, "SDL_VIDEODRIVER", "x11", true); - } - private static void LogException(string where, Exception ex) { try @@ -435,17 +415,8 @@ internal struct SdlMouseWheelEvent [return: MarshalAs(UnmanagedType.I1)] private static extern bool SDL_SetHint(string name, string value); - [DllImport(Lib, EntryPoint = "SDL_GetEnvironment")] - private static extern IntPtr SDL_GetEnvironment(); - - [DllImport(Lib, EntryPoint = "SDL_SetEnvironmentVariable", CharSet = CharSet.Ansi)] - [return: MarshalAs(UnmanagedType.I1)] - private static extern bool SDL_SetEnvironmentVariable( - IntPtr environment, - string name, - string value, - [MarshalAs(UnmanagedType.I1)] bool overwrite - ); + [DllImport(Lib, EntryPoint = "SDL_GetCurrentVideoDriver")] + private static extern IntPtr SDL_GetCurrentVideoDriver(); [DllImport(Lib, EntryPoint = "SDL_PollEvent")] [return: MarshalAs(UnmanagedType.I1)] diff --git a/ClientPlugin/Patches/PlatformGuards/CreateWindowPatch.cs b/ClientPlugin/Patches/PlatformGuards/CreateWindowPatch.cs index 0b1aedc..c64d52b 100644 --- a/ClientPlugin/Patches/PlatformGuards/CreateWindowPatch.cs +++ b/ClientPlugin/Patches/PlatformGuards/CreateWindowPatch.cs @@ -9,6 +9,7 @@ using VRage.Ansel; using VRage.Platform.Windows; using VRage.Platform.Windows.Forms; +using VRageMath; using VRageRender; namespace ClientPlugin.Patches.PlatformGuards; @@ -140,14 +141,22 @@ out int? y height = savedH; } - bool havePos = PluginWindowConfig.TryGetWindowedPosition(out int savedX, out int savedY); + Vector2I windowSize = SdlGameWindow.PixelsToPrimaryWindowSize(width, height); + width = windowSize.X; + height = windowSize.Y; + + int savedX = 0, + savedY = 0; + bool havePos = + !SdlRenderThread.IsWayland + && PluginWindowConfig.TryGetWindowedPosition(out savedX, out savedY); if (havePos) { x = savedX; y = savedY; } - // Clamp against the primary display before the window has an assigned display. + // Clamp logical window coordinates before the window has an assigned display. if ( TryGetPrimaryDisplayBounds(out int dx, out int dy, out int dw, out int dh) && dw >= 640 @@ -197,7 +206,7 @@ private struct SdlRectNative H; } - // SDL video queries share X11 state and must run on the SDL thread. + // SDL video queries must run on the SDL thread. private static bool TryGetPrimaryDisplayBounds(out int x, out int y, out int w, out int h) { var result = TryGetPrimaryDisplayBoundsOnRenderThread(); diff --git a/ClientPlugin/Patches/WindowManagement/CursorRenderRatePatch.cs b/ClientPlugin/Patches/WindowManagement/CursorRenderRatePatch.cs index 77c8387..a0fb928 100644 --- a/ClientPlugin/Patches/WindowManagement/CursorRenderRatePatch.cs +++ b/ClientPlugin/Patches/WindowManagement/CursorRenderRatePatch.cs @@ -15,7 +15,7 @@ internal static class CursorRenderRateState } // Move the software cursor from its 60 Hz game-thread position to the latest -// SDL position when the render thread processes its sprite. Preserve size for HiDPI. +// SDL position when the render thread processes its sprite. [HarmonyPatch(typeof(MySpritesRenderer), nameof(MySpritesRenderer.ProcessDrawMessage))] [HarmonyPatchCategory("Finish")] static class CursorRenderRatePatch @@ -45,7 +45,14 @@ static void Prefix(MyRenderMessageBase drawMessage) if (!sdlWindow.TryGetFreshInWindowMousePosition(out Vector2 fresh)) return; - // Translate only; size retains the game thread's HiDPI scaling. + Vector2I windowSize = sdlWindow.ClientSize; + Vector2I renderSize = MyRender11.ResolutionI; + if (windowSize.X <= 0 || windowSize.Y <= 0 || renderSize.X <= 0 || renderSize.Y <= 0) + return; + fresh.X *= renderSize.X / (float)windowSize.X; + fresh.Y *= renderSize.Y / (float)windowSize.Y; + + // SDL input is in logical coordinates; the sprite uses the current render size. RectangleF rect = sprite.DestinationRectangle; rect.X = fresh.X - rect.Width * 0.5f; rect.Y = fresh.Y - rect.Height * 0.5f; diff --git a/ClientPlugin/Patches/WindowManagement/MySandboxGameWindowResizePatches.cs b/ClientPlugin/Patches/WindowManagement/MySandboxGameWindowResizePatches.cs index 2289499..5371bc0 100644 --- a/ClientPlugin/Patches/WindowManagement/MySandboxGameWindowResizePatches.cs +++ b/ClientPlugin/Patches/WindowManagement/MySandboxGameWindowResizePatches.cs @@ -1,6 +1,7 @@ using ClientPlugin.Patches.PlatformGuards; using HarmonyLib; using Sandbox; +using Sandbox.Engine.Platform.VideoMode; using VRage.Utils; using VRageMath; using VRageRender; @@ -11,28 +12,45 @@ namespace ClientPlugin.Patches.WindowManagement; // Same-mode resize flow is one-way from window to backbuffer to avoid feedback. internal static class BackbufferResizeRequest { - // Periodic reconciliation covers missed SDL resize signals. - private const int PERIODIC_CHECK_INTERVAL_FRAMES = 60; - + private static readonly object Sync = new object(); private static bool s_requested; - private static int s_frameCounter; + private static int s_pendingModeChanges; public static void Request() { - s_requested = true; + lock (Sync) + { + if (s_pendingModeChanges == 0) + s_requested = true; + } } - public static void ProcessIfRequested(MySandboxGame game) + public static void BeginModeChange() { - if (++s_frameCounter >= PERIODIC_CHECK_INTERVAL_FRAMES) + lock (Sync) { - s_frameCounter = 0; - s_requested = true; + s_pendingModeChanges++; + s_requested = false; } + } - if (!s_requested) - return; - s_requested = false; + public static void CompleteModeChange() + { + lock (Sync) + { + if (--s_pendingModeChanges == 0) + s_requested = true; + } + } + + public static void ProcessIfRequested(MySandboxGame game) + { + lock (Sync) + { + if (!s_requested) + return; + s_requested = false; + } if (Sandbox.Engine.Platform.Game.IsDedicated) return; @@ -41,10 +59,6 @@ public static void ProcessIfRequested(MySandboxGame game) if (sdl == null) return; - // Fullscreen transitions briefly expose stale windowed state. - if (!sdl.IsWindowed) - return; - var render = game?.GameRenderComponent?.RenderThread; if (render == null) return; @@ -58,7 +72,11 @@ public static void ProcessIfRequested(MySandboxGame game) if (backbuffer == target) return; - MyRenderDeviceSettings current = render.CurrentSettings; + MyRenderDeviceSettings current = MyVideoSettingsManager.CurrentDeviceSettings; + if (current.BackBufferWidth <= 0 || current.BackBufferHeight <= 0) + current = render.CurrentSettings; + if (current.BackBufferWidth <= 0 || current.BackBufferHeight <= 0) + return; current.BackBufferWidth = target.X; current.BackBufferHeight = target.Y; MyLog.Default.WriteLine( diff --git a/ClientPlugin/Patches/WindowManagement/SplashScreenPatches.cs b/ClientPlugin/Patches/WindowManagement/SplashScreenPatches.cs index 1bae74e..6518b1d 100644 --- a/ClientPlugin/Patches/WindowManagement/SplashScreenPatches.cs +++ b/ClientPlugin/Patches/WindowManagement/SplashScreenPatches.cs @@ -1,12 +1,24 @@ using System; using ClientPlugin.Compatibility; using HarmonyLib; +using Sandbox; using Sandbox.Game; using VRage.Platform.Windows.Forms; using VRageMath; namespace ClientPlugin.Patches.WindowManagement; +[HarmonyPatch(typeof(MyCommonProgramStartup), nameof(MyCommonProgramStartup.InitSplashScreen))] +[HarmonyPatchCategory("Finish")] +static class InitSplashScreenPatch +{ + static void Prefix() + { + if (RenderingConfig.AllowRendering) + SdlRenderThread.Start(); + } +} + /// /// Supplies SDL splash behavior for the preloader-disabled WinForms methods. /// Pulsar's -sesplash flag controls whether the game calls them. diff --git a/ClientPlugin/Plugin.cs b/ClientPlugin/Plugin.cs index 3fb8514..a7825c5 100644 --- a/ClientPlugin/Plugin.cs +++ b/ClientPlugin/Plugin.cs @@ -38,11 +38,7 @@ public void Init(object gameInstance) harmony.PatchCategory("Init"); } - public void Dispose() - { - if (RenderingConfig.AllowRendering) - SdlRenderThread.Stop(); - } + public void Dispose() { } public void Update() { diff --git a/README.md b/README.md index 9b3a367..c0a2652 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ install and run it. Please read the release notes to decide which one is the bes - With the plugin, the game will run natively on Linux and does not use Proton/Wine at all. - The game's window (in windowed mode) can be resized freely and is compatible with display scaling. -- Runs on both X11 and Wayland (requires XWayland for X11 compatibility). +- Runs on native Wayland and X11. Set `SDL_VIDEODRIVER=x11` to force X11. - Fonts and icons are sharper than on Windows. ## Development diff --git a/Shared/Preloader.cs b/Shared/Preloader.cs index 1726a9a..8af06aa 100644 --- a/Shared/Preloader.cs +++ b/Shared/Preloader.cs @@ -605,12 +605,6 @@ private static void ReplaceProcessPrivateMemory(TypeDefinition type) )] public static void Finish() { -#if !MAGNETAR - // Splash creation uses SDL before Plugin.Init. - if (ClientPlugin.Compatibility.RenderingConfig.AllowRendering) - ClientPlugin.Compatibility.SdlRenderThread.Start(); -#endif - #if DEBUG && HARMONY_DEBUG Harmony.DEBUG = true; #endif