Merge branch 'dont-skip-leg-day'

This commit is contained in:
PJB3005
2025-08-17 16:27:20 +02:00
13 changed files with 131 additions and 95 deletions

View File

@@ -26,7 +26,7 @@ jobs:
dotnet-version: 9.0.x
- name: Package client
run: Tools/package_client_build.py -p windows mac linux
run: Tools/package_client_build.py
- name: Shuffle files around
run: |

View File

@@ -2,6 +2,7 @@ input-key-Escape = Escape
input-key-Control = Control
input-key-Shift = Shift
input-key-Alt = Alt
input-key-Alt-mac = ⌥
input-key-Menu = Menu
input-key-F1 = F1
input-key-F2 = F2
@@ -70,8 +71,8 @@ input-key-MouseButton9 = Mouse 9
input-key-LSystem-win = Left Win
input-key-RSystem-win = Right Win
input-key-LSystem-mac = Left Cmd
input-key-RSystem-mac = Right Cmd
input-key-LSystem-mac = Left
input-key-RSystem-mac = Right
input-key-LSystem-linux = Left Meta
input-key-RSystem-linux = Right Meta

View File

@@ -374,6 +374,8 @@ namespace Robust.Client.Graphics.Clyde
if (reg.IsDisposed)
return;
_sawmillWin.Debug($"Destroying window {reg.Id}");
reg.IsDisposed = true;
_glContext!.WindowDestroyed(reg);

View File

@@ -128,7 +128,11 @@ namespace Robust.Client.Graphics.Clyde
// macOS cannot.
if (OperatingSystem.IsWindows() || OperatingSystem.IsLinux())
_cfg.OverrideDefault(CVars.DisplayThreadWindowApi, true);
#if MACOS
// Trust macOS to not need threaded window blitting.
// (threaded window blitting is a workaround to avoid having to frequently MakeCurrent() on Windows, as it is broken).
_cfg.OverrideDefault(CVars.DisplayThreadWindowBlit, false);
#endif
_threadWindowBlit = _cfg.GetCVar(CVars.DisplayThreadWindowBlit);
_threadWindowApi = _cfg.GetCVar(CVars.DisplayThreadWindowApi);

View File

@@ -102,6 +102,8 @@ namespace Robust.Client.Graphics.Clyde
{
var data = _windowData[reg.Id];
data.BlitDoneEvent?.Set();
// Set events so blit thread properly wakes up and notices it needs to shut down.
data.BlitStartEvent?.Set();
_windowData.Remove(reg.Id);
}

View File

@@ -49,6 +49,7 @@ public static partial class SDL
public const string SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER = "SDL.filedialog.nfilters";
public const string SDL_PROP_FILE_DIALOG_FILTERS_POINTER = "SDL.filedialog.filters";
public const string SDL_PROP_FILE_DIALOG_WINDOW_POINTER = "SDL.filedialog.window";
public static int SDL_VERSIONNUM_MAJOR(int version) => version / 1000000;
public static int SDL_VERSIONNUM_MINOR(int version) => version / 1000 % 1000;

View File

@@ -81,6 +81,11 @@ internal partial class Clyde
case EventQuit:
ProcessEventQuit();
break;
#if MACOS
case EventWindowDestroyed:
ProcessEventWindowDestroyed();
break;
#endif
default:
_sawmill.Error($"Unknown SDL3 event type: {evb.GetType().Name}");
break;
@@ -255,5 +260,15 @@ internal partial class Clyde
{
_clyde.SendInputModeChanged();
}
#if MACOS
private void ProcessEventWindowDestroyed()
{
// For some reason, on macOS, closing a secondary window
// causes the GL context on the primary thread to crap itself.
// Rebinding it seems to fix it.
GLMakeContextCurrent(_clyde._mainWindow);
}
#endif
}
}

View File

@@ -46,6 +46,10 @@ internal partial class Clyde
}
}
// NOTE: Giving a parent window is required to avoid the file dialog being blocking on macOS.
var mainWindow = (Sdl3WindowReg)_clyde._mainWindow!;
SDL.SDL_SetPointerProperty(props, SDL.SDL_PROP_FILE_DIALOG_WINDOW_POINTER, mainWindow.Sdl3Window);
var task = ShowFileDialogWithProperties(type, props);
SDL.SDL_DestroyProperties(props);

View File

@@ -278,5 +278,9 @@ internal partial class Clyde
private sealed class EventKeyMapChanged : EventBase;
private sealed class EventQuit : EventBase;
#if MACOS
private sealed class EventWindowDestroyed : EventBase;
#endif
}
}

