Compare commits

..

1 Commits

Author SHA1 Message Date
ParantezTech f69f81f682 [HLE] Add RandomExports HLE 2026-07-18 23:33:45 +03:00
20 changed files with 888 additions and 839 deletions
+3 -3
View File
@@ -14,9 +14,9 @@ available, presents its decoded BGRA frames at the normal guest-flip boundary.
This preserves the game's own timing and lets the host Vulkan presenter display
the movie without trying to execute the PS5-specific Bink GPU decode path.
Without an adapter, Bink files remain visible to the guest and the game's
statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when
explicitly testing a title whose cinematics are optional.
Without an adapter, Bink movies are skipped by default: their open call returns
not-found so games that mark cinematics as optional progress to their next
state instead of waiting on an empty Bink GPU texture.
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestFeature"
"rollForward": "disable"
}
}
+5
View File
@@ -45,6 +45,11 @@ internal static partial class Program
[STAThread]
private static int Main(string[] args)
{
// Avoid blocking full collections while guest and render threads are
// running, and establish the GC mode before the runtime reserves the
// fixed guest address-space window.
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
try
{
return Run(args);
+1
View File
@@ -49,6 +49,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
+603
View File
@@ -0,0 +1,603 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.8, )",
"resolved": "10.0.8",
"contentHash": "dVbSXGIFNR5nZcv2tOLoWI+a9T4jtFd77IYjuND+QVe360qWgAF7H0WtoopYhRw/+SgpGUTyrkrh+65+ClNnfw=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "11.3.2",
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
},
"Avalonia.FreeDesktop": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
"dependencies": {
"Avalonia": "11.3.18",
"Tmds.DBus.Protocol": "0.21.3"
}
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
},
"Avalonia.Skia": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
"dependencies": {
"Avalonia": "11.3.18",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
}
},
"Avalonia.Win32": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
}
},
"Avalonia.X11": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.FreeDesktop": "11.3.18",
"Avalonia.Skia": "11.3.18"
}
},
"HarfBuzzSharp": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Input.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Silk.NET.Input.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.core": {
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
"SharpEmu.HLE": "[0.0.2-beta.3, )",
"SharpEmu.Libs": "[0.0.2-beta.3, )",
"SharpEmu.Logging": "[0.0.2-beta.3, )"
}
},
"sharpemu.debugger": {
"type": "Project",
"dependencies": {
"SharpEmu.Core": "[0.0.2-beta.3, )",
"SharpEmu.HLE": "[0.0.2-beta.3, )",
"SharpEmu.Logging": "[0.0.2-beta.3, )"
}
},
"sharpemu.gui": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.3.18, )",
"Avalonia.Desktop": "[11.3.18, )",
"Avalonia.Fonts.Inter": "[11.3.18, )",
"Avalonia.Themes.Fluent": "[11.3.18, )",
"SharpEmu.Core": "[0.0.2-beta.3, )",
"SharpEmu.Libs": "[0.0.2-beta.3, )",
"SharpEmu.Logging": "[0.0.2-beta.3, )",
"Tmds.DBus.Protocol": "[0.21.3, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[0.0.2-beta.3, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[0.0.2-beta.3, )",
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )",
"SharpEmu.ShaderCompiler.Metal": "[0.0.2-beta.3, )",
"SharpEmu.ShaderCompiler.Vulkan": "[0.0.2-beta.3, )",
"Silk.NET.Input": "[2.23.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
"Silk.NET.Windowing": "[2.23.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"sharpemu.shadercompiler": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[0.0.2-beta.3, )"
}
},
"sharpemu.shadercompiler.metal": {
"type": "Project",
"dependencies": {
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )"
}
},
"sharpemu.shadercompiler.vulkan": {
"type": "Project",
"dependencies": {
"SharpEmu.ShaderCompiler": "[0.0.2-beta.3, )"
}
},
"Avalonia": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Input": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Input.Glfw": "2.23.0"
}
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"Silk.NET.Core": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.EXT": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.KHR": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Tmds.DBus.Protocol": {
"type": "CentralTransitive",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
}
},
"net10.0/linux-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-arm64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/win-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
}
}
}
@@ -1410,8 +1410,6 @@ public sealed partial class DirectExecutionBackend
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Q2V+iqvjgC0" or // vsnprintf
"AV6ipCNa4Rw" or // strcasecmp
"viiwFMaNamA" or // strstr
"q1cHNfGycLI" or // scePadRead
"xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent
@@ -1573,8 +1571,6 @@ public sealed partial class DirectExecutionBackend
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
"AV6ipCNa4Rw" or // strcasecmp
"viiwFMaNamA" or // strstr
"pNtJdE3x49E" or // wcscmp
"fV2xHER+bKE" or // wcscoll
"E8wCoUEbfzk" or // wcsncmp
@@ -1408,54 +1408,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
0x75, 0xE7,
0xC3,
],
"AV6ipCNa4Rw" =>
[
0x0F, 0xB6, 0x07,
0x0F, 0xB6, 0x16,
0x8D, 0x48, 0xBF,
0x83, 0xF9, 0x19,
0x77, 0x03,
0x83, 0xC0, 0x20,
0x8D, 0x4A, 0xBF,
0x83, 0xF9, 0x19,
0x77, 0x03,
0x83, 0xC2, 0x20,
0x29, 0xD0,
0x75, 0x0C,
0x85, 0xD2,
0x74, 0x08,
0x48, 0xFF, 0xC7,
0x48, 0xFF, 0xC6,
0xEB, 0xD4,
0xC3,
],
"viiwFMaNamA" =>
[
0x0F, 0xB6, 0x16,
0x84, 0xD2,
0x74, 0x2D,
0x0F, 0xB6, 0x07,
0x84, 0xC0,
0x74, 0x2A,
0x38, 0xD0,
0x75, 0x1D,
0x4C, 0x8D, 0x47, 0x01,
0x4C, 0x8D, 0x4E, 0x01,
0x41, 0x0F, 0xB6, 0x09,
0x84, 0xC9,
0x74, 0x12,
0x41, 0x38, 0x08,
0x75, 0x08,
0x49, 0xFF, 0xC0,
0x49, 0xFF, 0xC1,
0xEB, 0xEB,
0x48, 0xFF, 0xC7,
0xEB, 0xD3,
0x48, 0x89, 0xF8,
0xC3,
0x31, 0xC0,
0xC3,
],
"pNtJdE3x49E" or "fV2xHER+bKE" =>
[
0x0F, 0xB7, 0x07,
+1 -1
View File
@@ -252,7 +252,7 @@ public static unsafe class JitStubs
var pattern = TlsAccessPattern;
var end = start + length - pattern.Length;
for (var ptr = start; ptr <= end; ptr++)
for (var ptr = start; ptr < end; ptr++)
{
if (MatchesPattern(ptr, pattern))
{
+10 -46
View File
@@ -92,11 +92,6 @@ public partial class MainWindow : Window
// plain window color remains the fallback when the asset fails to load.
private Bitmap? _defaultBackdrop;
// Whether the native loading/closing popup should be showing; it is a
// desktop-topmost popup, so it closes while the launcher is in the
// background or minimized and reopens from this flag on activation.
private bool _sessionLoadingActive;
// Controller navigation state.
private readonly DispatcherTimer _gamepadTimer;
private HostGamepadButtons _previousPadButtons;
@@ -155,18 +150,8 @@ public partial class MainWindow : Window
};
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
// Native popups float above every window on the desktop; they must
// follow the launcher into the background or a minimized state.
Activated += (_, _) =>
{
UpdateSessionBarVisibility();
SessionLoadingPopup.IsOpen = _sessionLoadingActive;
};
Deactivated += (_, _) =>
{
SessionBarPopup.IsOpen = false;
SessionLoadingPopup.IsOpen = false;
};
Activated += (_, _) => UpdateSessionBarVisibility();
Deactivated += (_, _) => SessionBarPopup.IsOpen = false;
TitleBar.PointerPressed += OnTitleBarPointerPressed;
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
@@ -429,15 +414,6 @@ public partial class MainWindow : Window
return;
}
if (_isRunning || _isStopping)
{
// The game renders inside the launcher window, so the launcher
// stays active while playing. The controller belongs to the game
// then: no navigation, and Circle/B must never stop the session.
_previousPadButtons = pad.Buttons;
return;
}
var shoulderPressed = pad.Buttons & ~_previousPadButtons;
if ((shoulderPressed & HostGamepadButtons.L1) != 0)
{
@@ -487,6 +463,11 @@ public partial class MainWindow : Window
LaunchSelected();
}
if ((pressed & HostGamepadButtons.Circle) != 0)
{
StopEmulator();
}
_previousPadButtons = pad.Buttons;
}
@@ -1645,23 +1626,13 @@ public partial class MainWindow : Window
base.OnPropertyChanged(change);
if (change.Property == WindowStateProperty)
{
// The XAML WindowState="Maximized" assignment raises this change
// during InitializeComponent, before named controls are wired up.
if (WindowState == WindowState.Minimized)
{
_sndPreview.Pause();
if (SessionLoadingPopup is { } popup)
{
popup.IsOpen = false;
}
}
else
{
_sndPreview.Resume();
if (SessionLoadingPopup is { } popup)
{
popup.IsOpen = _sessionLoadingActive;
}
}
}
}
@@ -2038,7 +2009,7 @@ public partial class MainWindow : Window
ContentToolbar.IsVisible = false;
ConsolePanel.IsVisible = false;
LaunchBar.IsVisible = false;
HideSessionLoading();
SessionLoadingPopup.IsOpen = false;
UpdateSessionBarVisibility();
}
});
@@ -2138,7 +2109,7 @@ public partial class MainWindow : Window
GameView.IsVisible = false;
GameView.IsHitTestVisible = true;
SessionBarPopup.IsOpen = false;
HideSessionLoading();
SessionLoadingPopup.IsOpen = false;
AnimateLibraryBlur(0, clearWhenComplete: true);
MainContent.Margin = new Thickness(32, 24, 32, 20);
ContentToolbar.IsVisible = true;
@@ -2222,14 +2193,7 @@ public partial class MainWindow : Window
{
SessionLoadingTitle.Text = title;
SessionLoadingDetail.Text = detail;
_sessionLoadingActive = true;
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
}
private void HideSessionLoading()
{
_sessionLoadingActive = false;
SessionLoadingPopup.IsOpen = false;
SessionLoadingPopup.IsOpen = true;
}
private void ReturnToLibraryWhileStopping()
+59 -40
View File
@@ -568,8 +568,6 @@ public static partial class AgcExports
public ulong WorkSequence { get; set; }
public ulong SubmissionSequence { get; set; }
public bool WaitMonitorRunning { get; set; }
public object WaitMonitorSignalGate { get; } = new();
public long WaitMonitorSignalVersion { get; set; }
}
private readonly record struct RegisteredAgcResource(
@@ -3315,11 +3313,24 @@ public static partial class AgcExports
length,
op,
out var dispatch,
out _))
out var indirectDimsRetryAddress))
{
state.FrameDispatchCount++;
ObserveComputeDispatch(ctx, gpuState, state, dispatch);
}
else if (indirectDimsRetryAddress != 0 &&
HandleSubmittedIndirectDimsWait(
ctx,
state,
commandAddress,
currentAddress,
offset,
dwordCount,
indirectDimsRetryAddress,
tracePackets))
{
return true; // suspend until the producer computes the dims
}
}
if (op == ItNop &&
@@ -3657,11 +3668,27 @@ public static partial class AgcExports
void CompleteAndWake()
{
CompleteLabelProducer(producer);
lock (gpuState.WaitMonitorSignalGate)
if (GpuWaitRegistry.Count == 0)
{
gpuState.WaitMonitorSignalVersion++;
Monitor.Pulse(gpuState.WaitMonitorSignalGate);
return;
}
// Resuming a DCB can enqueue another compute dispatch and wait for
// it. Never do that reentrantly on the Vulkan render thread.
ThreadPool.UnsafeQueueUserWorkItem(
static state =>
{
var (resumeContext, resumeGpuState) = state;
lock (resumeGpuState.Gate)
{
DrainResumableDcbs(
resumeContext,
resumeGpuState,
tracePackets: _traceAgc);
}
},
(ctx, gpuState),
preferLocal: false);
}
void ApplyAndQueueCompletion()
@@ -4840,45 +4867,38 @@ public static partial class AgcExports
SubmittedGpuState gpuState)
{
var delayMilliseconds = 1;
long observedSignal;
lock (gpuState.WaitMonitorSignalGate)
{
observedSignal = gpuState.WaitMonitorSignalVersion;
}
while (true)
{
int resumed;
int remaining;
var madeProgress = false;
lock (gpuState.Gate)
{
resumed = DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc);
remaining = GpuWaitRegistry.CountForMemory(ctx.Memory);
if (_traceAgc && resumed != 0)
var before = GpuWaitRegistry.CountForMemory(ctx.Memory);
if (before == 0)
{
gpuState.WaitMonitorRunning = false;
return;
}
DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc);
var after = GpuWaitRegistry.CountForMemory(ctx.Memory);
madeProgress = after < before;
if (madeProgress)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] agc.wait_monitor_resumed count={resumed} " +
$"remaining={remaining}");
$"[LOADER][TRACE] agc.wait_monitor_resumed count={before - after} " +
$"remaining={after}");
}
if (remaining == 0)
if (after == 0)
{
gpuState.WaitMonitorRunning = false;
return;
}
}
delayMilliseconds = resumed != 0
delayMilliseconds = madeProgress
? 1
: Math.Min(delayMilliseconds * 2, 16);
lock (gpuState.WaitMonitorSignalGate)
{
if (gpuState.WaitMonitorSignalVersion == observedSignal)
{
Monitor.Wait(gpuState.WaitMonitorSignalGate, delayMilliseconds);
}
observedSignal = gpuState.WaitMonitorSignalVersion;
}
Thread.Sleep(delayMilliseconds);
}
}
@@ -4932,17 +4952,16 @@ public static partial class AgcExports
// guest memory (labels are advanced by ReleaseMem/WriteData/DmaData packets
// or direct CPU writes) and resumes the ones now satisfied. A resumed DCB
// can itself write labels that unblock others, so loop to a fixed point.
private static int DrainResumableDcbs(
private static void DrainResumableDcbs(
CpuContext ctx,
SubmittedGpuState gpuState,
bool tracePackets)
{
if (!_gpuWaitSuspendEnabled)
{
return 0;
return;
}
var resumedCount = 0;
for (var pass = 0; pass < 256; pass++)
{
var woken = GpuWaitRegistry.CollectSatisfied(ctx.Memory, (address, is64Bit) =>
@@ -5020,7 +5039,7 @@ public static partial class AgcExports
}
}
return resumedCount;
return;
}
if (woken is not null)
@@ -5028,12 +5047,9 @@ public static partial class AgcExports
foreach (var waiter in woken)
{
ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets);
resumedCount++;
}
}
}
return resumedCount;
}
private static void ResumeSuspendedDcb(
@@ -8878,7 +8894,7 @@ public static partial class AgcExports
out _);
var globalMemoryBuffers =
CreateTranslatedComputeGlobalBuffers(evaluation);
GuestGpu.Current.SubmitComputeDispatch(
var workSequence = GuestGpu.Current.SubmitComputeDispatch(
shaderAddress,
computeShader,
textures,
@@ -8897,9 +8913,12 @@ public static partial class AgcExports
dispatch.ThreadCountX,
dispatch.ThreadCountY,
dispatch.ThreadCountZ);
// Vulkan queue order keeps dependent dispatches coherent. CPU visibility is
// published by explicit PM4 release/write actions instead of per dispatch.
gpuDispatch = true;
if (writesGlobalMemory &&
!GuestGpu.Current.WaitForGuestWork(workSequence))
{
computeError = $"global-write-sync-timeout sequence={workSequence}";
}
}
}
@@ -27,7 +27,7 @@ internal static class AgcShaderCompilerHooks
internal static void Install()
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
KernelMemoryCompatExports.TryReadShaderGuestMemory;
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
GuestDataPool.Shared;
}
+17 -10
View File
@@ -6,7 +6,6 @@ using SharpEmu.Libs.Kernel;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Ampr;
@@ -44,17 +43,17 @@ public static class AmprExports
{
public CachedHostFile(string path)
{
Handle = File.OpenHandle(
Stream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete,
bufferSize: 1024 * 1024,
FileOptions.RandomAccess);
Length = RandomAccess.GetLength(Handle);
}
public SafeFileHandle Handle { get; }
public long Length { get; }
public object Gate { get; } = new();
public FileStream Stream { get; }
}
[SysAbiExport(
@@ -736,7 +735,13 @@ public static class AmprExports
return openResult;
}
if (fileOffset >= (ulong)cachedFile.Length)
long fileLength;
lock (cachedFile.Gate)
{
fileLength = cachedFile.Stream.Length;
}
if (fileOffset >= (ulong)fileLength)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -755,10 +760,12 @@ public static class AmprExports
}
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
var read = RandomAccess.Read(
cachedFile.Handle,
buffer.AsSpan(0, request),
unchecked((long)absoluteOffset));
int read;
lock (cachedFile.Gate)
{
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
read = cachedFile.Stream.Read(buffer, 0, request);
}
if (read <= 0)
{
+9 -14
View File
@@ -29,9 +29,10 @@ internal static class Bink2MovieBridge
private static bool _availabilityReported;
/// <summary>
/// Returns true only when movie skipping was explicitly requested. Without
/// a host adapter the guest must be allowed to run the Bink implementation
/// statically linked into its executable.
/// Returns true when the guest should receive a normal "file not found"
/// result for a Bink movie. This is the safe default without a decoder:
/// games that treat movies as optional fall through to their next state
/// rather than submitting an empty Bink GPU texture forever.
/// </summary>
internal static bool ShouldSkipGuestMovie(string hostPath) =>
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
@@ -52,18 +53,12 @@ internal static class Bink2MovieBridge
return;
}
var mode = ResolveMode();
if (mode == MovieMode.Dummy)
if (ResolveMode() == MovieMode.Dummy)
{
AttachDummyMovieLocked(hostPath);
return;
}
if (mode != MovieMode.Native)
{
return;
}
var adapter = GetAdapterLocked();
if (adapter is null)
{
@@ -170,15 +165,16 @@ internal static class Bink2MovieBridge
return MovieMode.Skip;
}
// Prefer the optional host adapter when one is supplied. Otherwise let
// the game's statically linked Bink implementation consume the file.
// With no SDK adapter present, returning "not found" makes optional
// cinematics advance. Supplying either an explicit path or the normal
// side-by-side adapter enables native playback automatically.
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
EnumerateAdapterCandidates().Any(File.Exists))
{
return MovieMode.Native;
}
return MovieMode.Guest;
return MovieMode.Skip;
}
private static void AttachDummyMovieLocked(string hostPath)
@@ -339,7 +335,6 @@ internal static class Bink2MovieBridge
private enum MovieMode
{
Guest,
Skip,
Dummy,
Native,
+2 -121
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Numerics;
namespace SharpEmu.Libs.Gpu;
@@ -16,125 +15,7 @@ namespace SharpEmu.Libs.Gpu;
/// </summary>
internal static class GuestDataPool
{
public static ArrayPool<byte> Shared { get; } = new BoundedByteArrayPool(
public static ArrayPool<byte> Shared { get; } = ArrayPool<byte>.Create(
maxArrayLength: 16 * 1024 * 1024,
maxCachedBytes: 256UL * 1024 * 1024,
maxArraysPerBucket: 8);
public static void Trim() => ((BoundedByteArrayPool)Shared).Trim();
private sealed class BoundedByteArrayPool : ArrayPool<byte>
{
private readonly object _gate = new();
private readonly int _maxArrayLength;
private readonly ulong _maxCachedBytes;
private readonly int _maxArraysPerBucket;
private readonly Dictionary<int, Stack<byte[]>> _cachedByBucket = [];
private readonly HashSet<byte[]> _leases =
new(System.Collections.Generic.ReferenceEqualityComparer.Instance);
private ulong _cachedBytes;
public BoundedByteArrayPool(
int maxArrayLength,
ulong maxCachedBytes,
int maxArraysPerBucket)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArrayLength);
ArgumentOutOfRangeException.ThrowIfZero(maxCachedBytes);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArraysPerBucket);
_maxArrayLength = maxArrayLength;
_maxCachedBytes = maxCachedBytes;
_maxArraysPerBucket = maxArraysPerBucket;
}
public override byte[] Rent(int minimumLength)
{
ArgumentOutOfRangeException.ThrowIfNegative(minimumLength);
var length = GetAllocationLength(minimumLength);
byte[]? array = null;
lock (_gate)
{
if (length <= _maxArrayLength &&
_cachedByBucket.TryGetValue(length, out var bucket) &&
bucket.TryPop(out array))
{
_cachedBytes -= (ulong)array.LongLength;
}
array ??= new byte[length];
_leases.Add(array);
}
return array;
}
public override void Return(byte[] array, bool clearArray = false)
{
ArgumentNullException.ThrowIfNull(array);
lock (_gate)
{
if (!_leases.Remove(array))
{
return;
}
}
if (clearArray)
{
Array.Clear(array);
}
lock (_gate)
{
if (array.Length > _maxArrayLength ||
!IsBucketLength(array.Length) ||
(ulong)array.LongLength > _maxCachedBytes -
Math.Min(_cachedBytes, _maxCachedBytes))
{
return;
}
if (!_cachedByBucket.TryGetValue(array.Length, out var bucket))
{
bucket = new Stack<byte[]>();
_cachedByBucket.Add(array.Length, bucket);
}
if (bucket.Count >= _maxArraysPerBucket)
{
return;
}
bucket.Push(array);
_cachedBytes += (ulong)array.LongLength;
}
}
public void Trim()
{
lock (_gate)
{
_cachedByBucket.Clear();
_cachedBytes = 0;
}
}
private int GetAllocationLength(int minimumLength)
{
if (minimumLength <= 16)
{
return 16;
}
if (minimumLength > _maxArrayLength)
{
return minimumLength;
}
return checked((int)BitOperations.RoundUpToPowerOf2((uint)minimumLength));
}
private static bool IsBucketLength(int length) =>
length >= 16 && (length & (length - 1)) == 0;
}
maxArraysPerBucket: 96);
}
@@ -6010,7 +6010,7 @@ public static partial class KernelMemoryCompatExports
return highWaterMark;
}
private static unsafe bool TryReadHostMemory(ulong address, Span<byte> destination)
private static bool TryReadHostMemory(ulong address, Span<byte> destination)
{
if (destination.IsEmpty || !IsHostRangeAccessible(address, (ulong)destination.Length, writeAccess: false))
{
@@ -6019,7 +6019,9 @@ public static partial class KernelMemoryCompatExports
try
{
new ReadOnlySpan<byte>((void*)address, destination.Length).CopyTo(destination);
var temporary = new byte[destination.Length];
Marshal.Copy((nint)address, temporary, 0, temporary.Length);
temporary.AsSpan().CopyTo(destination);
return true;
}
catch
@@ -6059,41 +6061,6 @@ public static partial class KernelMemoryCompatExports
return false;
}
internal static bool TryReadShaderGuestMemory(
ulong address,
Span<byte> destination)
{
if (destination.IsEmpty)
{
return true;
}
if (TryReadTrackedLibcHeap(address, destination))
{
return true;
}
var length = (ulong)destination.Length;
lock (_memoryGate)
{
if (TryFindVirtualQueryRegionLocked(
address,
findNext: false,
out var region) &&
length <= region.Length &&
address >= region.Address &&
length <= region.Address + region.Length - address)
{
return TryReadHostMemory(address, destination);
}
}
// Direct execution uses guest virtual addresses as host virtual addresses.
// Some native mmap paths predate _mappedRegions tracking, so retain the same
// committed/readable-page fallback used by the libc compatibility layer.
return TryReadHostMemory(address, destination);
}
internal static bool TryReadTrackedLibcHeapGpuAlias(
ulong packedAddress,
Span<byte> destination)
@@ -6373,7 +6340,7 @@ public static partial class KernelMemoryCompatExports
return value != 0 && (value & (value - 1)) == 0;
}
private static unsafe bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
private static bool TryWriteHostMemory(ulong address, ReadOnlySpan<byte> source)
{
if (source.IsEmpty || !IsHostRangeAccessible(address, (ulong)source.Length, writeAccess: true))
{
@@ -6382,7 +6349,8 @@ public static partial class KernelMemoryCompatExports
try
{
source.CopyTo(new Span<byte>((void*)address, source.Length));
var temporary = source.ToArray();
Marshal.Copy(temporary, 0, (nint)address, temporary.Length);
return true;
}
catch
@@ -6409,37 +6377,20 @@ public static partial class KernelMemoryCompatExports
return false;
}
var endAddress = address + length - 1;
var currentAddress = address;
while (currentAddress <= endAddress)
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
{
if (!TryQueryHostPage(currentAddress, out var info) ||
!HasRequiredProtection(info.Protect, writeAccess))
{
return false;
}
return false;
}
var regionBase = unchecked((ulong)info.BaseAddress);
var regionSize = (ulong)info.RegionSize;
if (regionSize == 0 ||
regionBase > currentAddress ||
ulong.MaxValue - regionBase < regionSize)
{
return false;
}
var endAddress = address + length - 1;
if (endAddress == address)
{
return true;
}
var regionEnd = regionBase + regionSize;
if (regionEnd <= currentAddress)
{
return false;
}
if (regionEnd > endAddress)
{
return true;
}
currentAddress = regionEnd;
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
{
return false;
}
return true;
+7 -10
View File
@@ -36,7 +36,6 @@ public static class PerfOverlay
private static long _presentedInWindow;
private static long _submittedInWindow;
private static long _drawsInWindow;
private static long _guestBufferCacheBytes;
// Refreshed once per second so per-frame fills never allocate.
private static long _statsWindowStart = Stopwatch.GetTimestamp();
@@ -75,8 +74,11 @@ public static class PerfOverlay
if (last != 0)
{
var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
if (milliseconds < 1000.0)
{
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
}
}
}
@@ -86,9 +88,6 @@ public static class PerfOverlay
/// <summary>Called per translated draw/dispatch executed.</summary>
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
public static void SetGuestBufferCacheBytes(ulong bytes) =>
Interlocked.Exchange(ref _guestBufferCacheBytes, checked((long)bytes));
/// <summary>
/// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight.
/// Runs on the render thread.
@@ -166,7 +165,7 @@ public static class PerfOverlay
Environment.ProcessorCount;
_lastCpuTime = cpuTime;
var drawsPerFrame = _fps > 0 ? _drawsPerSecond / _fps : 0;
var drawsPerFrame = _fps > 0.5 ? _drawsPerSecond / _fps : 0;
var sessionStart = Interlocked.Read(ref _sessionStartTimestamp);
var elapsedSeconds = sessionStart == 0
? 0L
@@ -177,9 +176,7 @@ public static class PerfOverlay
_line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS";
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
var heapMb = GC.GetTotalMemory(false) / (1024 * 1024);
var guestBufferMb = Interlocked.Read(ref _guestBufferCacheBytes) / (1024 * 1024);
_line4 = $"MEM {heapMb}M BUF {guestBufferMb}M CPU {_cpuPercent:0}%";
_line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE";
_line5 = $"TIME {elapsedHours:00}:{elapsedMinutes:00}:{elapsedRemainingSeconds:00}";
}
}
+150 -346
View File
@@ -2412,9 +2412,7 @@ internal static unsafe class VulkanVideoPresenter
}
}
private static bool TryTakeGuestWork(
out PendingGuestWork work,
HashSet<string>? excludedQueues = null)
private static bool TryTakeGuestWork(out PendingGuestWork work)
{
lock (_gate)
{
@@ -2427,14 +2425,6 @@ internal static unsafe class VulkanVideoPresenter
}
var queueName = _pendingGuestQueueSchedule[_pendingGuestQueueCursor];
if (excludedQueues?.Contains(queueName) == true)
{
_pendingGuestQueueCursor =
(_pendingGuestQueueCursor + 1) % _pendingGuestQueueSchedule.Count;
queuesToProbe--;
continue;
}
if (!_pendingGuestWorkByQueue.TryGetValue(queueName, out var queue) ||
queue.First is not { } first)
{
@@ -2476,32 +2466,6 @@ internal static unsafe class VulkanVideoPresenter
}
}
private static bool RequeueGuestWorkFront(in PendingGuestWork work)
{
lock (_gate)
{
if (_closed)
{
return false;
}
if (!_pendingGuestWorkByQueue.TryGetValue(work.Queue.Name, out var queue))
{
queue = new LinkedList<PendingGuestWork>();
_pendingGuestWorkByQueue.Add(work.Queue.Name, queue);
_pendingGuestQueueSchedule.Add(work.Queue.Name);
}
// TryTakeGuestWork removes only the item count. Payload ownership
// remains live until CompleteGuestWork, so requeueing must not add
// the retained-byte total a second time.
queue.AddFirst(work);
_pendingGuestWorkCount++;
System.Threading.Monitor.PulseAll(_gate);
return true;
}
}
private static void CompleteGuestWork(in PendingGuestWork pending)
{
SharpEmu.HLE.GuestImageWriteTracker.FlushPendingDiagnostics();
@@ -2911,14 +2875,11 @@ internal static unsafe class VulkanVideoPresenter
DescriptorSetLayout DescriptorSetLayout,
PipelineLayout PipelineLayout);
private readonly record struct DirtyGuestBufferRange(
ulong Offset,
ulong Length,
string QueueName,
ulong Timeline);
private readonly record struct DirtyGuestBufferRange(ulong Offset, ulong Length);
private sealed class GuestBufferAllocation
{
public string QueueName = VulkanGuestQueueIdentity.Default.Name;
public ulong BaseAddress;
public ulong Size;
public VkBuffer Buffer;
@@ -2929,6 +2890,8 @@ internal static unsafe class VulkanVideoPresenter
public List<DirtyGuestBufferRange> DirtyRanges { get; } = [];
}
private const string SharedReadOnlyGuestBufferQueue = "shared.readonly";
private sealed class TranslatedDrawResources
{
public string DebugName = "SharpEmu translated";
@@ -4832,9 +4795,7 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty(
allocation,
globalBuffer.GuestOffset,
globalBuffer.GuestSize,
_activeGuestQueue.Name,
_submitTimeline);
globalBuffer.GuestSize);
}
}
}
@@ -5030,7 +4991,7 @@ internal static unsafe class VulkanVideoPresenter
}
}
private bool TryMakeActiveGuestQueueSubmissionsCpuVisible()
private void WaitForActiveGuestQueueSubmissionsForCpuVisibility()
{
FlushBatchedGuestCommands();
if (!_lastSubmittedTimelineByGuestQueue.TryGetValue(
@@ -5038,7 +4999,7 @@ internal static unsafe class VulkanVideoPresenter
out var targetTimeline) ||
targetTimeline <= _completedTimeline)
{
return true;
return;
}
PendingGuestSubmission? target = null;
@@ -5058,77 +5019,27 @@ internal static unsafe class VulkanVideoPresenter
$"{targetTimeline} (completed={_completedTimeline}).");
}
var waitStart = System.Diagnostics.Stopwatch.GetTimestamp();
var fence = target.Fence;
var status = _vk.GetFenceStatus(_device, fence);
if (status == Result.NotReady)
{
return false;
}
if (status == Result.ErrorDeviceLost)
{
_deviceLost = true;
return true;
}
Check(status, $"vkGetFenceStatus(queue visibility: {_activeGuestQueue.Name})");
Check(
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
$"vkWaitForFences(queue visibility: {_activeGuestQueue.Name})");
CollectCompletedGuestSubmissions(waitForOldest: false);
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
1000.0 / System.Diagnostics.Stopwatch.Frequency;
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader(
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " +
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline}");
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
$"waited_ms={waitedMs:F3}");
}
return true;
}
private void WaitForGuestBufferAllocationForCpuVisibility(
GuestBufferAllocation allocation)
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
{
if (IsGuestBufferAllocationReferencedByOpenBatch(allocation))
{
FlushBatchedGuestCommands();
}
var targetTimeline = allocation.LastUseTimeline;
if (targetTimeline <= _completedTimeline)
{
return;
}
PendingGuestSubmission? target = null;
foreach (var submission in _pendingGuestSubmissions)
{
if (submission.Timeline == targetTimeline)
{
target = submission;
break;
}
}
if (target is null)
{
throw new InvalidOperationException(
$"Guest buffer 0x{allocation.BaseAddress:X16} lost pending timeline " +
$"{targetTimeline} (completed={_completedTimeline}).");
}
var fence = target.Fence;
Check(
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
$"vkWaitForFences(buffer visibility: 0x{allocation.BaseAddress:X16})");
CollectCompletedGuestSubmissions(waitForOldest: false);
}
private bool TryExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
{
if (!TryMakeActiveGuestQueueSubmissionsCpuVisible())
{
return false;
}
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
work.Action();
if (_traceVulkanShaderEnabled)
@@ -5138,8 +5049,6 @@ internal static unsafe class VulkanVideoPresenter
$"submission={_activeGuestQueue.SubmissionId} " +
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
}
return true;
}
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
@@ -8277,6 +8186,9 @@ internal static unsafe class VulkanVideoPresenter
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
var endAddress = checked(guestBuffer.BaseAddress + size);
GuestBufferAllocation? allocation = null;
var allocationPriority = -1;
// This runs for every bound global buffer. Preserve the previous
// stable queue preference without allocating LINQ sort state.
foreach (var candidate in _guestBufferAllocations)
{
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
@@ -8285,8 +8197,22 @@ internal static unsafe class VulkanVideoPresenter
continue;
}
allocation = candidate;
break;
var candidatePriority = string.Equals(
candidate.QueueName,
_activeGuestQueue.Name,
StringComparison.Ordinal)
? 2
: string.Equals(
candidate.QueueName,
SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal)
? 1
: 0;
if (candidatePriority > allocationPriority)
{
allocation = candidate;
allocationPriority = candidatePriority;
}
}
if (allocation is null)
@@ -8321,7 +8247,11 @@ internal static unsafe class VulkanVideoPresenter
var shadow = allocation.Shadow.AsSpan(checked((int)guestOffset), guestBuffer.Length);
if (!source.SequenceEqual(shadow))
{
if (!guestBuffer.Writable &&
var sharedReadOnly = string.Equals(
allocation.QueueName,
SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal);
if (sharedReadOnly &&
(allocation.LastUseTimeline > _completedTimeline ||
IsGuestBufferAllocationReferencedByOpenBatch(allocation)))
{
@@ -8335,8 +8265,14 @@ internal static unsafe class VulkanVideoPresenter
// an in-flight shader access. Retire prior users, publish their
// dirty ranges to guest memory, then upload the current guest
// bytes (which may be newer than the parser's captured array).
WaitForGuestBufferAllocationForCpuVisibility(allocation);
WriteBackAllDirtyGuestBuffers();
if (!sharedReadOnly)
{
// Writable aliases are private to one logical guest queue.
// Retiring unrelated queues here recreates the global FIFO
// and turns routine buffer refreshes into queue-wide stalls.
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
}
// Populate the cached shadow copy first and write it out to the
// mapped allocation in one pass. The mapped memory is
// HOST_VISIBLE|HOST_COHERENT (write-combined on most drivers),
@@ -8482,7 +8418,7 @@ internal static unsafe class VulkanVideoPresenter
return;
}
var ranges = new List<(ulong Start, ulong End)>(buffers.Count);
var ranges = new List<(ulong Start, ulong End, bool Writable)>(buffers.Count);
foreach (var buffer in buffers)
{
if (buffer.BaseAddress == 0)
@@ -8496,7 +8432,8 @@ internal static unsafe class VulkanVideoPresenter
var paddedEnd = checked(buffer.BaseAddress + size + 3) & ~3UL;
ranges.Add((
alignedStart,
paddedEnd));
paddedEnd,
buffer.Writable && buffer.WriteBackToGuest));
}
if (ranges.Count == 0)
@@ -8505,7 +8442,7 @@ internal static unsafe class VulkanVideoPresenter
}
ranges.Sort(static (left, right) => left.Start.CompareTo(right.Start));
var merged = new List<(ulong Start, ulong End)>(ranges.Count);
var merged = new List<(ulong Start, ulong End, bool Writable)>(ranges.Count);
foreach (var range in ranges)
{
if (merged.Count == 0 || range.Start > merged[^1].End)
@@ -8517,18 +8454,25 @@ internal static unsafe class VulkanVideoPresenter
var previous = merged[^1];
merged[^1] = (
previous.Start,
Math.Max(previous.End, range.End));
Math.Max(previous.End, range.End),
previous.Writable || range.Writable);
}
foreach (var range in merged)
{
EnsureGuestBufferAllocation(range.Start, range.End);
EnsureGuestBufferAllocation(
range.Start,
range.End,
range.Writable
? _activeGuestQueue.Name
: SharedReadOnlyGuestBufferQueue);
}
}
private void EnsureGuestBufferAllocation(
ulong requestedStart,
ulong requestedEnd)
ulong requestedEnd,
string queueName)
{
var start = requestedStart;
var end = requestedEnd;
@@ -8537,6 +8481,10 @@ internal static unsafe class VulkanVideoPresenter
{
overlaps = _guestBufferAllocations
.Where(allocation =>
string.Equals(
allocation.QueueName,
queueName,
StringComparison.Ordinal) &&
allocation.BaseAddress < end &&
start < allocation.BaseAddress + allocation.Size)
.ToList();
@@ -8573,7 +8521,7 @@ internal static unsafe class VulkanVideoPresenter
WriteBackAllDirtyGuestBuffers();
}
var replacement = CreateGuestBufferAllocation(start, end);
var replacement = CreateGuestBufferAllocation(start, end, queueName);
foreach (var overlap in overlaps)
{
_guestBufferAllocations.Remove(overlap);
@@ -8582,27 +8530,22 @@ internal static unsafe class VulkanVideoPresenter
_guestBufferAllocations.Add(replacement);
_guestBufferAllocations.Sort(static (left, right) =>
left.BaseAddress.CompareTo(right.BaseAddress));
UpdateGuestBufferCacheMetric();
TraceVulkanShader(
$"vk.guest_buffer_allocation base=0x{start:X16} bytes={replacement.Size} " +
$"merged={overlaps.Count}");
}
private void UpdateGuestBufferCacheMetric()
{
var bytes = 0UL;
foreach (var allocation in _guestBufferAllocations)
{
bytes = checked(bytes + allocation.Size);
}
PerfOverlay.SetGuestBufferCacheBytes(bytes);
var queueOrder = string.CompareOrdinal(left.QueueName, right.QueueName);
return queueOrder != 0
? queueOrder
: left.BaseAddress.CompareTo(right.BaseAddress);
});
TraceVulkanShader(
$"vk.guest_buffer_allocation queue={queueName} " +
$"base=0x{start:X16} bytes={replacement.Size} " +
$"merged={overlaps.Count}");
}
private GuestBufferAllocation CreateGuestBufferAllocation(
ulong start,
ulong end)
ulong end,
string queueName)
{
var size = checked(end - start);
if (size == 0 || size > int.MaxValue)
@@ -8628,6 +8571,7 @@ internal static unsafe class VulkanVideoPresenter
$"SharpEmu guest VA 0x{start:X16}-0x{end:X16}");
return new GuestBufferAllocation
{
QueueName = queueName,
BaseAddress = start,
Size = size,
Buffer = buffer,
@@ -9633,6 +9577,15 @@ internal static unsafe class VulkanVideoPresenter
MarkSampledImagesInitialized(resources);
MarkStorageImagesInitialized(resources, traceContents: false);
if (work.WritesGlobalMemory)
{
// The CPU submit thread may immediately consume an indirect
// argument written by this dispatch. Wait for the specific
// guest fences and publish only dirty ranges; a queue-wide
// idle unnecessarily serialized presentation work too.
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
}
TraceVulkanShader(
$"vk.compute_dispatch groups={work.GroupCountX}x" +
$"{work.GroupCountY}x{work.GroupCountZ} " +
@@ -9862,9 +9815,7 @@ internal static unsafe class VulkanVideoPresenter
private static void MarkGuestBufferDirty(
GuestBufferAllocation allocation,
ulong offset,
ulong length,
string queueName,
ulong timeline)
ulong length)
{
if (length == 0)
{
@@ -9876,11 +9827,6 @@ internal static unsafe class VulkanVideoPresenter
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
{
var existing = allocation.DirtyRanges[index];
if (!string.Equals(existing.QueueName, queueName, StringComparison.Ordinal))
{
continue;
}
var existingEnd = existing.Offset + existing.Length;
if (end < existing.Offset || existingEnd < start)
{
@@ -9889,12 +9835,10 @@ internal static unsafe class VulkanVideoPresenter
start = Math.Min(start, existing.Offset);
end = Math.Max(end, existingEnd);
timeline = Math.Max(timeline, existing.Timeline);
allocation.DirtyRanges.RemoveAt(index);
}
allocation.DirtyRanges.Add(
new DirtyGuestBufferRange(start, end - start, queueName, timeline));
allocation.DirtyRanges.Add(new DirtyGuestBufferRange(start, end - start));
}
private void WriteBackAllDirtyGuestBuffers(string? queueName = null)
@@ -9907,51 +9851,33 @@ internal static unsafe class VulkanVideoPresenter
foreach (var allocation in _guestBufferAllocations)
{
if (queueName is not null &&
!string.Equals(allocation.QueueName, queueName, StringComparison.Ordinal))
{
continue;
}
if (allocation.DirtyRanges.Count != 0 &&
allocation.LastUseTimeline > _completedTimeline)
{
// A mapped HOST_COHERENT allocation still cannot be read
// by the CPU while a shader may be writing it. Callers
// normally retire the relevant fences first; keep this
// helper fail-closed if a future path forgets to do so.
TraceVulkanShader(
$"vk.global_writeback_deferred base=0x{allocation.BaseAddress:X16} " +
$"last_use={allocation.LastUseTimeline} completed={_completedTimeline}");
continue;
}
for (var index = allocation.DirtyRanges.Count - 1; index >= 0; index--)
{
var range = allocation.DirtyRanges[index];
if ((queueName is not null &&
!string.Equals(range.QueueName, queueName, StringComparison.Ordinal)) ||
range.Timeline > _completedTimeline)
{
continue;
}
if (range.Length == 0 || range.Length > int.MaxValue)
{
continue;
}
var rangeEnd = checked(range.Offset + range.Length);
var overlapsInFlightWrite = false;
for (var otherIndex = 0;
otherIndex < allocation.DirtyRanges.Count;
otherIndex++)
{
if (otherIndex == index)
{
continue;
}
var other = allocation.DirtyRanges[otherIndex];
if (other.Timeline <= _completedTimeline)
{
continue;
}
var otherEnd = checked(other.Offset + other.Length);
if (range.Offset < otherEnd && other.Offset < rangeEnd)
{
overlapsInFlightWrite = true;
break;
}
}
if (overlapsInFlightWrite)
{
continue;
}
var mappedBytes = new ReadOnlySpan<byte>(
(void*)(allocation.Mapped + checked((nint)range.Offset)),
checked((int)range.Length));
@@ -9981,7 +9907,6 @@ internal static unsafe class VulkanVideoPresenter
const int pageSize = 4096;
const int unreadableMergeGap = 16;
var livePageBuffer = GuestDataPool.Shared.Rent(pageSize);
var mappedPageBuffer = GuestDataPool.Shared.Rent(pageSize);
var pageRuns = new List<(int Start, int Length)>(64);
try
{
@@ -9990,49 +9915,35 @@ internal static unsafe class VulkanVideoPresenter
pageStart += pageSize)
{
var pageEnd = Math.Min(pageStart + pageSize, mappedBytes.Length);
var pageLength = pageEnd - pageStart;
var mappedPageSource = mappedBytes.Slice(pageStart, pageLength);
var shadowPage = shadowBytes.Slice(pageStart, pageLength);
if (mappedPageSource.SequenceEqual(shadowPage))
{
continue;
}
// HOST_COHERENT mappings are commonly uncached or
// write-combined on the CPU. Read each changed page
// once with a bulk copy, then perform the byte-level
// merge against ordinary cached memory.
var mappedPage = mappedPageBuffer.AsSpan(0, pageLength);
mappedPageSource.CopyTo(mappedPage);
pageRuns.Clear();
var cursor = 0;
while (cursor < pageLength)
var cursor = pageStart;
while (cursor < pageEnd)
{
while (cursor < pageLength &&
mappedPage[cursor] == shadowPage[cursor])
while (cursor < pageEnd &&
mappedBytes[cursor] == shadowBytes[cursor])
{
cursor++;
}
if (cursor == pageLength)
if (cursor == pageEnd)
{
break;
}
var runStart = cursor;
while (cursor < pageLength &&
mappedPage[cursor] != shadowPage[cursor])
while (cursor < pageEnd &&
mappedBytes[cursor] != shadowBytes[cursor])
{
cursor++;
}
var runLength = cursor - runStart;
pageRuns.Add((pageStart + runStart, runLength));
pageRuns.Add((runStart, runLength));
changedRuns++;
changedBytes += (ulong)runLength;
if (firstChangedOffset < 0)
{
firstChangedOffset = pageStart + runStart;
firstChangedOffset = runStart;
}
}
@@ -10042,12 +9953,13 @@ internal static unsafe class VulkanVideoPresenter
}
changedPages++;
var pageLength = pageEnd - pageStart;
var livePage = livePageBuffer.AsSpan(0, pageLength);
if (memory.TryRead(guestAddress + (ulong)pageStart, livePage))
{
foreach (var run in pageRuns)
{
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mappedBytes.Slice(run.Start, run.Length).CopyTo(
livePage.Slice(run.Start - pageStart, run.Length));
}
@@ -10055,7 +9967,7 @@ internal static unsafe class VulkanVideoPresenter
{
foreach (var run in pageRuns)
{
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mappedBytes.Slice(run.Start, run.Length).CopyTo(
shadowBytes.Slice(run.Start, run.Length));
}
@@ -10070,9 +9982,7 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty(
allocation,
range.Offset + (ulong)run.Start,
(ulong)run.Length,
range.QueueName,
range.Timeline);
(ulong)run.Length);
}
continue;
@@ -10108,7 +10018,7 @@ internal static unsafe class VulkanVideoPresenter
overlayIndex++)
{
var run = pageRuns[overlayIndex];
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mappedBytes.Slice(run.Start, run.Length).CopyTo(
mergedLive.Slice(
run.Start - mergedStart,
run.Length));
@@ -10124,7 +10034,7 @@ internal static unsafe class VulkanVideoPresenter
overlayIndex++)
{
var run = pageRuns[overlayIndex];
mappedPage.Slice(run.Start - pageStart, run.Length).CopyTo(
mappedBytes.Slice(run.Start, run.Length).CopyTo(
shadowBytes.Slice(run.Start, run.Length));
}
@@ -10141,9 +10051,7 @@ internal static unsafe class VulkanVideoPresenter
exactIndex++)
{
var run = pageRuns[exactIndex];
var changed = mappedPage.Slice(
run.Start - pageStart,
run.Length);
var changed = mappedBytes.Slice(run.Start, run.Length);
fallbackWrites++;
if (memory.TryWrite(
guestAddress + (ulong)run.Start,
@@ -10160,9 +10068,7 @@ internal static unsafe class VulkanVideoPresenter
MarkGuestBufferDirty(
allocation,
range.Offset + (ulong)run.Start,
(ulong)run.Length,
range.QueueName,
range.Timeline);
(ulong)run.Length);
}
}
}
@@ -10171,7 +10077,6 @@ internal static unsafe class VulkanVideoPresenter
finally
{
GuestDataPool.Shared.Return(livePageBuffer);
GuestDataPool.Shared.Return(mappedPageBuffer);
}
var probe = mappedBytes[..Math.Min(mappedBytes.Length, 256)];
@@ -10441,7 +10346,6 @@ internal static unsafe class VulkanVideoPresenter
}
GuestDepthResource? depth = null;
DepthFramebufferResource? depthFramebuffer = null;
var clearDepthSeparately = false;
if (ShouldAttachGuestDepth(
work.DepthTarget,
draw.RenderState.Depth) &&
@@ -10469,26 +10373,12 @@ internal static unsafe class VulkanVideoPresenter
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
}
clearDepthSeparately = clearDepthForDraw &&
(depth.Width < firstTarget.Width ||
depth.Height < firstTarget.Height);
if (targets.Length == 1 && !clearDepthSeparately)
if (targets.Length == 1)
{
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
}
}
if (depth is not null && !clearDepthSeparately)
{
// Guest color images may be allocated at their maximum
// resolution while the active viewport and DB surface use
// a smaller dynamic-rendering extent. Vulkan requires the
// framebuffer extent to fit every attachment.
extent = new Extent2D(
Math.Min(firstTarget.Width, depth.Width),
Math.Min(firstTarget.Height, depth.Height));
}
if (clearDepthForDraw)
{
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
@@ -10522,18 +10412,17 @@ internal static unsafe class VulkanVideoPresenter
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
if (targets.Length > 1)
{
var attachedDepth = clearDepthSeparately ? null : depth;
(renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
formats,
targets.Select(target => target.MipViews.Length > 0
? target.MipViews[0]
: target.View).ToArray(),
extent.Width,
extent.Height,
firstTarget.Width,
firstTarget.Height,
targets.Select(target =>
target.Initialized || target.InitialUploadPending).ToArray(),
attachedDepth,
attachedDepth?.Initialized == true && !clearDepthForDraw);
depth,
depth?.Initialized == true && !clearDepthForDraw);
transientRenderPass = renderPass;
transientFramebuffer = framebuffer;
}
@@ -10544,8 +10433,8 @@ internal static unsafe class VulkanVideoPresenter
formats,
extent,
targets,
hasDepthAttachment: depth is not null && !clearDepthSeparately,
feedbackDepth: clearDepthSeparately ? null : depth);
hasDepthAttachment: depth is not null,
feedbackDepth: depth);
resources.TransientRenderPass = transientRenderPass;
resources.TransientFramebuffer = transientFramebuffer;
transientRenderPass = default;
@@ -10565,10 +10454,6 @@ internal static unsafe class VulkanVideoPresenter
submitted = true;
BeginDebugLabel(_commandBuffer, resources.DebugName);
if (clearDepthSeparately && depth is not null)
{
RecordStandaloneGuestDepthClear(depth);
}
var hasStorageImages = false;
foreach (var texture in resources.Textures)
{
@@ -10632,7 +10517,6 @@ internal static unsafe class VulkanVideoPresenter
toColorAttachments);
if (depth is not null &&
!clearDepthSeparately &&
depth.Layout == ImageLayout.ShaderReadOnlyOptimal)
{
var toDepthAttachment = new ImageMemoryBarrier
@@ -10670,7 +10554,7 @@ internal static unsafe class VulkanVideoPresenter
framebuffer,
extent,
colorAttachmentCount: targets.Length,
hasDepthAttachment: depth is not null && !clearDepthSeparately,
hasDepthAttachment: depth is not null,
clearDepth: depth?.ClearDepth ?? 1f);
RecordTranslatedDrawInPass(resources, extent);
_vk.CmdEndRenderPass(_commandBuffer);
@@ -10726,10 +10610,7 @@ internal static unsafe class VulkanVideoPresenter
if (depth is not null)
{
depth.Initialized = true;
if (!clearDepthSeparately)
{
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
}
depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
if (clearDepthForDraw)
{
depth.InitializationSource = "guest-depth-clear";
@@ -11743,6 +11624,14 @@ internal static unsafe class VulkanVideoPresenter
return existing;
}
if (depth.Width < color.Width || depth.Height < color.Height)
{
throw new InvalidOperationException(
$"guest depth 0x{depth.Address:X16} extent {depth.Width}x{depth.Height} " +
$"is smaller than color target 0x{color.Address:X16} " +
$"{color.Width}x{color.Height}");
}
var attachmentView = color.MipViews.Length > 0 ? color.MipViews[0] : color.View;
var loadRenderPass = CreateDepthRenderPass(
color.Format,
@@ -11769,8 +11658,8 @@ internal static unsafe class VulkanVideoPresenter
RenderPass = loadRenderPass,
AttachmentCount = 2,
PAttachments = attachments,
Width = Math.Min(color.Width, depth.Width),
Height = Math.Min(color.Height, depth.Height),
Width = color.Width,
Height = color.Height,
Layers = 1,
};
Check(
@@ -12266,7 +12155,6 @@ internal static unsafe class VulkanVideoPresenter
EvictDirtyCachedTextures();
var completedWork = 0;
HashSet<string>? deferredOrderedQueues = null;
var renderWorkDeadline = _renderWorkBudgetTicks > 0
? System.Diagnostics.Stopwatch.GetTimestamp() + _renderWorkBudgetTicks
: long.MaxValue;
@@ -12284,7 +12172,7 @@ internal static unsafe class VulkanVideoPresenter
break;
}
if (!TryTakeGuestWork(out var pendingGuestWork, deferredOrderedQueues))
if (!TryTakeGuestWork(out var pendingGuestWork))
{
break;
}
@@ -12311,7 +12199,6 @@ internal static unsafe class VulkanVideoPresenter
_enqueueAsImmediateQueueFollowup = true;
_immediateFollowupTail = null;
var work = pendingGuestWork.Work;
var deferGuestWork = false;
var traceWork = ShouldTracePresentedGuestImageContentsForDiagnostics();
var workStart = traceWork ? System.Diagnostics.Stopwatch.GetTimestamp() : 0L;
@@ -12339,7 +12226,7 @@ internal static unsafe class VulkanVideoPresenter
ExecuteGuestImageWrite(guestImageWrite);
break;
case VulkanOrderedGuestAction orderedAction:
deferGuestWork = !TryExecuteOrderedGuestAction(orderedAction);
ExecuteOrderedGuestAction(orderedAction);
break;
case VulkanOrderedGuestFlip orderedFlip:
ExecuteOrderedGuestFlip(orderedFlip);
@@ -12351,21 +12238,12 @@ internal static unsafe class VulkanVideoPresenter
}
finally
{
if (!deferGuestWork || !RequeueGuestWorkFront(pendingGuestWork))
{
CompleteGuestWork(pendingGuestWork);
}
CompleteGuestWork(pendingGuestWork);
_enqueueAsImmediateQueueFollowup = false;
_immediateFollowupTail = null;
Volatile.Write(ref _executingGuestWorkSequence, 0);
}
if (deferGuestWork)
{
deferredOrderedQueues ??= new HashSet<string>(StringComparer.Ordinal);
deferredOrderedQueues.Add(pendingGuestWork.Queue.Name);
}
if (workStart != 0)
{
var elapsedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - workStart)
@@ -13322,79 +13200,6 @@ internal static unsafe class VulkanVideoPresenter
depth.Layout = ImageLayout.ShaderReadOnlyOptimal;
}
private void RecordStandaloneGuestDepthClear(GuestDepthResource depth)
{
var depthRange = new ImageSubresourceRange(
ImageAspectFlags.DepthBit,
0,
1,
0,
1);
var sourceStage = PipelineStageFlags.TopOfPipeBit;
var sourceAccess = AccessFlags.None;
switch (depth.Layout)
{
case ImageLayout.ShaderReadOnlyOptimal:
sourceStage =
PipelineStageFlags.VertexShaderBit |
PipelineStageFlags.FragmentShaderBit |
PipelineStageFlags.ComputeShaderBit;
sourceAccess = AccessFlags.ShaderReadBit;
break;
case ImageLayout.DepthStencilAttachmentOptimal:
sourceStage =
PipelineStageFlags.EarlyFragmentTestsBit |
PipelineStageFlags.LateFragmentTestsBit;
sourceAccess =
AccessFlags.DepthStencilAttachmentReadBit |
AccessFlags.DepthStencilAttachmentWriteBit;
break;
case ImageLayout.TransferDstOptimal:
sourceStage = PipelineStageFlags.TransferBit;
sourceAccess = AccessFlags.TransferWriteBit;
break;
}
if (depth.Layout != ImageLayout.TransferDstOptimal)
{
var toTransfer = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = sourceAccess,
DstAccessMask = AccessFlags.TransferWriteBit,
OldLayout = depth.Layout,
NewLayout = ImageLayout.TransferDstOptimal,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = depth.Image,
SubresourceRange = depthRange,
};
_vk.CmdPipelineBarrier(
_commandBuffer,
sourceStage,
PipelineStageFlags.TransferBit,
0,
0,
null,
0,
null,
1,
&toTransfer);
}
var clearValue = new ClearDepthStencilValue(depth.ClearDepth, 0);
_vk.CmdClearDepthStencilImage(
_commandBuffer,
depth.Image,
ImageLayout.TransferDstOptimal,
&clearValue,
1,
&depthRange);
depth.Initialized = true;
depth.Layout = ImageLayout.TransferDstOptimal;
depth.InitializationSource = "guest-depth-clear";
}
private void RecordRenderTargetFeedbackSnapshots(
TranslatedDrawResources resources,
PipelineStageFlags shaderStage)
@@ -15160,7 +14965,6 @@ internal static unsafe class VulkanVideoPresenter
DestroyGuestBufferAllocation(allocation);
}
_guestBufferAllocations.Clear();
PerfOverlay.SetGuestBufferCacheBytes(0);
_hostBufferPool.Dispose();
foreach (var guestImage in _guestImages.Values)
{
@@ -2350,8 +2350,7 @@ public static class Gen5ShaderScalarEvaluator
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes) &&
FallbackMemoryReader?.Invoke(address, bytes) != true)
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
@@ -1,99 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler;
using Xunit;
namespace SharpEmu.Libs.Tests.Agc;
public sealed class Gen5ScalarMemoryFallbackTests
{
private const ulong ScalarTableAddress = 0x4_4665_4FD0;
private static readonly object FallbackReaderGate = new();
[Fact]
public void ScalarLoadReadsTrackedFallbackMemory()
{
var expected = new uint[]
{
0x4665_4F70,
0x0000_0004,
0x4EA7_FCE0,
0x0000_0004,
};
var table = new byte[expected.Length * sizeof(uint)];
for (var index = 0; index < expected.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(
table.AsSpan(index * sizeof(uint), sizeof(uint)),
expected[index]);
}
var load = new Gen5ShaderInstruction(
0,
Gen5ShaderEncoding.Smem,
"SLoadDwordx4",
[],
[Gen5Operand.Scalar(0)],
[
Gen5Operand.Scalar(16),
Gen5Operand.Scalar(17),
Gen5Operand.Scalar(18),
Gen5Operand.Scalar(19),
],
new Gen5ScalarMemoryControl(4, 0, null));
var end = new Gen5ShaderInstruction(
8,
Gen5ShaderEncoding.Sopp,
"SEndpgm",
[],
[],
[],
null);
var state = new Gen5ShaderState(
new Gen5ShaderProgram(0, [load, end]),
[unchecked((uint)ScalarTableAddress), (uint)(ScalarTableAddress >> 32)],
null);
var ctx = new CpuContext(new FakeCpuMemory(0x1000, 0x100), Generation.Gen5);
lock (FallbackReaderGate)
{
var previousReader = Gen5ShaderScalarEvaluator.FallbackMemoryReader;
try
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader = ReadFallback;
Assert.True(
Gen5ShaderScalarEvaluator.TryEvaluate(
ctx,
state,
out var evaluation,
out var error),
error);
Assert.Equal(expected, evaluation.ScalarRegisters.Skip(16).Take(4));
}
finally
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader = previousReader;
}
}
bool ReadFallback(ulong address, Span<byte> destination)
{
if (address < ScalarTableAddress)
{
return false;
}
var offset = address - ScalarTableAddress;
if (offset + (ulong)destination.Length > (ulong)table.Length)
{
return false;
}
table.AsSpan((int)offset, destination.Length).CopyTo(destination);
return true;
}
}
}
@@ -1,26 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Native;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed unsafe class JitStubsTests
{
[Fact]
public void FindTlsAccessPatterns_IncludesLastValidOffset()
{
var pattern = JitStubs.TlsAccessPattern;
var code = new byte[pattern.Length + 3];
pattern.CopyTo(code.AsSpan(3));
fixed (byte* codePointer = code)
{
var matches = JitStubs.FindTlsAccessPatterns(codePointer, code.Length);
var match = Assert.Single(matches);
Assert.Equal((nint)(codePointer + 3), match);
}
}
}