mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-11 20:18:45 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d204afd10 | |||
| 7bd1e8dc29 | |||
| fad61e116e | |||
| bc297fa175 | |||
| 79e767ed9f | |||
| 1ea1396979 | |||
| a1dea499bc | |||
| fee6af3136 |
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -9,5 +9,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||||
|
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||||
|
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||||
|
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@ path = [
|
|||||||
"**/packages.lock.json",
|
"**/packages.lock.json",
|
||||||
"scripts/ps5_names.txt",
|
"scripts/ps5_names.txt",
|
||||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||||
"_logs/**"
|
"_logs/**",
|
||||||
|
".github/images/**"
|
||||||
]
|
]
|
||||||
precedence = "aggregate"
|
precedence = "aggregate"
|
||||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||||
|
|||||||
@@ -8,6 +8,62 @@
|
|||||||
"resolved": "10.0.3",
|
"resolved": "10.0.3",
|
||||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||||
},
|
},
|
||||||
|
"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.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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
},
|
||||||
"sharpemu.core": {
|
"sharpemu.core": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -23,7 +79,10 @@
|
|||||||
"sharpemu.libs": {
|
"sharpemu.libs": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )"
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sharpemu.logging": {
|
"sharpemu.logging": {
|
||||||
@@ -34,10 +93,57 @@
|
|||||||
"requested": "[1.21.0, )",
|
"requested": "[1.21.0, )",
|
||||||
"resolved": "1.21.0",
|
"resolved": "1.21.0",
|
||||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||||
|
},
|
||||||
|
"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.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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net10.0/linux-x64": {},
|
"net10.0/linux-x64": {
|
||||||
"net10.0/osx-arm64": {},
|
"Ultz.Native.GLFW": {
|
||||||
"net10.0/win-x64": {}
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/osx-arm64": {
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net10.0/win-x64": {
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,17 +8,105 @@
|
|||||||
"resolved": "1.21.0",
|
"resolved": "1.21.0",
|
||||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||||
},
|
},
|
||||||
|
"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.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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
},
|
||||||
"sharpemu.hle": {
|
"sharpemu.hle": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
},
|
},
|
||||||
"sharpemu.libs": {
|
"sharpemu.libs": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )"
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sharpemu.logging": {
|
"sharpemu.logging": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
|
},
|
||||||
|
"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.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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,21 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelEventQueueCompatExports
|
public static class KernelEventQueueCompatExports
|
||||||
{
|
{
|
||||||
|
private const int KernelEventSize = 0x20;
|
||||||
|
|
||||||
private static readonly object _eventQueueGate = new();
|
private static readonly object _eventQueueGate = new();
|
||||||
private static readonly HashSet<ulong> _eventQueues = new();
|
private static readonly HashSet<ulong> _eventQueues = new();
|
||||||
|
private static readonly Dictionary<ulong, Queue<KernelQueuedEvent>> _pendingEvents = new();
|
||||||
private static long _nextEventQueueHandle = 1;
|
private static long _nextEventQueueHandle = 1;
|
||||||
|
|
||||||
|
public readonly record struct KernelQueuedEvent(
|
||||||
|
ulong Ident,
|
||||||
|
short Filter,
|
||||||
|
ushort Flags,
|
||||||
|
uint Fflags,
|
||||||
|
ulong Data,
|
||||||
|
ulong UserData);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "D0OdFMjp46I",
|
Nid = "D0OdFMjp46I",
|
||||||
ExportName = "sceKernelCreateEqueue",
|
ExportName = "sceKernelCreateEqueue",
|
||||||
@@ -30,6 +41,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
_eventQueues.Add(handle);
|
_eventQueues.Add(handle);
|
||||||
|
_pendingEvents[handle] = new Queue<KernelQueuedEvent>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
@@ -52,6 +64,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
_eventQueues.Remove(handle);
|
_eventQueues.Remove(handle);
|
||||||
|
_pendingEvents.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceEventQueue(ctx, "delete", handle);
|
TraceEventQueue(ctx, "delete", handle);
|
||||||
@@ -165,23 +178,108 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelWaitEqueue(CpuContext ctx)
|
public static int KernelWaitEqueue(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var eventsAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var eventCapacity = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||||
var outCountAddress = ctx[CpuRegister.Rcx];
|
var outCountAddress = ctx[CpuRegister.Rcx];
|
||||||
var timeoutAddress = ctx[CpuRegister.R8];
|
var timeoutAddress = ctx[CpuRegister.R8];
|
||||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, 0))
|
|
||||||
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
|
if (deliveredCount > 0)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "wait-block", ctx[CpuRegister.Rdi]);
|
TraceEventQueue(ctx, "wait-deliver", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceEventQueue(ctx, "wait", ctx[CpuRegister.Rdi]);
|
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
|
||||||
|
{
|
||||||
|
TraceEventQueue(ctx, "wait-block", handle);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceEventQueue(ctx, "wait", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool IsValidEqueue(ulong handle)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
return _eventQueues.Contains(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
if (!_eventQueues.Contains(handle))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
|
{
|
||||||
|
queue = new Queue<KernelQueuedEvent>();
|
||||||
|
_pendingEvents[handle] = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
queue.Enqueue(queuedEvent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
|
||||||
|
{
|
||||||
|
if (eventsAddress == 0 || eventCapacity <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
KernelQueuedEvent[] events;
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = Math.Min(eventCapacity, queue.Count);
|
||||||
|
events = new KernelQueuedEvent[count];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
events[i] = queue.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < events.Length; i++)
|
||||||
|
{
|
||||||
|
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
||||||
|
{
|
||||||
|
Span<byte> eventBytes = stackalloc byte[KernelEventSize];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x00..], queuedEvent.Ident);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(eventBytes[0x08..], queuedEvent.Filter);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(eventBytes[0x0A..], queuedEvent.Flags);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(eventBytes[0x0C..], queuedEvent.Fflags);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x10..], queuedEvent.Data);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x18..], queuedEvent.UserData);
|
||||||
|
return ctx.Memory.TryWrite(address, eventBytes);
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -1444,6 +1444,51 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Oy6IpwgtYOk",
|
||||||
|
ExportName = "lseek",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixLseek(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var result = KernelLseekCore(
|
||||||
|
unchecked((int)ctx[CpuRegister.Rdi]),
|
||||||
|
unchecked((long)ctx[CpuRegister.Rsi]),
|
||||||
|
unchecked((int)ctx[CpuRegister.Rdx]),
|
||||||
|
out var position);
|
||||||
|
|
||||||
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = ulong.MaxValue;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)position);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "oib76F-12fk",
|
||||||
|
ExportName = "sceKernelLseek",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelLseek(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var result = KernelLseekCore(
|
||||||
|
unchecked((int)ctx[CpuRegister.Rdi]),
|
||||||
|
unchecked((long)ctx[CpuRegister.Rsi]),
|
||||||
|
unchecked((int)ctx[CpuRegister.Rdx]),
|
||||||
|
out var position);
|
||||||
|
|
||||||
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)position);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "taRWhTJFTgE",
|
Nid = "taRWhTJFTgE",
|
||||||
ExportName = "sceKernelGetdirentries",
|
ExportName = "sceKernelGetdirentries",
|
||||||
@@ -1474,6 +1519,58 @@ public static class KernelMemoryCompatExports
|
|||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static OrbisGen2Result KernelLseekCore(int fd, long offset, int whence, out long position)
|
||||||
|
{
|
||||||
|
position = -1;
|
||||||
|
|
||||||
|
FileStream? stream;
|
||||||
|
lock (_fdGate)
|
||||||
|
{
|
||||||
|
_openFiles.TryGetValue(fd, out stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream is null)
|
||||||
|
{
|
||||||
|
LogIoTrace("lseek", $"fd:{fd}", $"offset={offset} whence={whence} result=badfd");
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeekOrigin origin;
|
||||||
|
switch (whence)
|
||||||
|
{
|
||||||
|
case SeekSet:
|
||||||
|
origin = SeekOrigin.Begin;
|
||||||
|
break;
|
||||||
|
case SeekCur:
|
||||||
|
origin = SeekOrigin.Current;
|
||||||
|
break;
|
||||||
|
case SeekEnd:
|
||||||
|
origin = SeekOrigin.End;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=invalid_whence");
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
position = stream.Seek(offset, origin);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=io_error ex={ex.Message}");
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} result=invalid ex={ex.Message}");
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogIoTrace("lseek", stream.Name, $"fd={fd} offset={offset} whence={whence} pos={position}");
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "FxVZqBAA7ks",
|
Nid = "FxVZqBAA7ks",
|
||||||
ExportName = "_write",
|
ExportName = "_write",
|
||||||
@@ -3567,13 +3664,13 @@ public static class KernelMemoryCompatExports
|
|||||||
var devlogAppRoot = ResolveDevlogAppRoot();
|
var devlogAppRoot = ResolveDevlogAppRoot();
|
||||||
if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase))
|
if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var relative = guestPath["/devlog/app/".Length..].Replace('/', Path.DirectorySeparatorChar);
|
var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]);
|
||||||
return Path.Combine(devlogAppRoot, relative);
|
return Path.Combine(devlogAppRoot, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase))
|
if (guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var relative = guestPath["devlog/app/".Length..].Replace('/', Path.DirectorySeparatorChar);
|
var relative = NormalizeMountRelativePath(guestPath["devlog/app/".Length..]);
|
||||||
return Path.Combine(devlogAppRoot, relative);
|
return Path.Combine(devlogAppRoot, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3586,7 +3683,7 @@ public static class KernelMemoryCompatExports
|
|||||||
var temp0Root = ResolveTemp0Root();
|
var temp0Root = ResolveTemp0Root();
|
||||||
if (guestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase))
|
if (guestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var relative = guestPath["/temp0/".Length..].Replace('/', Path.DirectorySeparatorChar);
|
var relative = NormalizeMountRelativePath(guestPath["/temp0/".Length..]);
|
||||||
return Path.Combine(temp0Root, relative);
|
return Path.Combine(temp0Root, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3606,13 +3703,13 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
if (guestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase))
|
if (guestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var relative = guestPath["/app0/".Length..].Replace('/', Path.DirectorySeparatorChar);
|
var relative = NormalizeMountRelativePath(guestPath["/app0/".Length..]);
|
||||||
return Path.Combine(app0Root, relative);
|
return Path.Combine(app0Root, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (guestPath.StartsWith("app0/", StringComparison.OrdinalIgnoreCase))
|
if (guestPath.StartsWith("app0/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var relative = guestPath["app0/".Length..].Replace('/', Path.DirectorySeparatorChar);
|
var relative = NormalizeMountRelativePath(guestPath["app0/".Length..]);
|
||||||
return Path.Combine(app0Root, relative);
|
return Path.Combine(app0Root, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3628,6 +3725,14 @@ public static class KernelMemoryCompatExports
|
|||||||
return guestPath;
|
return guestPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeMountRelativePath(string relativePath)
|
||||||
|
{
|
||||||
|
return relativePath
|
||||||
|
.TrimStart('/', '\\')
|
||||||
|
.Replace('/', Path.DirectorySeparatorChar)
|
||||||
|
.Replace('\\', Path.DirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveDevlogAppRoot()
|
private static string ResolveDevlogAppRoot()
|
||||||
{
|
{
|
||||||
var configuredRoot = Environment.GetEnvironmentVariable("SHARPEMU_DEVLOG_APP_DIR");
|
var configuredRoot = Environment.GetEnvironmentVariable("SHARPEMU_DEVLOG_APP_DIR");
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
public static class KernelSemaphoreCompatExports
|
||||||
|
{
|
||||||
|
private const int MaxSemaphoreNameLength = 128;
|
||||||
|
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
||||||
|
private static int _nextSemaphoreHandle = 1;
|
||||||
|
|
||||||
|
private sealed class KernelSemaphoreState
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required int InitialCount { get; init; }
|
||||||
|
public required int MaxCount { get; init; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
public int WaitingThreads { get; set; }
|
||||||
|
public object Gate { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "188x57JYp0g",
|
||||||
|
ExportName = "sceKernelCreateSema",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelCreateSema(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var attr = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var initialCount = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||||
|
var maxCount = unchecked((int)ctx[CpuRegister.R8]);
|
||||||
|
var optionAddress = ctx[CpuRegister.R9];
|
||||||
|
|
||||||
|
if (semaphoreAddress == 0 ||
|
||||||
|
nameAddress == 0 ||
|
||||||
|
attr > 2 ||
|
||||||
|
initialCount < 0 ||
|
||||||
|
maxCount <= 0 ||
|
||||||
|
initialCount > maxCount ||
|
||||||
|
optionAddress != 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
|
if (handle == 0)
|
||||||
|
{
|
||||||
|
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
|
}
|
||||||
|
|
||||||
|
_semaphores[handle] = new KernelSemaphoreState
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
InitialCount = initialCount,
|
||||||
|
MaxCount = maxCount,
|
||||||
|
Count = initialCount,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||||
|
{
|
||||||
|
_semaphores.TryRemove(handle, out _);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Zxa0VhQVTsk",
|
||||||
|
ExportName = "sceKernelWaitSema",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelWaitSema(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var timeoutAddress = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
if (semaphore.Count >= needCount)
|
||||||
|
{
|
||||||
|
semaphore.Count -= needCount;
|
||||||
|
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutAddress != 0)
|
||||||
|
{
|
||||||
|
if (!TryReadUInt32(ctx, timeoutAddress, out _))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitSema"))
|
||||||
|
{
|
||||||
|
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphore.WaitingThreads++;
|
||||||
|
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "12wOHk8ywb0",
|
||||||
|
ExportName = "sceKernelPollSema",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelPollSema(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
if (semaphore.Count < needCount)
|
||||||
|
{
|
||||||
|
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphore.Count -= needCount;
|
||||||
|
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4czppHBiriw",
|
||||||
|
ExportName = "sceKernelSignalSema",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelSignalSema(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var signalCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signalCount <= 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphore.Count += signalCount;
|
||||||
|
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4DM06U2BNEY",
|
||||||
|
ExportName = "sceKernelCancelSema",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelCancelSema(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var setCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var waitingThreadsAddress = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setCount > semaphore.MaxCount)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||||
|
semaphore.WaitingThreads = 0;
|
||||||
|
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "R1Jvn8bSCW8",
|
||||||
|
ExportName = "sceKernelDeleteSema",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelDeleteSema(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_semaphores.TryRemove(handle, out var semaphore))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (address == 0 || maxLength <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = new byte[Math.Min(maxLength, 4096)];
|
||||||
|
for (var i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)i, current))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[i] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceSemaphore(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
public static class PadExports
|
||||||
|
{
|
||||||
|
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
|
||||||
|
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||||
|
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||||
|
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||||
|
private const int PrimaryUserId = 1;
|
||||||
|
private const int StandardPortType = 0;
|
||||||
|
private const int PrimaryPadHandle = 1;
|
||||||
|
private const int ControllerInformationSize = 0x1C;
|
||||||
|
private const int PadDataSize = 0x78;
|
||||||
|
|
||||||
|
private static bool _initialized;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hv1luiJrqQM",
|
||||||
|
ExportName = "scePadInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_initialized = true;
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xk0AcarP3V4",
|
||||||
|
ExportName = "scePadOpen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var index = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rcx];
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisPadErrorNotInitialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId == -1)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, PrimaryPadHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "clVvL4ZDntw",
|
||||||
|
ExportName = "scePadSetMotionSensorState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return handle == PrimaryPadHandle
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gjP9-KQzoUk",
|
||||||
|
ExportName = "scePadGetControllerInformation",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadGetControllerInformation(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var informationAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (informationAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> information = stackalloc byte[ControllerInformationSize];
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
|
||||||
|
information[0x08] = 30;
|
||||||
|
information[0x09] = 30;
|
||||||
|
information[0x0A] = StandardPortType;
|
||||||
|
information[0x0B] = 1;
|
||||||
|
information[0x0C] = 1;
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(informationAddress, information)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "YndgXqQVV7c",
|
||||||
|
ExportName = "scePadReadState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadReadState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> data = stackalloc byte[PadDataSize];
|
||||||
|
data.Clear();
|
||||||
|
data[0x04] = 128;
|
||||||
|
data[0x05] = 128;
|
||||||
|
data[0x06] = 128;
|
||||||
|
data[0x07] = 128;
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||||
|
data[0x4C] = 1;
|
||||||
|
var timestampTicks = Stopwatch.GetTimestamp();
|
||||||
|
var timestampMicroseconds =
|
||||||
|
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
|
||||||
|
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||||
|
data[0x50..],
|
||||||
|
timestampMicroseconds);
|
||||||
|
data[0x68] = 1;
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(dataAddress, data)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||||
|
<PackageReference Include="Silk.NET.Windowing" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.SystemService;
|
||||||
|
|
||||||
|
public static class SystemServiceExports
|
||||||
|
{
|
||||||
|
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
||||||
|
private const int SystemServiceStatusSize = 0x0C;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rPo6tV8D9bM",
|
||||||
|
ExportName = "sceSystemServiceGetStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceGetStatus(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var statusAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (statusAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
|
||||||
|
status.Clear();
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(status, 0);
|
||||||
|
status[0x06] = 1;
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(statusAddress, status)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.UserService;
|
||||||
|
|
||||||
|
public static class UserServiceExports
|
||||||
|
{
|
||||||
|
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005);
|
||||||
|
private const int PrimaryUserId = 1;
|
||||||
|
private const int InvalidUserId = -1;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "j3YMu1MVNNo",
|
||||||
|
ExportName = "sceUserServiceInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CdWp0oHWGr0",
|
||||||
|
ExportName = "sceUserServiceGetInitialUser",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetInitialUser(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userIdAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (userIdAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fPhymKNvK-A",
|
||||||
|
ExportName = "sceUserServiceGetLoginUserIdList",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetLoginUserIdList(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userIdListAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (userIdListAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x00..], PrimaryUserId);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x04..], InvalidUserId);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
||||||
|
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -15,28 +16,79 @@ public static class VideoOutExports
|
|||||||
private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009);
|
private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009);
|
||||||
private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A);
|
private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A);
|
||||||
private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B);
|
private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B);
|
||||||
|
private const int OrbisVideoOutErrorInvalidEventQueue = unchecked((int)0x8029000C);
|
||||||
|
private const int OrbisVideoOutErrorInvalidEvent = unchecked((int)0x8029000D);
|
||||||
private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A);
|
private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A);
|
||||||
private const int SceVideoOutBusTypeMain = 0;
|
private const int SceVideoOutBusTypeMain = 0;
|
||||||
private const int SceVideoOutBufferAttributeOptionNone = 0;
|
private const int SceVideoOutBufferAttributeOptionNone = 0;
|
||||||
|
private const int SceVideoOutTilingModeLinear = 1;
|
||||||
private const int MaxOpenPorts = 4;
|
private const int MaxOpenPorts = 4;
|
||||||
private const int MaxDisplayBuffers = 16;
|
private const int MaxDisplayBuffers = 16;
|
||||||
private const int VideoOutBufferAttributeSize = 0x24;
|
private const int MaxDisplayBufferGroups = 4;
|
||||||
|
private const int MaxFrameDumps = 8;
|
||||||
|
private const int VideoOutBufferAttributeSize = 0x28;
|
||||||
private const int VideoOutBufferAttribute2Size = 0x50;
|
private const int VideoOutBufferAttribute2Size = 0x50;
|
||||||
private const int VideoOutBuffersEntrySize = 0x20;
|
private const int VideoOutBuffersEntrySize = 0x20;
|
||||||
|
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
||||||
|
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
||||||
|
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
|
||||||
|
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
|
||||||
|
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
|
||||||
|
private const ulong SceVideoOutInternalEventFlip = 0x6;
|
||||||
|
private const short OrbisKernelEventFilterVideoOut = -13;
|
||||||
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
|
private static readonly object _frameDumpGate = new();
|
||||||
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
||||||
|
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
|
||||||
private static int _nextHandle = 1;
|
private static int _nextHandle = 1;
|
||||||
|
private static int _frameDumpCount;
|
||||||
|
private static long _nextFrameDumpIndex;
|
||||||
|
|
||||||
private sealed class VideoOutPortState
|
private sealed class VideoOutPortState
|
||||||
{
|
{
|
||||||
public required int Handle { get; init; }
|
public required int Handle { get; init; }
|
||||||
public int FlipRate { get; set; }
|
public int FlipRate { get; set; }
|
||||||
public ulong VblankCount { get; set; }
|
public ulong VblankCount { get; set; }
|
||||||
public int NextSetId { get; set; } = 1;
|
public ulong FlipCount { get; set; }
|
||||||
public HashSet<int> RegisteredSetIds { get; } = new();
|
public int CurrentBuffer { get; set; } = -1;
|
||||||
|
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
|
||||||
|
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
||||||
|
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class VideoOutBufferGroup
|
||||||
|
{
|
||||||
|
public required int Index { get; init; }
|
||||||
|
public required BufferAttribute Attribute { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class VideoOutBufferSlot
|
||||||
|
{
|
||||||
|
public int GroupIndex { get; set; } = -1;
|
||||||
|
public ulong AddressLeft { get; set; }
|
||||||
|
public ulong AddressRight { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct FlipEventRegistration(ulong Equeue, ulong UserData);
|
||||||
|
|
||||||
|
private readonly record struct BufferAttribute(
|
||||||
|
ulong PixelFormat,
|
||||||
|
uint TilingMode,
|
||||||
|
uint AspectRatio,
|
||||||
|
uint Width,
|
||||||
|
uint Height,
|
||||||
|
uint PitchInPixel,
|
||||||
|
ulong Option);
|
||||||
|
|
||||||
|
internal readonly record struct DisplayBufferInfo(
|
||||||
|
ulong Address,
|
||||||
|
ulong PixelFormat,
|
||||||
|
uint TilingMode,
|
||||||
|
uint Width,
|
||||||
|
uint Height,
|
||||||
|
uint PitchInPixel);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Up36PTk687E",
|
Nid = "Up36PTk687E",
|
||||||
ExportName = "sceVideoOutOpen",
|
ExportName = "sceVideoOutOpen",
|
||||||
@@ -136,6 +188,165 @@ public static class VideoOutExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HXzjK9yI30k",
|
||||||
|
ExportName = "sceVideoOutAddFlipEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutAddFlipEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var equeue = ctx[CpuRegister.Rdi];
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var userData = ctx[CpuRegister.Rdx];
|
||||||
|
if (!TryGetPort(handle, out var port))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidEventQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
port.FlipEvents.Add(new FlipEventRegistration(equeue, userData));
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "U46NwOiJpys",
|
||||||
|
ExportName = "sceVideoOutSubmitFlip",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutSubmitFlip(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var bufferIndex = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var flipMode = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
var flipArg = unchecked((long)ctx[CpuRegister.Rcx]);
|
||||||
|
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "U2JJtSqNKZI",
|
||||||
|
ExportName = "sceVideoOutGetEventId",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutGetEventId(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var eventAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (eventAddress == 0)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(eventAddress, out var ident) ||
|
||||||
|
!TryReadInt16(ctx, eventAddress + 0x08, out var filter))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rWUTcKdkUzQ",
|
||||||
|
ExportName = "sceVideoOutGetEventData",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutGetEventData(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var eventAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (eventAddress == 0 || dataAddress == 0)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(eventAddress, out var ident) ||
|
||||||
|
!TryReadInt16(ctx, eventAddress + 0x08, out var filter) ||
|
||||||
|
!ctx.TryReadUInt64(eventAddress + 0x10, out var data))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.TryWriteUInt64(dataAddress, data >> 16)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int SubmitFlipFromAgc(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) =>
|
||||||
|
SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
||||||
|
|
||||||
|
internal static void SubmitHostRgbaFrame(ReadOnlySpan<byte> rgbaFrame, uint width, uint height)
|
||||||
|
{
|
||||||
|
if (rgbaFrame.Length != checked((int)(width * height * 4)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bgraFrame = new byte[rgbaFrame.Length];
|
||||||
|
for (var offset = 0; offset < rgbaFrame.Length; offset += 4)
|
||||||
|
{
|
||||||
|
bgraFrame[offset + 0] = rgbaFrame[offset + 2];
|
||||||
|
bgraFrame[offset + 1] = rgbaFrame[offset + 1];
|
||||||
|
bgraFrame[offset + 2] = rgbaFrame[offset + 0];
|
||||||
|
bgraFrame[offset + 3] = rgbaFrame[offset + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
VulkanVideoPresenter.Submit(bgraFrame, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info)
|
||||||
|
{
|
||||||
|
info = default;
|
||||||
|
if (bufferIndex < 0 || bufferIndex >= MaxDisplayBuffers)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_ports.TryGetValue(handle, out var port))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var slot = port.BufferSlots[bufferIndex];
|
||||||
|
if (slot.AddressLeft == 0 ||
|
||||||
|
slot.GroupIndex < 0 ||
|
||||||
|
slot.GroupIndex >= port.Groups.Length ||
|
||||||
|
port.Groups[slot.GroupIndex] is not { } group)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = group.Attribute;
|
||||||
|
info = new DisplayBufferInfo(
|
||||||
|
slot.AddressLeft,
|
||||||
|
attribute.PixelFormat,
|
||||||
|
attribute.TilingMode,
|
||||||
|
attribute.Width,
|
||||||
|
attribute.Height,
|
||||||
|
attribute.PitchInPixel);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "MTxxrOCeSig",
|
Nid = "MTxxrOCeSig",
|
||||||
ExportName = "sceVideoOutSetWindowModeMargins",
|
ExportName = "sceVideoOutSetWindowModeMargins",
|
||||||
@@ -173,9 +384,23 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
return port.RegisteredSetIds.Remove(attributeIndex)
|
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
{
|
||||||
: OrbisVideoOutErrorInvalidValue;
|
return OrbisVideoOutErrorInvalidValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
port.Groups[attributeIndex] = null;
|
||||||
|
foreach (var slot in port.BufferSlots)
|
||||||
|
{
|
||||||
|
if (slot.GroupIndex == attributeIndex)
|
||||||
|
{
|
||||||
|
slot.GroupIndex = -1;
|
||||||
|
slot.AddressLeft = 0;
|
||||||
|
slot.AddressRight = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,16 +517,21 @@ public static class VideoOutExports
|
|||||||
return OrbisVideoOutErrorInvalidValue;
|
return OrbisVideoOutErrorInvalidValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TryReadBufferAttribute(ctx, attributeAddress, false, out var attribute))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<ulong> addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)];
|
||||||
for (var i = 0; i < bufferNum; i++)
|
for (var i = 0; i < bufferNum; i++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out _))
|
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out addresses[i]))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var setId = AllocateBufferSet(port);
|
return RegisterBufferRange(port, startIndex, addresses[..bufferNum], attribute);
|
||||||
return setId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -348,34 +578,464 @@ public static class VideoOutExports
|
|||||||
return OrbisVideoOutErrorInvalidValue;
|
return OrbisVideoOutErrorInvalidValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<ulong> addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)];
|
||||||
for (var i = 0; i < bufferNum; i++)
|
for (var i = 0; i < bufferNum; i++)
|
||||||
{
|
{
|
||||||
var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize);
|
var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize);
|
||||||
if (!ctx.TryReadUInt64(entryAddress + 0x00, out _) ||
|
if (!ctx.TryReadUInt64(entryAddress + 0x00, out addresses[i]) ||
|
||||||
!ctx.TryReadUInt64(entryAddress + 0x08, out _))
|
!ctx.TryReadUInt64(entryAddress + 0x08, out _))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_stateGate)
|
var groupIndex = RegisterBufferRange(port, bufferIndexStart, addresses[..bufferNum], attribute, setIndex);
|
||||||
{
|
return groupIndex < 0 ? groupIndex : setIndex;
|
||||||
port.RegisteredSetIds.Add(setIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return setIndex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int AllocateBufferSet(VideoOutPortState port)
|
private static int SubmitFlip(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg)
|
||||||
|
{
|
||||||
|
if (!TryGetPort(handle, out var port))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferIndex < -1 || bufferIndex >= MaxDisplayBuffers)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong eventData;
|
||||||
|
List<FlipEventRegistration> flipEvents;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
port.CurrentBuffer = bufferIndex;
|
||||||
|
port.FlipCount++;
|
||||||
|
var eventCount = Math.Min(port.FlipCount, 0xFUL);
|
||||||
|
var timeBits = (ulong)Environment.TickCount64 & 0xFFFUL;
|
||||||
|
eventData = timeBits | (eventCount << 12) | ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||||
|
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var flipEvent in flipEvents)
|
||||||
|
{
|
||||||
|
_ = KernelEventQueueCompatExports.EnqueueEvent(
|
||||||
|
flipEvent.Equeue,
|
||||||
|
new KernelEventQueueCompatExports.KernelQueuedEvent(
|
||||||
|
SceVideoOutInternalEventFlip,
|
||||||
|
OrbisKernelEventFilterVideoOut,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
eventData,
|
||||||
|
flipEvent.UserData));
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
var setId = port.NextSetId++;
|
var groupIndex = requestedGroupIndex >= 0 ? requestedGroupIndex : FindFreeGroupIndex(port);
|
||||||
port.RegisteredSetIds.Add(setId);
|
if (groupIndex < 0 || groupIndex >= MaxDisplayBufferGroups)
|
||||||
return setId;
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port.Groups[groupIndex] is not null)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorResourceBusy;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < addresses.Length; i++)
|
||||||
|
{
|
||||||
|
if (port.BufferSlots[startIndex + i].GroupIndex >= 0)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorResourceBusy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
port.Groups[groupIndex] = new VideoOutBufferGroup
|
||||||
|
{
|
||||||
|
Index = groupIndex,
|
||||||
|
Attribute = attribute,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < addresses.Length; i++)
|
||||||
|
{
|
||||||
|
var slot = port.BufferSlots[startIndex + i];
|
||||||
|
slot.GroupIndex = groupIndex;
|
||||||
|
slot.AddressLeft = addresses[i];
|
||||||
|
slot.AddressRight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceVideoOut(
|
||||||
|
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||||
|
return groupIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int FindFreeGroupIndex(VideoOutPortState port)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < port.Groups.Length; i++)
|
||||||
|
{
|
||||||
|
if (port.Groups[i] is null)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
|
||||||
|
{
|
||||||
|
attribute = default;
|
||||||
|
if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) ||
|
||||||
|
!TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) ||
|
||||||
|
!TryReadUInt32(ctx, attributeAddress + 0x10, out var height))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribute2)
|
||||||
|
{
|
||||||
|
if (!ctx.TryReadUInt64(attributeAddress + 0x18, out var option) ||
|
||||||
|
!ctx.TryReadUInt64(attributeAddress + 0x20, out var pixelFormat))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat), tilingMode, 0, width, height, width, option);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) ||
|
||||||
|
!TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) ||
|
||||||
|
!TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) ||
|
||||||
|
!TryReadUInt32(ctx, attributeAddress + 0x18, out var option32))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat32), tilingMode, aspectRatio, width, height, pitchInPixel, option32);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryDumpFrame(CpuContext ctx, VideoOutPortState port, int bufferIndex, int flipMode, long flipArg)
|
||||||
|
{
|
||||||
|
if (bufferIndex < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoOutBufferSlot slot;
|
||||||
|
VideoOutBufferGroup? group;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
slot = port.BufferSlots[bufferIndex];
|
||||||
|
group = slot.GroupIndex >= 0 && slot.GroupIndex < port.Groups.Length
|
||||||
|
? port.Groups[slot.GroupIndex]
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group is null || slot.AddressLeft == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var attribute = group.Attribute;
|
||||||
|
if (attribute.Width == 0 || attribute.Height == 0 || attribute.Width > 8192 || attribute.Height > 8192)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytesPerPixel = GetBytesPerPixel(attribute.PixelFormat);
|
||||||
|
if (bytesPerPixel == 0)
|
||||||
|
{
|
||||||
|
return DumpRawFrame(ctx, port.Handle, slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "unsupported-format");
|
||||||
|
}
|
||||||
|
|
||||||
|
var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel;
|
||||||
|
var rowBytes = checked((int)(pitch * bytesPerPixel));
|
||||||
|
var visibleRowBytes = checked((int)(attribute.Width * bytesPerPixel));
|
||||||
|
var frameBytes = checked((ulong)rowBytes * attribute.Height);
|
||||||
|
if (frameBytes > 256UL * 1024UL * 1024UL)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_frameDumpGate)
|
||||||
|
{
|
||||||
|
if (_frameDumpCount >= MaxFrameDumps)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ulong fnvOffsetBasis = 14695981039346656037UL;
|
||||||
|
const ulong fnvPrime = 1099511628211UL;
|
||||||
|
var fingerprint = fnvOffsetBasis;
|
||||||
|
var row = new byte[rowBytes];
|
||||||
|
for (uint y = 0; y < attribute.Height; y++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var value in row.AsSpan(0, visibleRowBytes))
|
||||||
|
{
|
||||||
|
fingerprint = (fingerprint ^ value) * fnvPrime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fingerprintKey = (port.Handle, bufferIndex, slot.AddressLeft);
|
||||||
|
lock (_frameDumpGate)
|
||||||
|
{
|
||||||
|
if (_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) &&
|
||||||
|
previousFingerprint == fingerprint)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_frameDumpCount >= MaxFrameDumps)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastFrameFingerprints[fingerprintKey] = fingerprint;
|
||||||
|
_frameDumpCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rgb = new byte[checked((int)(attribute.Width * attribute.Height * 3))];
|
||||||
|
var rgbOffset = 0;
|
||||||
|
for (uint y = 0; y < attribute.Height; y++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConvertRowToRgb(row.AsSpan(0, visibleRowBytes), rgb.AsSpan(rgbOffset, (int)attribute.Width * 3), attribute.PixelFormat);
|
||||||
|
rgbOffset += (int)attribute.Width * 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex);
|
||||||
|
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
||||||
|
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
||||||
|
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
|
||||||
|
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool DumpRawFrame(CpuContext ctx, int handle, ulong address, BufferAttribute attribute, int bufferIndex, int flipMode, long flipArg, string reason)
|
||||||
|
{
|
||||||
|
var bytesPerPixel = Math.Max(GetBytesPerPixel(attribute.PixelFormat), 4u);
|
||||||
|
var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel;
|
||||||
|
var byteCount = checked((ulong)pitch * attribute.Height * bytesPerPixel);
|
||||||
|
if (byteCount == 0 || byteCount > 256UL * 1024UL * 1024UL)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = new byte[(int)byteCount];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fingerprint = ComputeFingerprint(bytes);
|
||||||
|
var fingerprintKey = (handle, bufferIndex, address);
|
||||||
|
lock (_frameDumpGate)
|
||||||
|
{
|
||||||
|
if ((_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) &&
|
||||||
|
previousFingerprint == fingerprint) ||
|
||||||
|
_frameDumpCount >= MaxFrameDumps)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastFrameFingerprints[fingerprintKey] = fingerprint;
|
||||||
|
_frameDumpCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex);
|
||||||
|
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
||||||
|
File.WriteAllBytes(basePath + ".raw", bytes);
|
||||||
|
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
|
||||||
|
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong ComputeFingerprint(ReadOnlySpan<byte> bytes)
|
||||||
|
{
|
||||||
|
const ulong fnvOffsetBasis = 14695981039346656037UL;
|
||||||
|
const ulong fnvPrime = 1099511628211UL;
|
||||||
|
var fingerprint = fnvOffsetBasis;
|
||||||
|
foreach (var value in bytes)
|
||||||
|
{
|
||||||
|
fingerprint = (fingerprint ^ value) * fnvPrime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint GetBytesPerPixel(ulong pixelFormat) =>
|
||||||
|
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
|
||||||
|
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
||||||
|
SceVideoOutPixelFormatA2R10G10B10 or
|
||||||
|
SceVideoOutPixelFormatA2R10G10B10Srgb or
|
||||||
|
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
|
||||||
|
? 4u
|
||||||
|
: 0u;
|
||||||
|
|
||||||
|
private static ulong NormalizePixelFormat(ulong pixelFormat)
|
||||||
|
{
|
||||||
|
if (GetBytesPerPixel(pixelFormat) != 0)
|
||||||
|
{
|
||||||
|
return pixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
var low = (uint)(pixelFormat & 0xFFFF_FFFFUL);
|
||||||
|
if (GetBytesPerPixel(low) != 0)
|
||||||
|
{
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
|
||||||
|
var high = (uint)(pixelFormat >> 32);
|
||||||
|
if (GetBytesPerPixel(high) != 0)
|
||||||
|
{
|
||||||
|
return high;
|
||||||
|
}
|
||||||
|
|
||||||
|
var packed = high | (low >> 16);
|
||||||
|
return GetBytesPerPixel(packed) != 0 ? packed : pixelFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConvertRowToRgb(ReadOnlySpan<byte> source, Span<byte> destination, ulong pixelFormat)
|
||||||
|
{
|
||||||
|
var dst = 0;
|
||||||
|
for (var src = 0; src + 3 < source.Length; src += 4)
|
||||||
|
{
|
||||||
|
if (pixelFormat == SceVideoOutPixelFormatA8B8G8R8Srgb)
|
||||||
|
{
|
||||||
|
destination[dst++] = source[src + 0];
|
||||||
|
destination[dst++] = source[src + 1];
|
||||||
|
destination[dst++] = source[src + 2];
|
||||||
|
}
|
||||||
|
else if (pixelFormat is SceVideoOutPixelFormatA2R10G10B10 or SceVideoOutPixelFormatA2R10G10B10Srgb or SceVideoOutPixelFormatA2R10G10B10Bt2020Pq)
|
||||||
|
{
|
||||||
|
var value = BinaryPrimitives.ReadUInt32LittleEndian(source[src..(src + 4)]);
|
||||||
|
destination[dst++] = (byte)(((value >> 20) & 0x3FF) >> 2);
|
||||||
|
destination[dst++] = (byte)(((value >> 10) & 0x3FF) >> 2);
|
||||||
|
destination[dst++] = (byte)((value & 0x3FF) >> 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destination[dst++] = source[src + 2];
|
||||||
|
destination[dst++] = source[src + 1];
|
||||||
|
destination[dst++] = source[src + 0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFrameDumpBasePath(long frameIndex, int handle, int bufferIndex)
|
||||||
|
{
|
||||||
|
var directory = GetLogsDirectory();
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
return Path.Combine(directory, $"videoout_frame_{frameIndex:D4}_h{handle}_b{bufferIndex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetLogsDirectory()
|
||||||
|
{
|
||||||
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (current is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(current.FullName, "SharpEmu.slnx")))
|
||||||
|
{
|
||||||
|
return Path.Combine(current.FullName, "logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteBmp(string path, uint width, uint height, byte[] rgb)
|
||||||
|
{
|
||||||
|
var rowStride = checked((int)(((width * 3u) + 3u) & ~3u));
|
||||||
|
var pixelBytes = checked(rowStride * (int)height);
|
||||||
|
var fileSize = 54 + pixelBytes;
|
||||||
|
using var stream = File.Create(path);
|
||||||
|
Span<byte> header = stackalloc byte[54];
|
||||||
|
header[0] = (byte)'B';
|
||||||
|
header[1] = (byte)'M';
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[0x02..], (uint)fileSize);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[0x0A..], 54);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[0x0E..], 40);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(header[0x12..], (int)width);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(header[0x16..], -(int)height);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(header[0x1A..], 1);
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(header[0x1C..], 24);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(header[0x22..], (uint)pixelBytes);
|
||||||
|
stream.Write(header);
|
||||||
|
|
||||||
|
var row = new byte[rowStride];
|
||||||
|
var sourceStride = (int)width * 3;
|
||||||
|
var heightInt = (int)height;
|
||||||
|
var widthInt = (int)width;
|
||||||
|
for (var y = 0; y < heightInt; y++)
|
||||||
|
{
|
||||||
|
row.AsSpan().Clear();
|
||||||
|
var src = rgb.AsSpan(y * sourceStride, sourceStride);
|
||||||
|
for (var x = 0; x < widthInt; x++)
|
||||||
|
{
|
||||||
|
row[(x * 3) + 0] = src[(x * 3) + 2];
|
||||||
|
row[(x * 3) + 1] = src[(x * 3) + 1];
|
||||||
|
row[(x * 3) + 2] = src[(x * 3) + 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Write(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteFrameMetadata(
|
||||||
|
string path,
|
||||||
|
ulong address,
|
||||||
|
BufferAttribute attribute,
|
||||||
|
int bufferIndex,
|
||||||
|
int flipMode,
|
||||||
|
long flipArg,
|
||||||
|
string kind,
|
||||||
|
ulong fingerprint)
|
||||||
|
{
|
||||||
|
File.WriteAllText(
|
||||||
|
path,
|
||||||
|
$"kind={kind}\naddress=0x{address:X16}\nbuffer_index={bufferIndex}\nflip_mode={flipMode}\nflip_arg={flipArg}\nfingerprint=0x{fingerprint:X16}\npixel_format=0x{attribute.PixelFormat:X}\ntiling_mode={attribute.TilingMode}\nwidth={attribute.Width}\nheight={attribute.Height}\npitch_in_pixel={attribute.PitchInPixel}\noption=0x{attribute.Option:X}\n");
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsValidBufferRange(int startIndex, int bufferNum)
|
private static bool IsValidBufferRange(int startIndex, int bufferNum)
|
||||||
{
|
{
|
||||||
return startIndex >= 0 &&
|
return startIndex >= 0 &&
|
||||||
@@ -393,6 +1053,17 @@ public static class VideoOutExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static VideoOutBufferSlot[] CreateBufferSlots()
|
||||||
|
{
|
||||||
|
var slots = new VideoOutBufferSlot[MaxDisplayBuffers];
|
||||||
|
for (var i = 0; i < slots.Length; i++)
|
||||||
|
{
|
||||||
|
slots[i] = new VideoOutBufferSlot();
|
||||||
|
}
|
||||||
|
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value)
|
private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value)
|
||||||
{
|
{
|
||||||
var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08);
|
var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08);
|
||||||
@@ -406,4 +1077,40 @@ public static class VideoOutExports
|
|||||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadInt16(CpuContext ctx, ulong address, out short value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(short)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadInt16LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceVideoOut(string message)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,702 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using Silk.NET.Core;
|
||||||
|
using Silk.NET.Core.Native;
|
||||||
|
using Silk.NET.Maths;
|
||||||
|
using Silk.NET.Vulkan;
|
||||||
|
using Silk.NET.Vulkan.Extensions.KHR;
|
||||||
|
using Silk.NET.Windowing;
|
||||||
|
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||||
|
using VkSemaphore = Silk.NET.Vulkan.Semaphore;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.VideoOut;
|
||||||
|
|
||||||
|
internal static unsafe class VulkanVideoPresenter
|
||||||
|
{
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
private static Thread? _thread;
|
||||||
|
private static byte[]? _latestFrame;
|
||||||
|
private static uint _latestWidth;
|
||||||
|
private static uint _latestHeight;
|
||||||
|
private static long _latestSequence;
|
||||||
|
private static bool _closed;
|
||||||
|
|
||||||
|
public static void Submit(byte[] bgraFrame, uint width, uint height)
|
||||||
|
{
|
||||||
|
if (bgraFrame.Length != checked((int)(width * height * 4)))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_closed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_latestFrame = bgraFrame;
|
||||||
|
_latestWidth = width;
|
||||||
|
_latestHeight = height;
|
||||||
|
_latestSequence++;
|
||||||
|
if (_thread is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_thread = new Thread(Run)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu Vulkan VideoOut",
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Run()
|
||||||
|
{
|
||||||
|
uint width;
|
||||||
|
uint height;
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
width = _latestWidth;
|
||||||
|
height = _latestHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var presenter = new Presenter(width, height);
|
||||||
|
presenter.Run();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] Vulkan VideoOut presenter failed: {exception.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_closed = true;
|
||||||
|
_thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryTakeFrame(long presentedSequence, out Frame frame)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_latestFrame is null || _latestSequence == presentedSequence)
|
||||||
|
{
|
||||||
|
frame = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = new Frame(_latestFrame, _latestWidth, _latestHeight, _latestSequence);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct Frame(byte[] Pixels, uint Width, uint Height, long Sequence);
|
||||||
|
|
||||||
|
private sealed class Presenter : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IWindow _window;
|
||||||
|
private Vk _vk = null!;
|
||||||
|
private KhrSurface _surfaceApi = null!;
|
||||||
|
private KhrSwapchain _swapchainApi = null!;
|
||||||
|
private Instance _instance;
|
||||||
|
private SurfaceKHR _surface;
|
||||||
|
private PhysicalDevice _physicalDevice;
|
||||||
|
private Device _device;
|
||||||
|
private Queue _queue;
|
||||||
|
private uint _queueFamilyIndex;
|
||||||
|
private SwapchainKHR _swapchain;
|
||||||
|
private Image[] _swapchainImages = [];
|
||||||
|
private bool[] _imageInitialized = [];
|
||||||
|
private Format _swapchainFormat;
|
||||||
|
private Extent2D _extent;
|
||||||
|
private CommandPool _commandPool;
|
||||||
|
private CommandBuffer _commandBuffer;
|
||||||
|
private VkSemaphore _imageAvailable;
|
||||||
|
private VkSemaphore _renderFinished;
|
||||||
|
private VkBuffer _stagingBuffer;
|
||||||
|
private DeviceMemory _stagingMemory;
|
||||||
|
private ulong _stagingSize;
|
||||||
|
private long _presentedSequence;
|
||||||
|
private bool _vulkanReady;
|
||||||
|
private bool _firstFramePresented;
|
||||||
|
|
||||||
|
public Presenter(uint width, uint height)
|
||||||
|
{
|
||||||
|
var options = WindowOptions.DefaultVulkan;
|
||||||
|
options.Size = new Vector2D<int>((int)width, (int)height);
|
||||||
|
options.Title = "SharpEmu VideoOut";
|
||||||
|
options.WindowBorder = WindowBorder.Fixed;
|
||||||
|
options.VSync = true;
|
||||||
|
_window = Window.Create(options);
|
||||||
|
_window.Load += Initialize;
|
||||||
|
_window.Render += Render;
|
||||||
|
_window.Closing += DisposeVulkan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run() => _window.Run();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisposeVulkan();
|
||||||
|
_window.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Initialize()
|
||||||
|
{
|
||||||
|
_vk = Vk.GetApi();
|
||||||
|
CreateInstance();
|
||||||
|
CreateSurface();
|
||||||
|
SelectPhysicalDevice();
|
||||||
|
CreateDevice();
|
||||||
|
CreateSwapchain();
|
||||||
|
CreateCommandResources();
|
||||||
|
_vulkanReady = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateInstance()
|
||||||
|
{
|
||||||
|
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var applicationInfo = new ApplicationInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ApplicationInfo,
|
||||||
|
PApplicationName = applicationName,
|
||||||
|
ApplicationVersion = Vk.MakeVersion(0, 0, 1),
|
||||||
|
PEngineName = applicationName,
|
||||||
|
EngineVersion = Vk.MakeVersion(0, 0, 1),
|
||||||
|
ApiVersion = Vk.Version12,
|
||||||
|
};
|
||||||
|
|
||||||
|
var extensions = _window.VkSurface!.GetRequiredExtensions(out var extensionCount);
|
||||||
|
var createInfo = new InstanceCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.InstanceCreateInfo,
|
||||||
|
PApplicationInfo = &applicationInfo,
|
||||||
|
EnabledExtensionCount = extensionCount,
|
||||||
|
PpEnabledExtensionNames = extensions,
|
||||||
|
};
|
||||||
|
|
||||||
|
Check(_vk.CreateInstance(&createInfo, null, out _instance), "vkCreateInstance");
|
||||||
|
if (!_vk.TryGetInstanceExtension(_instance, out _surfaceApi))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SilkMarshal.Free((nint)applicationName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateSurface()
|
||||||
|
{
|
||||||
|
var instanceHandle = new VkHandle(_instance.Handle);
|
||||||
|
var surfaceHandle = _window.VkSurface!.Create<AllocationCallbacks>(instanceHandle, null);
|
||||||
|
_surface = new SurfaceKHR(surfaceHandle.Handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectPhysicalDevice()
|
||||||
|
{
|
||||||
|
uint deviceCount = 0;
|
||||||
|
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, null), "vkEnumeratePhysicalDevices");
|
||||||
|
if (deviceCount == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("No Vulkan physical device was found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var devices = new PhysicalDevice[deviceCount];
|
||||||
|
fixed (PhysicalDevice* devicePointer = devices)
|
||||||
|
{
|
||||||
|
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var device in devices)
|
||||||
|
{
|
||||||
|
uint queueCount = 0;
|
||||||
|
_vk.GetPhysicalDeviceQueueFamilyProperties(device, &queueCount, null);
|
||||||
|
var queues = new QueueFamilyProperties[queueCount];
|
||||||
|
fixed (QueueFamilyProperties* queuePointer = queues)
|
||||||
|
{
|
||||||
|
_vk.GetPhysicalDeviceQueueFamilyProperties(device, &queueCount, queuePointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint index = 0; index < queueCount; index++)
|
||||||
|
{
|
||||||
|
var supportsGraphics = (queues[index].QueueFlags & QueueFlags.GraphicsBit) != 0;
|
||||||
|
_surfaceApi.GetPhysicalDeviceSurfaceSupport(device, index, _surface, out var supportsPresent);
|
||||||
|
if (!supportsGraphics || !supportsPresent)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_physicalDevice = device;
|
||||||
|
_queueFamilyIndex = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateDevice()
|
||||||
|
{
|
||||||
|
var priority = 1.0f;
|
||||||
|
var queueInfo = new DeviceQueueCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DeviceQueueCreateInfo,
|
||||||
|
QueueFamilyIndex = _queueFamilyIndex,
|
||||||
|
QueueCount = 1,
|
||||||
|
PQueuePriorities = &priority,
|
||||||
|
};
|
||||||
|
|
||||||
|
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var createInfo = new DeviceCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.DeviceCreateInfo,
|
||||||
|
QueueCreateInfoCount = 1,
|
||||||
|
PQueueCreateInfos = &queueInfo,
|
||||||
|
EnabledExtensionCount = 1,
|
||||||
|
PpEnabledExtensionNames = &swapchainExtension,
|
||||||
|
};
|
||||||
|
|
||||||
|
Check(_vk.CreateDevice(_physicalDevice, &createInfo, null, out _device), "vkCreateDevice");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SilkMarshal.Free((nint)swapchainExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
||||||
|
if (!_vk.TryGetDeviceExtension(_instance, _device, out _swapchainApi))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("VK_KHR_swapchain is unavailable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateSwapchain()
|
||||||
|
{
|
||||||
|
Check(
|
||||||
|
_surfaceApi.GetPhysicalDeviceSurfaceCapabilities(_physicalDevice, _surface, out var capabilities),
|
||||||
|
"vkGetPhysicalDeviceSurfaceCapabilitiesKHR");
|
||||||
|
|
||||||
|
uint formatCount = 0;
|
||||||
|
Check(
|
||||||
|
_surfaceApi.GetPhysicalDeviceSurfaceFormats(_physicalDevice, _surface, &formatCount, null),
|
||||||
|
"vkGetPhysicalDeviceSurfaceFormatsKHR");
|
||||||
|
var formats = new SurfaceFormatKHR[formatCount];
|
||||||
|
fixed (SurfaceFormatKHR* formatPointer = formats)
|
||||||
|
{
|
||||||
|
Check(
|
||||||
|
_surfaceApi.GetPhysicalDeviceSurfaceFormats(_physicalDevice, _surface, &formatCount, formatPointer),
|
||||||
|
"vkGetPhysicalDeviceSurfaceFormatsKHR");
|
||||||
|
}
|
||||||
|
|
||||||
|
var surfaceFormat = ChooseSurfaceFormat(formats);
|
||||||
|
_swapchainFormat = surfaceFormat.Format;
|
||||||
|
_extent = ChooseExtent(capabilities);
|
||||||
|
var imageCount = capabilities.MinImageCount + 1;
|
||||||
|
if (capabilities.MaxImageCount != 0)
|
||||||
|
{
|
||||||
|
imageCount = Math.Min(imageCount, capabilities.MaxImageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
var compositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha);
|
||||||
|
var createInfo = new SwapchainCreateInfoKHR
|
||||||
|
{
|
||||||
|
SType = StructureType.SwapchainCreateInfoKhr,
|
||||||
|
Surface = _surface,
|
||||||
|
MinImageCount = imageCount,
|
||||||
|
ImageFormat = surfaceFormat.Format,
|
||||||
|
ImageColorSpace = surfaceFormat.ColorSpace,
|
||||||
|
ImageExtent = _extent,
|
||||||
|
ImageArrayLayers = 1,
|
||||||
|
ImageUsage = ImageUsageFlags.TransferDstBit,
|
||||||
|
ImageSharingMode = SharingMode.Exclusive,
|
||||||
|
PreTransform = capabilities.CurrentTransform,
|
||||||
|
CompositeAlpha = compositeAlpha,
|
||||||
|
PresentMode = PresentModeKHR.FifoKhr,
|
||||||
|
Clipped = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
Check(_swapchainApi.CreateSwapchain(_device, &createInfo, null, out _swapchain), "vkCreateSwapchainKHR");
|
||||||
|
|
||||||
|
uint swapchainImageCount = 0;
|
||||||
|
Check(
|
||||||
|
_swapchainApi.GetSwapchainImages(_device, _swapchain, &swapchainImageCount, null),
|
||||||
|
"vkGetSwapchainImagesKHR");
|
||||||
|
_swapchainImages = new Image[swapchainImageCount];
|
||||||
|
fixed (Image* imagePointer = _swapchainImages)
|
||||||
|
{
|
||||||
|
Check(
|
||||||
|
_swapchainApi.GetSwapchainImages(_device, _swapchain, &swapchainImageCount, imagePointer),
|
||||||
|
"vkGetSwapchainImagesKHR");
|
||||||
|
}
|
||||||
|
|
||||||
|
_imageInitialized = new bool[swapchainImageCount];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateCommandResources()
|
||||||
|
{
|
||||||
|
var poolInfo = new CommandPoolCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandPoolCreateInfo,
|
||||||
|
Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
|
||||||
|
QueueFamilyIndex = _queueFamilyIndex,
|
||||||
|
};
|
||||||
|
Check(_vk.CreateCommandPool(_device, &poolInfo, null, out _commandPool), "vkCreateCommandPool");
|
||||||
|
|
||||||
|
var allocateInfo = new CommandBufferAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandBufferAllocateInfo,
|
||||||
|
CommandPool = _commandPool,
|
||||||
|
Level = CommandBufferLevel.Primary,
|
||||||
|
CommandBufferCount = 1,
|
||||||
|
};
|
||||||
|
Check(_vk.AllocateCommandBuffers(_device, &allocateInfo, out _commandBuffer), "vkAllocateCommandBuffers");
|
||||||
|
|
||||||
|
var semaphoreInfo = new SemaphoreCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.SemaphoreCreateInfo,
|
||||||
|
};
|
||||||
|
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _imageAvailable), "vkCreateSemaphore");
|
||||||
|
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _renderFinished), "vkCreateSemaphore");
|
||||||
|
|
||||||
|
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateStagingBuffer(ulong size)
|
||||||
|
{
|
||||||
|
var bufferInfo = new BufferCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.BufferCreateInfo,
|
||||||
|
Size = size,
|
||||||
|
Usage = BufferUsageFlags.TransferSrcBit,
|
||||||
|
SharingMode = SharingMode.Exclusive,
|
||||||
|
};
|
||||||
|
Check(_vk.CreateBuffer(_device, &bufferInfo, null, out _stagingBuffer), "vkCreateBuffer");
|
||||||
|
|
||||||
|
_vk.GetBufferMemoryRequirements(_device, _stagingBuffer, out var requirements);
|
||||||
|
var memoryInfo = new MemoryAllocateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.MemoryAllocateInfo,
|
||||||
|
AllocationSize = requirements.Size,
|
||||||
|
MemoryTypeIndex = FindMemoryType(
|
||||||
|
requirements.MemoryTypeBits,
|
||||||
|
MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit),
|
||||||
|
};
|
||||||
|
Check(_vk.AllocateMemory(_device, &memoryInfo, null, out _stagingMemory), "vkAllocateMemory");
|
||||||
|
Check(_vk.BindBufferMemory(_device, _stagingBuffer, _stagingMemory, 0), "vkBindBufferMemory");
|
||||||
|
_stagingSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint FindMemoryType(uint typeBits, MemoryPropertyFlags requiredFlags)
|
||||||
|
{
|
||||||
|
_vk.GetPhysicalDeviceMemoryProperties(_physicalDevice, out var properties);
|
||||||
|
var memoryTypes = &properties.MemoryTypes.Element0;
|
||||||
|
for (uint index = 0; index < properties.MemoryTypeCount; index++)
|
||||||
|
{
|
||||||
|
if ((typeBits & (1u << (int)index)) != 0 &&
|
||||||
|
(memoryTypes[index].PropertyFlags & requiredFlags) == requiredFlags)
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("No compatible Vulkan host-visible memory type was found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Render(double _)
|
||||||
|
{
|
||||||
|
if (!_vulkanReady || !TryTakeFrame(_presentedSequence, out var frame))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixels = frame.Width == _extent.Width && frame.Height == _extent.Height
|
||||||
|
? frame.Pixels
|
||||||
|
: ScaleBgra(frame.Pixels, frame.Width, frame.Height, _extent.Width, _extent.Height);
|
||||||
|
if ((ulong)pixels.Length > _stagingSize)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint imageIndex;
|
||||||
|
Check(
|
||||||
|
_swapchainApi.AcquireNextImage(
|
||||||
|
_device,
|
||||||
|
_swapchain,
|
||||||
|
ulong.MaxValue,
|
||||||
|
_imageAvailable,
|
||||||
|
default,
|
||||||
|
&imageIndex),
|
||||||
|
"vkAcquireNextImageKHR");
|
||||||
|
|
||||||
|
void* mapped;
|
||||||
|
Check(
|
||||||
|
_vk.MapMemory(_device, _stagingMemory, 0, (ulong)pixels.Length, 0, &mapped),
|
||||||
|
"vkMapMemory");
|
||||||
|
fixed (byte* source = pixels)
|
||||||
|
{
|
||||||
|
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
|
||||||
|
}
|
||||||
|
_vk.UnmapMemory(_device, _stagingMemory);
|
||||||
|
|
||||||
|
Check(_vk.ResetCommandBuffer(_commandBuffer, 0), "vkResetCommandBuffer");
|
||||||
|
var beginInfo = new CommandBufferBeginInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.CommandBufferBeginInfo,
|
||||||
|
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||||
|
};
|
||||||
|
Check(_vk.BeginCommandBuffer(_commandBuffer, &beginInfo), "vkBeginCommandBuffer");
|
||||||
|
|
||||||
|
var oldLayout = _imageInitialized[imageIndex]
|
||||||
|
? ImageLayout.PresentSrcKhr
|
||||||
|
: ImageLayout.Undefined;
|
||||||
|
var toTransfer = new ImageMemoryBarrier
|
||||||
|
{
|
||||||
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
|
SrcAccessMask = _imageInitialized[imageIndex] ? AccessFlags.MemoryReadBit : 0,
|
||||||
|
DstAccessMask = AccessFlags.TransferWriteBit,
|
||||||
|
OldLayout = oldLayout,
|
||||||
|
NewLayout = ImageLayout.TransferDstOptimal,
|
||||||
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
|
Image = _swapchainImages[imageIndex],
|
||||||
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
|
};
|
||||||
|
_vk.CmdPipelineBarrier(
|
||||||
|
_commandBuffer,
|
||||||
|
_imageInitialized[imageIndex] ? PipelineStageFlags.BottomOfPipeBit : PipelineStageFlags.TopOfPipeBit,
|
||||||
|
PipelineStageFlags.TransferBit,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
&toTransfer);
|
||||||
|
|
||||||
|
var copyRegion = new BufferImageCopy
|
||||||
|
{
|
||||||
|
ImageSubresource = new ImageSubresourceLayers
|
||||||
|
{
|
||||||
|
AspectMask = ImageAspectFlags.ColorBit,
|
||||||
|
LayerCount = 1,
|
||||||
|
},
|
||||||
|
ImageExtent = new Extent3D(_extent.Width, _extent.Height, 1),
|
||||||
|
};
|
||||||
|
_vk.CmdCopyBufferToImage(
|
||||||
|
_commandBuffer,
|
||||||
|
_stagingBuffer,
|
||||||
|
_swapchainImages[imageIndex],
|
||||||
|
ImageLayout.TransferDstOptimal,
|
||||||
|
1,
|
||||||
|
©Region);
|
||||||
|
|
||||||
|
var toPresent = new ImageMemoryBarrier
|
||||||
|
{
|
||||||
|
SType = StructureType.ImageMemoryBarrier,
|
||||||
|
SrcAccessMask = AccessFlags.TransferWriteBit,
|
||||||
|
DstAccessMask = AccessFlags.MemoryReadBit,
|
||||||
|
OldLayout = ImageLayout.TransferDstOptimal,
|
||||||
|
NewLayout = ImageLayout.PresentSrcKhr,
|
||||||
|
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
|
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
|
||||||
|
Image = _swapchainImages[imageIndex],
|
||||||
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
|
};
|
||||||
|
_vk.CmdPipelineBarrier(
|
||||||
|
_commandBuffer,
|
||||||
|
PipelineStageFlags.TransferBit,
|
||||||
|
PipelineStageFlags.BottomOfPipeBit,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
1,
|
||||||
|
&toPresent);
|
||||||
|
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer");
|
||||||
|
|
||||||
|
var waitStage = PipelineStageFlags.TransferBit;
|
||||||
|
var imageAvailable = _imageAvailable;
|
||||||
|
var commandBuffer = _commandBuffer;
|
||||||
|
var renderFinished = _renderFinished;
|
||||||
|
var submitInfo = new SubmitInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.SubmitInfo,
|
||||||
|
WaitSemaphoreCount = 1,
|
||||||
|
PWaitSemaphores = &imageAvailable,
|
||||||
|
PWaitDstStageMask = &waitStage,
|
||||||
|
CommandBufferCount = 1,
|
||||||
|
PCommandBuffers = &commandBuffer,
|
||||||
|
SignalSemaphoreCount = 1,
|
||||||
|
PSignalSemaphores = &renderFinished,
|
||||||
|
};
|
||||||
|
Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit");
|
||||||
|
|
||||||
|
var swapchain = _swapchain;
|
||||||
|
var presentInfo = new PresentInfoKHR
|
||||||
|
{
|
||||||
|
SType = StructureType.PresentInfoKhr,
|
||||||
|
WaitSemaphoreCount = 1,
|
||||||
|
PWaitSemaphores = &renderFinished,
|
||||||
|
SwapchainCount = 1,
|
||||||
|
PSwapchains = &swapchain,
|
||||||
|
PImageIndices = &imageIndex,
|
||||||
|
};
|
||||||
|
Check(_swapchainApi.QueuePresent(_queue, &presentInfo), "vkQueuePresentKHR");
|
||||||
|
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
|
||||||
|
_imageInitialized[imageIndex] = true;
|
||||||
|
_presentedSequence = frame.Sequence;
|
||||||
|
if (!_firstFramePresented)
|
||||||
|
{
|
||||||
|
_firstFramePresented = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Vulkan VideoOut presented first guest frame: {frame.Width}x{frame.Height}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities)
|
||||||
|
{
|
||||||
|
if (capabilities.CurrentExtent.Width != uint.MaxValue)
|
||||||
|
{
|
||||||
|
return capabilities.CurrentExtent;
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = _window.FramebufferSize;
|
||||||
|
return new Extent2D(
|
||||||
|
Math.Clamp((uint)Math.Max(size.X, 1), capabilities.MinImageExtent.Width, capabilities.MaxImageExtent.Width),
|
||||||
|
Math.Clamp((uint)Math.Max(size.Y, 1), capabilities.MinImageExtent.Height, capabilities.MaxImageExtent.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SurfaceFormatKHR ChooseSurfaceFormat(IReadOnlyList<SurfaceFormatKHR> formats)
|
||||||
|
{
|
||||||
|
foreach (var format in formats)
|
||||||
|
{
|
||||||
|
if (format.Format is Format.B8G8R8A8Srgb or Format.B8G8R8A8Unorm &&
|
||||||
|
format.ColorSpace == ColorSpaceKHR.SpaceSrgbNonlinearKhr)
|
||||||
|
{
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return formats.Count > 0
|
||||||
|
? formats[0]
|
||||||
|
: throw new InvalidOperationException("The Vulkan surface exposes no pixel formats.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompositeAlphaFlagsKHR ChooseCompositeAlpha(CompositeAlphaFlagsKHR supported)
|
||||||
|
{
|
||||||
|
foreach (var candidate in new[]
|
||||||
|
{
|
||||||
|
CompositeAlphaFlagsKHR.OpaqueBitKhr,
|
||||||
|
CompositeAlphaFlagsKHR.PreMultipliedBitKhr,
|
||||||
|
CompositeAlphaFlagsKHR.PostMultipliedBitKhr,
|
||||||
|
CompositeAlphaFlagsKHR.InheritBitKhr,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if ((supported & candidate) != 0)
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("The Vulkan surface exposes no composite alpha mode.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImageSubresourceRange ColorSubresourceRange() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AspectMask = ImageAspectFlags.ColorBit,
|
||||||
|
LevelCount = 1,
|
||||||
|
LayerCount = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static byte[] ScaleBgra(byte[] source, uint sourceWidth, uint sourceHeight, uint width, uint height)
|
||||||
|
{
|
||||||
|
var destination = new byte[checked((int)(width * height * 4))];
|
||||||
|
for (uint y = 0; y < height; y++)
|
||||||
|
{
|
||||||
|
var sourceY = (uint)(((ulong)y * sourceHeight) / height);
|
||||||
|
for (uint x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
var sourceX = (uint)(((ulong)x * sourceWidth) / width);
|
||||||
|
var sourceOffset = checked((int)(((ulong)sourceY * sourceWidth + sourceX) * 4));
|
||||||
|
var destinationOffset = checked((int)(((ulong)y * width + x) * 4));
|
||||||
|
source.AsSpan(sourceOffset, 4).CopyTo(destination.AsSpan(destinationOffset, 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposeVulkan()
|
||||||
|
{
|
||||||
|
if (!_vulkanReady)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_vulkanReady = false;
|
||||||
|
_vk.DeviceWaitIdle(_device);
|
||||||
|
if (_stagingBuffer.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyBuffer(_device, _stagingBuffer, null);
|
||||||
|
}
|
||||||
|
if (_stagingMemory.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.FreeMemory(_device, _stagingMemory, null);
|
||||||
|
}
|
||||||
|
if (_imageAvailable.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroySemaphore(_device, _imageAvailable, null);
|
||||||
|
}
|
||||||
|
if (_renderFinished.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroySemaphore(_device, _renderFinished, null);
|
||||||
|
}
|
||||||
|
if (_commandPool.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyCommandPool(_device, _commandPool, null);
|
||||||
|
}
|
||||||
|
if (_swapchain.Handle != 0)
|
||||||
|
{
|
||||||
|
_swapchainApi.DestroySwapchain(_device, _swapchain, null);
|
||||||
|
}
|
||||||
|
if (_device.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyDevice(_device, null);
|
||||||
|
}
|
||||||
|
if (_surface.Handle != 0)
|
||||||
|
{
|
||||||
|
_surfaceApi.DestroySurface(_instance, _surface, null);
|
||||||
|
}
|
||||||
|
if (_instance.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyInstance(_instance, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Check(Result result, string operation)
|
||||||
|
{
|
||||||
|
if (result != Result.Success)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{operation} failed with {result}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,91 @@
|
|||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net10.0": {
|
"net10.0": {
|
||||||
|
"Silk.NET.Vulkan": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[2.23.0, )",
|
||||||
|
"resolved": "2.23.0",
|
||||||
|
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Silk.NET.Core": "2.23.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
|
"type": "Direct",
|
||||||
|
"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": "Direct",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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.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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Ultz.Native.GLFW": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.4.0",
|
||||||
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
|
},
|
||||||
"sharpemu.hle": {
|
"sharpemu.hle": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user