View File

@@ -7,8 +7,10 @@ using Robust.Shared.Maths;
using SDL3;
using TerraFX.Interop.Windows;
using TerraFX.Interop.Xlib;
#if WINDOWS
using BOOL = TerraFX.Interop.Windows.BOOL;
using Windows = TerraFX.Interop.Windows.Windows;
#endif
using GLAttr = SDL3.SDL.SDL_GLAttr;
using X11Window = TerraFX.Interop.Xlib.Window;
@@ -142,9 +144,12 @@ internal partial class Clyde
});
}
private static void WinThreadWinDestroy(CmdWinDestroy cmd)
private void WinThreadWinDestroy(CmdWinDestroy cmd)
{
SDL.SDL_DestroyWindow(cmd.Window);
#if MACOS
SendEvent(new EventWindowDestroyed());
#endif
}
private (nint window, nint context) CreateSdl3WindowForRenderer(
@@ -461,6 +466,7 @@ internal partial class Clyde
var reg = (Sdl3WindowReg)window;
var windowPtr = WinPtr(reg);
#if WINDOWS
// On Windows, SwapBuffers does not correctly sync to the DWM compositor.
// This means OpenGL vsync is effectively broken by default on Windows.
// We manually sync via DwmFlush(). GLFW does this automatically, SDL3 does not.
@@ -473,7 +479,7 @@ internal partial class Clyde
var dwmFlush = false;
var swapInterval = 0;
if (OperatingSystem.IsWindows() && !reg.Fullscreen && reg.SwapInterval > 0)
if (!reg.Fullscreen && reg.SwapInterval > 0)
{
BOOL compositing;
// 6.2 is Windows 8
@@ -492,9 +498,12 @@ internal partial class Clyde
swapInterval = reg.SwapInterval;
}
}
#endif
//_sawmill.Debug($"Swapping: {window.Id} @ {_clyde._gameTiming.CurFrame}");
SDL.SDL_GL_SwapWindow(windowPtr);
#if WINDOWS
if (dwmFlush)
{
var i = swapInterval;
@@ -505,6 +514,7 @@ internal partial class Clyde
SDL.SDL_GL_SetSwapInterval(swapInterval);
}
#endif
}
public uint? WindowGetX11Id(WindowReg window)
@@ -547,17 +557,18 @@ internal partial class Clyde
public void TextInputSetRect(WindowReg reg, UIBox2i rect, int cursor)
{
var ratio = ((Sdl3WindowReg)reg).PixelRatio;
SendCmd(new CmdTextInputSetRect
{
Window = WinPtr(reg),
Rect = new SDL.SDL_Rect
{
x = rect.Left,
y = rect.Top,
w = rect.Width,
h = rect.Height
x = (int)(rect.Left / ratio.X),
y = (int)(rect.Top / ratio.Y),
w = (int)(rect.Width / ratio.X),
h = (int)(rect.Height / ratio.Y)
},
Cursor = cursor
Cursor = (int)(cursor / ratio.X)
});
}

View File

@@ -61,6 +61,10 @@ internal partial class Clyde
// https://github.com/libsdl-org/SDL/issues/11813
SDL.SDL_SetHint(SDL.SDL_HINT_WINDOWS_GAMEINPUT, "0");
#if MACOS
SDL.SDL_SetHint(SDL.SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, "1");
#endif
var res = SDL.SDL_Init(SDL.SDL_InitFlags.SDL_INIT_VIDEO | SDL.SDL_InitFlags.SDL_INIT_EVENTS);
if (!res)
{

View File

@@ -197,6 +197,13 @@ namespace Robust.Client.Input
locId += "-linux";
}
#if MACOS
if (key == Key.Alt)
{
locId += "-mac";
}
#endif
if (loc.TryGetString(locId, out var name))
return name;

View File

@@ -9,6 +9,7 @@ import sys
import zipfile
import argparse
import glob
from enum import StrEnum
from typing import List, Optional
@@ -28,12 +29,34 @@ except ImportError:
p = os.path.join
PLATFORM_WINDOWS = "windows"
PLATFORM_WIN = "win"
PLATFORM_LINUX = "linux"
PLATFORM_LINUX_ARM64 = "linux-arm64"
PLATFORM_MACOS = "mac"
PLATFORM_OSX = "osx"
PLATFORM_FREEBSD = "freebsd"
TARGET_OS_WINDOWS = "Windows"
TARGET_OS_MACOS = "MacOS"
TARGET_OS_LINUX = "Linux"
TARGET_OS_FREEBSD = "FreeBSD"
class TargetOS(StrEnum):
Windows = "Windows"
MacOS = "MacOS"
Linux = "Linux"
FreeBSD = "FreeBSD"
RID_WIN_X64 = f"{PLATFORM_WIN}-x64"
RID_WIN_ARM64 = f"{PLATFORM_WIN}-arm64"
RID_LINUX_X64 = f"{PLATFORM_LINUX}-x64"
RID_LINUX_ARM64 = f"{PLATFORM_LINUX}-arm64"
RID_OSX_X64 = f"{PLATFORM_OSX}-x64"
RID_OSX_ARM64 = f"{PLATFORM_OSX}-arm64"
RID_FREEBSD_X64 = f"{PLATFORM_FREEBSD}-x64"
RID_FREEBSD_ARM64 = f"{PLATFORM_FREEBSD}-arm64"
DEFAULT_RIDS = [RID_WIN_X64, RID_LINUX_X64, RID_OSX_X64, RID_FREEBSD_X64]
ALL_RIDS = [RID_WIN_X64, RID_WIN_ARM64, RID_LINUX_X64, RID_LINUX_ARM64, RID_OSX_X64, RID_OSX_ARM64, RID_FREEBSD_X64, RID_FREEBSD_ARM64]
IGNORED_RESOURCES = {
".gitignore",
".directory",
@@ -88,7 +111,7 @@ def main() -> None:
parser.add_argument("--platform",
"-p",
action="store",
choices=[PLATFORM_WINDOWS, PLATFORM_MACOS, PLATFORM_LINUX, PLATFORM_FREEBSD],
choices=ALL_RIDS,
nargs="*",
help="Which platform to build for. If not provided, all platforms will be built")
@@ -97,11 +120,17 @@ def main() -> None:
help=argparse.SUPPRESS)
args = parser.parse_args()
platforms = args.platform
skip_build = args.skip_build
platforms: list[str] = args.platform
skip_build: bool = args.skip_build
if not platforms:
platforms = [PLATFORM_WINDOWS, PLATFORM_MACOS, PLATFORM_LINUX, PLATFORM_FREEBSD]
platforms = DEFAULT_RIDS
# Validate that nobody put invalid platform names in.
for rid in platforms:
if rid not in ALL_RIDS:
print(Fore.RED + f"Invalid platform specified: '{rid}'" + Style.RESET_ALL)
exit(1)
if os.path.exists("release"):
print(Fore.BLUE + Style.DIM +
@@ -111,32 +140,25 @@ def main() -> None:
else:
os.mkdir("release")
for platform in platforms:
build_for_platform(platform, skip_build)
if PLATFORM_WINDOWS in platforms:
if not skip_build:
wipe_bin()
build_windows(skip_build)
if PLATFORM_LINUX in platforms:
if not skip_build:
wipe_bin()
build_linux(skip_build, "linux", "Linux")
def build_for_platform(rid: str, skip_build: bool):
print(Fore.GREEN + f"Building for platform '{rid}'..." + Style.RESET_ALL)
if PLATFORM_LINUX_ARM64 in platforms:
if not skip_build:
wipe_bin()
build_linux_arm64(skip_build)
if PLATFORM_MACOS in platforms:
if not skip_build:
wipe_bin()
build_macos(skip_build)
if PLATFORM_FREEBSD in platforms:
if not skip_build:
wipe_bin()
build_linux(skip_build, "freebsd", "FreeBSD")
if not skip_build:
wipe_bin()
platform = rid.split('-', maxsplit=2)[0]
if platform == PLATFORM_WIN:
build_windows(rid, skip_build)
elif platform == PLATFORM_LINUX:
build_linux_like(rid, TargetOS.Linux, skip_build)
elif platform == PLATFORM_OSX:
build_macos(rid, skip_build)
elif platform == PLATFORM_FREEBSD:
build_linux_like(rid, TargetOS.FreeBSD, skip_build)
def wipe_bin():
print(Fore.BLUE + Style.DIM +
@@ -146,53 +168,42 @@ def wipe_bin():
shutil.rmtree("bin")
def build_windows(skip_build: bool) -> None:
# Run a full build.
print(Fore.GREEN + "Building project for Windows x64..." + Style.RESET_ALL)
def build_windows(rid: str, skip_build: bool) -> None:
if not skip_build:
publish_client("win-x64", "Windows")
publish_client(rid, TargetOS.Windows)
if sys.platform != "win32":
subprocess.run(["Tools/exe_set_subsystem.py", p("bin", "Client", "win-x64", "publish", "Robust.Client"), "2"])
subprocess.run(["Tools/exe_set_subsystem.py", p("bin", "Client", rid, "publish", "Robust.Client"), "2"])
print(Fore.GREEN + "Packaging Windows x64 client..." + Style.RESET_ALL)
print(Fore.GREEN + f"Packaging {rid} client..." + Style.RESET_ALL)
client_zip = zipfile.ZipFile(
p("release", "Robust.Client_win-x64.zip"), "w",
p("release", f"Robust.Client_{rid}.zip"), "w",
compression=zipfile.ZIP_DEFLATED)
copy_dir_into_zip(p("bin", "Client", "win-x64", "publish"), "", client_zip, IGNORED_FILES_WINDOWS)
copy_dir_into_zip(p("bin", "Client", rid, "publish"), "", client_zip, IGNORED_FILES_WINDOWS)
copy_resources("Resources", client_zip)
# Cool we're done.
client_zip.close()
def build_macos(skip_build: bool) -> None:
print(Fore.GREEN + "Building project for macOS x64..." + Style.RESET_ALL)
def build_macos(rid: str, skip_build: bool) -> None:
if not skip_build:
publish_client("osx-x64", "MacOS")
publish_client(rid, TargetOS.MacOS)
print(Fore.GREEN + "Packaging macOS x64 client..." + Style.RESET_ALL)
print(Fore.GREEN + f"Packaging {rid} client..." + Style.RESET_ALL)
# Client has to go in an app bundle.
client_zip = zipfile.ZipFile(p("release", "Robust.Client_osx-x64.zip"), "a",
client_zip = zipfile.ZipFile(p("release", f"Robust.Client_{rid}.zip"), "a",
compression=zipfile.ZIP_DEFLATED)
contents = p("Space Station 14.app", "Contents", "Resources")
copy_dir_into_zip(p("BuildFiles", "Mac", "Space Station 14.app"), "Space Station 14.app", client_zip)
copy_dir_into_zip(p("bin", "Client", "osx-x64", "publish"), contents, client_zip, IGNORED_FILES_MACOS)
copy_dir_into_zip(p("bin", "Client", rid, "publish"), contents, client_zip, IGNORED_FILES_MACOS)
copy_resources(p(contents, "Resources"), client_zip)
client_zip.close()
def build_linux(skip_build: bool, platform, name) -> None:
"""Build on Unix-like platforms including Linux and FreeBSD."""
# Run a full build.
rid = "%s-x64" % platform
print(Fore.GREEN + "Building project for %s..." % rid + Style.RESET_ALL)
def build_linux_like(rid: str, target_os: TargetOS, skip_build: bool) -> None:
if not skip_build:
publish_client(rid, name)
publish_client(rid, target_os)
print(Fore.GREEN + "Packaging %s client..." % rid + Style.RESET_ALL)
@@ -206,37 +217,7 @@ def build_linux(skip_build: bool, platform, name) -> None:
client_zip.close()
def build_linux_arm64(skip_build: bool) -> None:
# Run a full build.
# TODO: Linux-arm64 is currently server-only.
pass
""" print(Fore.GREEN + "Building project for Linux ARM64 (SERVER ONLY)..." + Style.RESET_ALL)
if not skip_build:
subprocess.run([
"dotnet",
"build",
"SpaceStation14.sln",
"-c", "Release",
"--nologo",
"/v:m",
"/p:TargetOS=Linux",
"/t:Rebuild",
"/p:FullRelease=True"
], check=True)
publish_client("linux-arm64", "Linux", True)
print(Fore.GREEN + "Packaging Linux ARM64 server..." + Style.RESET_ALL)
server_zip = zipfile.ZipFile(p("release", "SS14.Server_Linux_ARM64.zip"), "w",
compression=zipfile.ZIP_DEFLATED)
copy_dir_into_zip(p("RobustToolbox", "bin", "Server", "linux-arm64", "publish"), "", server_zip)
copy_resources(p("Resources"), server_zip, server=True)
server_zip.close()"""
def publish_client(runtime: str, target_os: str) -> None:
def publish_client(runtime: str, target_os: TargetOS) -> None:
base = [
"dotnet", "publish",
"--runtime", runtime,