mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aa084704a | |||
| 0bd63c5a74 | |||
| 25ea376d41 | |||
| 94463576f8 | |||
| c707f4d2ce | |||
| 0c750ffa61 | |||
| cd27d3824b | |||
| 74c18837f3 | |||
| 6bb7cd5192 | |||
| 7d4b6c1187 | |||
| f90b5fa82c | |||
| bd33ce464e | |||
| 3ac4c7a0bb | |||
| 3983c70475 | |||
| d0e7adec3b | |||
| 30d6e9eda6 | |||
| 919b9d92bb | |||
| 8c0903f297 | |||
| 5594d89cbd |
@@ -349,10 +349,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||
|
||||
// POSIX treats the requested address as a hint, and Rosetta may
|
||||
// relocate whole guest windows. Over-allocate once so the returned
|
||||
// host range always contains an aligned guest-visible start.
|
||||
if (!OperatingSystem.IsWindows())
|
||||
// macOS needs alignment over-allocation; Linux uses exact-address search.
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
var reserveSize = effectiveAlignment > PageSize
|
||||
? alignedSize + effectiveAlignment
|
||||
|
||||
@@ -23,6 +23,9 @@ public sealed class GuiSettings
|
||||
|
||||
public bool StrictDynlibResolution { get; set; }
|
||||
|
||||
/// <summary>GPU implementation selected for newly launched games.</summary>
|
||||
public string RenderingBackend { get; set; } = "Legacy";
|
||||
|
||||
/// <summary>
|
||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||
/// </summary>
|
||||
|
||||
@@ -43,6 +43,12 @@
|
||||
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.RenderingBackend.Label": "Rendering backend",
|
||||
"Options.RenderingBackend.Desc": "Graphics implementation used for newly launched games. Native Vulkan requires a separate library.",
|
||||
"Options.RenderingBackend.Native": "Native Vulkan (Experimental)",
|
||||
"Options.RenderingBackend.Legacy": "Silk.NET Vulkan (Default)",
|
||||
"Launch.NativeBackendMissing": "WARNING: Native Vulkan is selected, but {0} was not found next to SharpEmu. Download it from https://github.com/sharpemu/sharpemu.vulkan or select Silk.NET Vulkan in Options.",
|
||||
|
||||
"Options.Strict.Label": "Strict dynlib resolution",
|
||||
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
||||
|
||||
|
||||
@@ -203,6 +203,19 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="RenderingBackendLabel" Text="Rendering backend" FontSize="13" />
|
||||
<TextBlock x:Name="RenderingBackendDesc" Text="Graphics implementation used for newly launched games."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="RenderingBackendBox" Width="200" SelectedIndex="0"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="RenderingBackendLegacyItem" Content="Silk.NET Vulkan (Default)" />
|
||||
<ComboBoxItem x:Name="RenderingBackendNativeItem" Content="Native Vulkan (Experimental)" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
||||
|
||||
@@ -116,6 +116,8 @@ public partial class MainWindow : Window
|
||||
// The settings page edits _settings live, so a launch started while
|
||||
// it is open already uses the new values.
|
||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||
RenderingBackendBox.SelectionChanged += (_, _) =>
|
||||
_settings.RenderingBackend = SelectedRenderingBackend();
|
||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||
@@ -439,6 +441,11 @@ public partial class MainWindow : Window
|
||||
CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
|
||||
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
||||
|
||||
RenderingBackendLabel.Text = loc.Get("Options.RenderingBackend.Label");
|
||||
RenderingBackendDesc.Text = loc.Get("Options.RenderingBackend.Desc");
|
||||
RenderingBackendNativeItem.Content = loc.Get("Options.RenderingBackend.Native");
|
||||
RenderingBackendLegacyItem.Content = loc.Get("Options.RenderingBackend.Legacy");
|
||||
|
||||
StrictLabel.Text = loc.Get("Options.Strict.Label");
|
||||
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
||||
|
||||
@@ -596,6 +603,10 @@ public partial class MainWindow : Window
|
||||
|
||||
private void ApplySettingsToControls()
|
||||
{
|
||||
RenderingBackendBox.SelectedIndex = string.Equals(
|
||||
_settings.RenderingBackend,
|
||||
"Native",
|
||||
StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
||||
{
|
||||
"trace" => 0,
|
||||
@@ -654,6 +665,9 @@ public partial class MainWindow : Window
|
||||
};
|
||||
}
|
||||
|
||||
private string SelectedRenderingBackend() =>
|
||||
RenderingBackendBox.SelectedIndex == 1 ? "Native" : "Legacy";
|
||||
|
||||
private void UpdateLogFilePathText()
|
||||
{
|
||||
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
||||
@@ -1473,6 +1487,31 @@ public partial class MainWindow : Window
|
||||
_appliedEnvironmentVariables.Add(name);
|
||||
}
|
||||
|
||||
// The GUI owns this setting when it launches the emulator. Set both
|
||||
// choices explicitly so a SHARPEMU_GPU_BACKEND value inherited by the
|
||||
// launcher cannot silently override the Options menu.
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SHARPEMU_GPU_BACKEND",
|
||||
string.Equals(_settings.RenderingBackend, "Legacy", StringComparison.OrdinalIgnoreCase)
|
||||
? "legacy"
|
||||
: "native");
|
||||
|
||||
if (string.Equals(_settings.RenderingBackend, "Native", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var nativeLibraryName = OperatingSystem.IsWindows()
|
||||
? "sharpemu_gpu_vulkan.dll"
|
||||
: OperatingSystem.IsMacOS()
|
||||
? "libsharpemu_gpu_vulkan.dylib"
|
||||
: "libsharpemu_gpu_vulkan.so";
|
||||
var emulatorDirectory = Path.GetDirectoryName(_emulatorExePath) ?? AppContext.BaseDirectory;
|
||||
if (!File.Exists(Path.Combine(emulatorDirectory, nativeLibraryName)))
|
||||
{
|
||||
AppendConsoleLine(
|
||||
Localization.Instance.Format("Launch.NativeBackendMissing", nativeLibraryName),
|
||||
WarningLineBrush);
|
||||
}
|
||||
}
|
||||
|
||||
var emulator = new EmulatorProcess();
|
||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu;
|
||||
|
||||
@@ -12,7 +13,25 @@ namespace SharpEmu.Libs.Gpu;
|
||||
/// </summary>
|
||||
internal static class GuestGpu
|
||||
{
|
||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
|
||||
private static readonly Lazy<IGuestGpuBackend> Instance = new(CreateBackend);
|
||||
|
||||
public static IGuestGpuBackend Current => Instance.Value;
|
||||
|
||||
private static IGuestGpuBackend CreateBackend()
|
||||
{
|
||||
if (!string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND"),
|
||||
"native",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new VulkanGuestGpuBackend();
|
||||
}
|
||||
|
||||
if (!NativeVulkanApi.IsAvailable(out var error))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARN] {error}");
|
||||
}
|
||||
|
||||
return new NativeVulkanGuestGpuBackend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE.Host;
|
||||
using SharpEmu.HLE.Host.Posix;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal sealed unsafe class NativeGpuInputSource : IPosixWindowInputSource
|
||||
{
|
||||
internal static NativeGpuInputSource Instance { get; } = new();
|
||||
private readonly object _gate = new();
|
||||
private readonly uint[] _keys = new uint[8];
|
||||
private bool _focused;
|
||||
private bool _gamepadConnected;
|
||||
private HostGamepadState _gamepad;
|
||||
private string? _gamepadName;
|
||||
|
||||
private NativeGpuInputSource() { }
|
||||
|
||||
internal void Attach() => PosixHostInput.SetSource(this);
|
||||
|
||||
internal void Update(NativeVulkanApi.Input* state)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_focused = state->KeyboardFocused != 0;
|
||||
for (var index = 0; index < _keys.Length; ++index) _keys[index] = state->VirtualKeys[index];
|
||||
_gamepadConnected = state->GamepadConnected != 0;
|
||||
_gamepad = new HostGamepadState(
|
||||
_gamepadConnected,
|
||||
(HostGamepadButtons)state->GamepadButtons,
|
||||
state->LeftX,
|
||||
state->LeftY,
|
||||
state->RightX,
|
||||
state->RightY,
|
||||
state->LeftTrigger,
|
||||
state->RightTrigger);
|
||||
_gamepadName = _gamepadConnected
|
||||
? Marshal.PtrToStringUTF8((nint)state->GamepadNameUtf8)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasKeyboardFocus
|
||||
{
|
||||
get { lock (_gate) return _focused; }
|
||||
}
|
||||
|
||||
public bool IsKeyDown(int virtualKey)
|
||||
{
|
||||
if ((uint)virtualKey >= 256) return false;
|
||||
lock (_gate) return (_keys[virtualKey / 32] & (1u << (virtualKey % 32))) != 0;
|
||||
}
|
||||
|
||||
public int GetGamepadStates(Span<HostGamepadState> destination)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_gamepadConnected || destination.IsEmpty) return 0;
|
||||
destination[0] = _gamepad;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public string? DescribeConnectedGamepad()
|
||||
{
|
||||
lock (_gate) return _gamepadConnected ? _gamepadName ?? "SDL gamepad" : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal static unsafe class NativeGpuPacket
|
||||
{
|
||||
internal static NativeGpuResult SubmitDraw(
|
||||
nint backend,
|
||||
VulkanCompiledGuestShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> memoryBuffers,
|
||||
uint width,
|
||||
uint height,
|
||||
uint attributeCount,
|
||||
VulkanCompiledGuestShader? vertexShader,
|
||||
uint vertexCount,
|
||||
uint instanceCount,
|
||||
uint primitiveType,
|
||||
GuestIndexBuffer? indexBuffer,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers,
|
||||
GuestRenderState? renderState,
|
||||
IReadOnlyList<GuestRenderTarget>? targets,
|
||||
bool publishTargets)
|
||||
{
|
||||
using var storage = new Storage();
|
||||
var nativeTextures = storage.Allocate<NativeVulkanApi.Texture>(textures.Count);
|
||||
for (var index = 0; index < textures.Count; ++index)
|
||||
nativeTextures[index] = Texture(textures[index], storage);
|
||||
var nativeMemory = storage.Allocate<NativeVulkanApi.MemoryBuffer>(memoryBuffers.Count);
|
||||
for (var index = 0; index < memoryBuffers.Count; ++index)
|
||||
nativeMemory[index] = new() { Address = memoryBuffers[index].BaseAddress, Data = storage.Pin(memoryBuffers[index].Data) };
|
||||
var vertices = vertexBuffers ?? [];
|
||||
var nativeVertices = storage.Allocate<NativeVulkanApi.VertexBuffer>(vertices.Count);
|
||||
for (var index = 0; index < vertices.Count; ++index)
|
||||
{
|
||||
var source = vertices[index];
|
||||
nativeVertices[index] = new()
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.VertexBuffer), Location = source.Location,
|
||||
ComponentCount = source.ComponentCount, DataFormat = source.DataFormat,
|
||||
NumberFormat = source.NumberFormat, Address = source.BaseAddress, Stride = source.Stride,
|
||||
OffsetBytes = source.OffsetBytes, Data = storage.Pin(source.Data),
|
||||
};
|
||||
}
|
||||
var targetList = targets ?? [];
|
||||
var nativeTargets = storage.Allocate<NativeVulkanApi.RenderTarget>(targetList.Count);
|
||||
for (var index = 0; index < targetList.Count; ++index)
|
||||
{
|
||||
var source = targetList[index];
|
||||
nativeTargets[index] = new()
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.RenderTarget), Address = source.Address,
|
||||
Width = source.Width, Height = source.Height, Format = source.Format,
|
||||
NumberType = source.NumberType, MipLevels = source.MipLevels,
|
||||
};
|
||||
}
|
||||
var state = renderState ?? GuestRenderState.Default;
|
||||
var blends = storage.Allocate<NativeVulkanApi.Blend>(state.Blends.Count);
|
||||
for (var index = 0; index < state.Blends.Count; ++index)
|
||||
{
|
||||
var source = state.Blends[index];
|
||||
blends[index] = new()
|
||||
{
|
||||
Enable = source.Enable ? 1u : 0u, ColorSrc = source.ColorSrcFactor,
|
||||
ColorDst = source.ColorDstFactor, ColorFunc = source.ColorFunc,
|
||||
AlphaSrc = source.AlphaSrcFactor, AlphaDst = source.AlphaDstFactor,
|
||||
AlphaFunc = source.AlphaFunc, SeparateAlpha = source.SeparateAlphaBlend ? 1u : 0u,
|
||||
WriteMask = source.WriteMask,
|
||||
};
|
||||
}
|
||||
NativeVulkanApi.IndexBuffer nativeIndex = default;
|
||||
NativeVulkanApi.IndexBuffer* nativeIndexPointer = null;
|
||||
if (indexBuffer is not null)
|
||||
{
|
||||
nativeIndex = new() { Data = storage.Pin(indexBuffer.Data), Is32Bit = indexBuffer.Is32Bit ? 1u : 0u };
|
||||
nativeIndexPointer = &nativeIndex;
|
||||
}
|
||||
NativeVulkanApi.Rect nativeScissor = default; NativeVulkanApi.Rect* scissorPointer = null;
|
||||
if (state.Scissor is { } scissor)
|
||||
{
|
||||
nativeScissor = new() { X = scissor.X, Y = scissor.Y, Width = scissor.Width, Height = scissor.Height };
|
||||
scissorPointer = &nativeScissor;
|
||||
}
|
||||
NativeVulkanApi.Viewport nativeViewport = default; NativeVulkanApi.Viewport* viewportPointer = null;
|
||||
if (state.Viewport is { } viewport)
|
||||
{
|
||||
nativeViewport = new()
|
||||
{
|
||||
X = viewport.X, Y = viewport.Y, Width = viewport.Width, Height = viewport.Height,
|
||||
MinDepth = viewport.MinDepth, MaxDepth = viewport.MaxDepth,
|
||||
};
|
||||
viewportPointer = &nativeViewport;
|
||||
}
|
||||
var draw = new NativeVulkanApi.Draw
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Draw),
|
||||
Width = width,
|
||||
Height = height,
|
||||
VertexSpirv = storage.Pin(vertexShader?.Spirv ?? SpirvFixedShaders.CreateFullscreenVertex(attributeCount)),
|
||||
PixelSpirv = storage.Pin(pixelShader.Spirv), Textures = nativeTextures,
|
||||
TextureCount = (uint)textures.Count, MemoryBuffers = nativeMemory,
|
||||
MemoryBufferCount = (uint)memoryBuffers.Count, VertexBuffers = nativeVertices,
|
||||
VertexBufferCount = (uint)vertices.Count, Targets = nativeTargets,
|
||||
TargetCount = (uint)targetList.Count, Blends = blends, BlendCount = (uint)state.Blends.Count,
|
||||
IndexBuffer = nativeIndexPointer, Scissor = scissorPointer, ViewportState = viewportPointer,
|
||||
AttributeCount = attributeCount, VertexCount = vertexCount, InstanceCount = instanceCount,
|
||||
PrimitiveType = primitiveType, PublishTargets = publishTargets ? 1u : 0u,
|
||||
};
|
||||
return NativeVulkanApi.SubmitDraw(backend, &draw);
|
||||
}
|
||||
|
||||
internal static NativeGpuResult SubmitCompute(
|
||||
nint backend,
|
||||
ulong shaderAddress,
|
||||
VulkanCompiledGuestShader shader,
|
||||
IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> buffers,
|
||||
uint x, uint y, uint z)
|
||||
{
|
||||
using var storage = new Storage();
|
||||
var nativeTextures = storage.Allocate<NativeVulkanApi.Texture>(textures.Count);
|
||||
for (var index = 0; index < textures.Count; ++index) nativeTextures[index] = Texture(textures[index], storage);
|
||||
var nativeBuffers = storage.Allocate<NativeVulkanApi.MemoryBuffer>(buffers.Count);
|
||||
for (var index = 0; index < buffers.Count; ++index)
|
||||
nativeBuffers[index] = new() { Address = buffers[index].BaseAddress, Data = storage.Pin(buffers[index].Data) };
|
||||
var compute = new NativeVulkanApi.Compute
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Compute), ShaderAddress = shaderAddress,
|
||||
Spirv = storage.Pin(shader.Spirv), Textures = nativeTextures, TextureCount = (uint)textures.Count,
|
||||
MemoryBuffers = nativeBuffers, MemoryBufferCount = (uint)buffers.Count,
|
||||
GroupsX = x, GroupsY = y, GroupsZ = z,
|
||||
};
|
||||
return NativeVulkanApi.SubmitCompute(backend, &compute);
|
||||
}
|
||||
|
||||
private static NativeVulkanApi.Texture Texture(GuestDrawTexture source, Storage storage)
|
||||
{
|
||||
var result = new NativeVulkanApi.Texture
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.Texture), Address = source.Address,
|
||||
Width = source.Width, Height = source.Height, Format = source.Format,
|
||||
NumberType = source.NumberType, RgbaPixels = storage.Pin(source.RgbaPixels),
|
||||
IsFallback = source.IsFallback ? 1u : 0u, IsStorage = source.IsStorage ? 1u : 0u,
|
||||
MipLevels = source.MipLevels, MipLevel = source.MipLevel, Pitch = source.Pitch,
|
||||
TileMode = source.TileMode, DstSelect = source.DstSelect,
|
||||
};
|
||||
result.SamplerState.Words[0] = source.Sampler.Word0;
|
||||
result.SamplerState.Words[1] = source.Sampler.Word1;
|
||||
result.SamplerState.Words[2] = source.Sampler.Word2;
|
||||
result.SamplerState.Words[3] = source.Sampler.Word3;
|
||||
return result;
|
||||
}
|
||||
|
||||
private sealed class Storage : IDisposable
|
||||
{
|
||||
private readonly List<GCHandle> _pins = [];
|
||||
private readonly List<nint> _allocations = [];
|
||||
internal NativeVulkanApi.Bytes Pin(byte[] data)
|
||||
{
|
||||
if (data.Length == 0) return default;
|
||||
var pin = GCHandle.Alloc(data, GCHandleType.Pinned); _pins.Add(pin);
|
||||
return new() { Data = (void*)pin.AddrOfPinnedObject(), Size = (nuint)data.Length };
|
||||
}
|
||||
internal T* Allocate<T>(int count) where T : unmanaged
|
||||
{
|
||||
if (count == 0) return null;
|
||||
var pointer = NativeMemory.AllocZeroed((nuint)count, (nuint)sizeof(T));
|
||||
if (pointer is null) throw new OutOfMemoryException();
|
||||
_allocations.Add((nint)pointer); return (T*)pointer;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var pin in _pins) pin.Free();
|
||||
foreach (var allocation in _allocations) NativeMemory.Free((void*)allocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
internal enum NativeGpuResult
|
||||
{
|
||||
Success = 0,
|
||||
NotFound = 1,
|
||||
NotReady = 2,
|
||||
InvalidArgument = -1,
|
||||
IncompatibleAbi = -2,
|
||||
PlatformError = -3,
|
||||
VulkanError = -4,
|
||||
OutOfMemory = -5,
|
||||
InternalError = -6,
|
||||
}
|
||||
|
||||
internal static unsafe partial class NativeVulkanApi
|
||||
{
|
||||
internal const uint AbiVersion = 1;
|
||||
private const string Library = "sharpemu_gpu_vulkan";
|
||||
|
||||
internal static string LibraryFileName =>
|
||||
OperatingSystem.IsWindows() ? "sharpemu_gpu_vulkan.dll" :
|
||||
OperatingSystem.IsMacOS() ? "libsharpemu_gpu_vulkan.dylib" :
|
||||
"libsharpemu_gpu_vulkan.so";
|
||||
|
||||
internal static bool IsAvailable(out string error)
|
||||
{
|
||||
if (NativeLibrary.TryLoad(
|
||||
Library,
|
||||
typeof(NativeVulkanApi).Assembly,
|
||||
DllImportSearchPath.SafeDirectories | DllImportSearchPath.AssemblyDirectory,
|
||||
out var handle))
|
||||
{
|
||||
NativeLibrary.Free(handle);
|
||||
error = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
error =
|
||||
$"The optional native Vulkan backend was selected, but {LibraryFileName} could not be loaded. " +
|
||||
"Place the library from https://github.com/sharpemu/sharpemu.vulkan next to the SharpEmu executable " +
|
||||
"and ensure its SDL3 and Vulkan runtime dependencies are installed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct CreateInfo
|
||||
{
|
||||
internal uint StructSize;
|
||||
internal uint AbiVersion;
|
||||
internal uint Width;
|
||||
internal uint Height;
|
||||
internal uint EnableValidation;
|
||||
internal byte* TitleUtf8;
|
||||
internal delegate* unmanaged[Cdecl]<int, byte*, void*, void> Log;
|
||||
internal void* LogUser;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Bytes { internal void* Data; internal nuint Size; }
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Sampler { internal fixed uint Words[4]; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Texture
|
||||
{
|
||||
internal uint StructSize; internal ulong Address; internal uint Width, Height, Format, NumberType;
|
||||
internal Bytes RgbaPixels; internal uint IsFallback, IsStorage, MipLevels, MipLevel;
|
||||
internal uint Pitch, TileMode, DstSelect; internal Sampler SamplerState;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct MemoryBuffer { internal ulong Address; internal Bytes Data; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct VertexBuffer
|
||||
{
|
||||
internal uint StructSize, Location, ComponentCount, DataFormat, NumberFormat;
|
||||
internal ulong Address; internal uint Stride, OffsetBytes; internal Bytes Data;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct IndexBuffer { internal Bytes Data; internal uint Is32Bit; }
|
||||
[StructLayout(LayoutKind.Sequential)] internal struct Rect { internal int X, Y; internal uint Width, Height; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Viewport { internal float X, Y, Width, Height, MinDepth, MaxDepth; }
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Blend
|
||||
{
|
||||
internal uint Enable, ColorSrc, ColorDst, ColorFunc, AlphaSrc, AlphaDst, AlphaFunc;
|
||||
internal uint SeparateAlpha, WriteMask;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct RenderTarget
|
||||
{
|
||||
internal uint StructSize; internal ulong Address;
|
||||
internal uint Width, Height, Format, NumberType, MipLevels;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Draw
|
||||
{
|
||||
internal uint StructSize, Width, Height; internal Bytes VertexSpirv, PixelSpirv;
|
||||
internal Texture* Textures; internal uint TextureCount;
|
||||
internal MemoryBuffer* MemoryBuffers; internal uint MemoryBufferCount;
|
||||
internal VertexBuffer* VertexBuffers; internal uint VertexBufferCount;
|
||||
internal RenderTarget* Targets; internal uint TargetCount;
|
||||
internal Blend* Blends; internal uint BlendCount;
|
||||
internal IndexBuffer* IndexBuffer; internal Rect* Scissor; internal Viewport* ViewportState;
|
||||
internal uint AttributeCount, VertexCount, InstanceCount, PrimitiveType, PublishTargets;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Compute
|
||||
{
|
||||
internal uint StructSize; internal ulong ShaderAddress; internal Bytes Spirv;
|
||||
internal Texture* Textures; internal uint TextureCount;
|
||||
internal MemoryBuffer* MemoryBuffers; internal uint MemoryBufferCount;
|
||||
internal uint GroupsX, GroupsY, GroupsZ;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct Input
|
||||
{
|
||||
internal uint StructSize, KeyboardFocused;
|
||||
internal fixed uint VirtualKeys[8];
|
||||
internal uint GamepadConnected, GamepadButtons;
|
||||
internal byte LeftX, LeftY, RightX, RightY, LeftTrigger, RightTrigger;
|
||||
internal fixed byte Reserved[2]; internal fixed byte GamepadNameUtf8[128];
|
||||
}
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_abi_version")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial uint GetAbiVersion();
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_last_error")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static partial byte* LastError(nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_create")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult Create(CreateInfo* info, out nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_destroy")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial void Destroy(nint backend);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_poll")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult Poll(nint backend, out uint shouldClose);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_input_snapshot")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult InputSnapshot(nint backend, Input* input);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_present_bgra")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult PresentBgra(
|
||||
nint backend,
|
||||
void* pixels,
|
||||
nuint size,
|
||||
uint width,
|
||||
uint height,
|
||||
uint pitch);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_submit_draw")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult SubmitDraw(nint backend, Draw* draw);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_submit_compute")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult SubmitCompute(nint backend, Compute* compute);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_register_display_buffer")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult RegisterDisplayBuffer(nint backend, ulong address, uint format);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_present_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult PresentGuestImage(
|
||||
nint backend, ulong address, uint width, uint height, uint pitch);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_has_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult HasGuestImage(nint backend, ulong address, uint format, uint numberType);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_blit_guest_image")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult BlitGuestImage(
|
||||
nint backend, ulong sourceAddress, uint sourceWidth, uint sourceHeight, uint sourceFormat,
|
||||
ulong destinationAddress, uint destinationWidth, uint destinationHeight, uint destinationFormat);
|
||||
|
||||
[LibraryImport(Library, EntryPoint = "se_gpu_render_target_output_kind")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
internal static partial NativeGpuResult RenderTargetOutputKind(uint format, uint numberType, out uint outputKind);
|
||||
|
||||
internal static string GetError(nint backend)
|
||||
{
|
||||
var pointer = LastError(backend);
|
||||
return pointer is null ? "Unknown native GPU error" : Marshal.PtrToStringUTF8((nint)pointer)!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Libs.Gpu.Vulkan;
|
||||
using SharpEmu.ShaderCompiler;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
namespace SharpEmu.Libs.Gpu.NativeVulkan;
|
||||
|
||||
/// <summary>Native C++ Vulkan implementation of the guest-domain GPU seam.</summary>
|
||||
internal sealed unsafe class NativeVulkanGuestGpuBackend : IGuestGpuBackend
|
||||
{
|
||||
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
|
||||
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
|
||||
|
||||
private readonly object _startGate = new();
|
||||
private readonly BlockingCollection<Action<nint>> _commands = new(new ConcurrentQueue<Action<nint>>(), 256);
|
||||
private readonly ManualResetEventSlim _ready = new(false);
|
||||
private Thread? _thread;
|
||||
private Exception? _startError;
|
||||
|
||||
public void EnsureStarted(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0) return;
|
||||
lock (_startGate)
|
||||
{
|
||||
if (_thread is null)
|
||||
{
|
||||
_thread = new Thread(() => Run(width, height))
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu native Vulkan",
|
||||
};
|
||||
_thread.Start();
|
||||
}
|
||||
}
|
||||
_ready.Wait();
|
||||
if (_startError is not null)
|
||||
{
|
||||
if (_startError is DllNotFoundException)
|
||||
{
|
||||
_ = NativeVulkanApi.IsAvailable(out var error);
|
||||
throw new InvalidOperationException(error, _startError);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Native Vulkan startup failed", _startError);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCompileVertexShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
out IGuestCompiledShader? shader, out string error, int globalBufferBase = 0,
|
||||
int totalGlobalBufferCount = -1, int imageBindingBase = 0, int scalarRegisterBufferIndex = -1,
|
||||
int requiredVertexOutputCount = 0, ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileVertexShader(state, evaluation, out var compiled, out error,
|
||||
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex,
|
||||
requiredVertexOutputCount, storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public bool TryCompilePixelShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> outputs, out IGuestCompiledShader? shader, out string error,
|
||||
int globalBufferBase = 0, int totalGlobalBufferCount = -1, int imageBindingBase = 0,
|
||||
int scalarRegisterBufferIndex = -1, uint pixelInputEnable = 0, uint pixelInputAddress = 0,
|
||||
ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, outputs, out var compiled, out error,
|
||||
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex,
|
||||
pixelInputEnable, pixelInputAddress, storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public bool TryCompileComputeShader(Gen5ShaderState state, Gen5ShaderEvaluation evaluation,
|
||||
uint localSizeX, uint localSizeY, uint localSizeZ, out IGuestCompiledShader? shader, out string error,
|
||||
int totalGlobalBufferCount = -1, int initialScalarBufferIndex = -1, uint waveLaneCount = 32,
|
||||
ulong storageBufferOffsetAlignment = 1)
|
||||
{
|
||||
shader = null;
|
||||
if (!Gen5SpirvTranslator.TryCompileComputeShader(state, evaluation, localSizeX, localSizeY, localSizeZ,
|
||||
out var compiled, out error, totalGlobalBufferCount, initialScalarBufferIndex, waveLaneCount,
|
||||
storageBufferOffsetAlignment)) return false;
|
||||
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
|
||||
}
|
||||
|
||||
public IGuestCompiledShader GetDepthOnlyFragmentShader() => DepthOnlyFragmentShader;
|
||||
|
||||
public void HideSplashScreen() { }
|
||||
|
||||
public void Submit(byte[] bgraFrame, uint width, uint height)
|
||||
{
|
||||
if (bgraFrame.Length != checked((int)(width * height * 4))) return;
|
||||
EnsureStarted(width, height);
|
||||
Enqueue(handle =>
|
||||
{
|
||||
fixed (byte* pixels = bgraFrame)
|
||||
Check(handle, NativeVulkanApi.PresentBgra(handle, pixels, (nuint)bgraFrame.Length, width, height, width * 4));
|
||||
});
|
||||
}
|
||||
|
||||
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height)
|
||||
{
|
||||
if (drawKind != GuestDrawKind.FullscreenBarycentric || width == 0 || height == 0) return;
|
||||
EnsureStarted(width, height);
|
||||
var pixel = new VulkanCompiledGuestShader(SpirvFixedShaders.CreateBarycentricFragment());
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, pixel, [], [],
|
||||
width, height, 1, null, 3, 1, 4, null, null, null, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitTranslatedDraw(IGuestCompiledShader pixelShader, IReadOnlyList<GuestDrawTexture> textures,
|
||||
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers, uint width, uint height, uint attributeCount,
|
||||
IGuestCompiledShader? vertexShader = null, uint vertexCount = 3, uint instanceCount = 1,
|
||||
uint primitiveType = 4, GuestIndexBuffer? indexBuffer = null,
|
||||
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null, GuestRenderState? renderState = null)
|
||||
{
|
||||
EnsureStarted(width, height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
width, height, attributeCount, vs, vertexCount, instanceCount, primitiveType, indexBuffer,
|
||||
vertexCopy, renderState, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitOffscreenTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, IReadOnlyList<GuestRenderTarget> targets, IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3, uint instanceCount = 1, uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null, IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null, GuestDepthTarget? depthTarget = null, ulong shaderAddress = 0)
|
||||
{
|
||||
if (targets.Count == 0) return;
|
||||
EnsureStarted(targets[0].Width, targets[0].Height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var targetCopy = targets.ToArray(); var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
targetCopy[0].Width, targetCopy[0].Height, attributeCount, vs, vertexCount, instanceCount,
|
||||
primitiveType, indexBuffer, vertexCopy, renderState, targetCopy, true)));
|
||||
}
|
||||
|
||||
public void SubmitDepthOnlyTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, GuestDepthTarget depthTarget, IGuestCompiledShader? vertexShader = null,
|
||||
uint vertexCount = 3, uint instanceCount = 1, uint primitiveType = 4,
|
||||
GuestIndexBuffer? indexBuffer = null, IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
|
||||
GuestRenderState? renderState = null, ulong shaderAddress = 0)
|
||||
{
|
||||
EnsureStarted(depthTarget.Width, depthTarget.Height);
|
||||
var ps = Spirv(pixelShader); var vs = vertexShader is null ? null : Spirv(vertexShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
var vertexCopy = vertexBuffers?.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
depthTarget.Width, depthTarget.Height, attributeCount, vs, vertexCount, instanceCount,
|
||||
primitiveType, indexBuffer, vertexCopy, renderState, null, false)));
|
||||
}
|
||||
|
||||
public void SubmitStorageTranslatedDraw(IGuestCompiledShader pixelShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint attributeCount, uint width, uint height, ulong shaderAddress = 0)
|
||||
{
|
||||
EnsureStarted(width, height); var ps = Spirv(pixelShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
GuestRenderTarget[] targets = [new(0, width, height, 12, 7)];
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitDraw(handle, ps, textureCopy, memoryCopy,
|
||||
width, height, attributeCount, null, 3, 1, 4, null, null, null, targets, false)));
|
||||
}
|
||||
|
||||
public long SubmitComputeDispatch(ulong shaderAddress, IGuestCompiledShader computeShader,
|
||||
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
|
||||
uint groupCountX, uint groupCountY, uint groupCountZ, uint baseGroupX, uint baseGroupY,
|
||||
uint baseGroupZ, uint localSizeX, uint localSizeY, uint localSizeZ, bool isIndirect,
|
||||
bool writesGlobalMemory, uint threadCountX = uint.MaxValue, uint threadCountY = uint.MaxValue,
|
||||
uint threadCountZ = uint.MaxValue)
|
||||
{
|
||||
EnsureStarted(1280, 720); var shader = Spirv(computeShader);
|
||||
var textureCopy = textures.ToArray(); var memoryCopy = globalMemoryBuffers.ToArray();
|
||||
Enqueue(handle => Check(handle, NativeGpuPacket.SubmitCompute(handle, shaderAddress, shader,
|
||||
textureCopy, memoryCopy, groupCountX, groupCountY, groupCountZ)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool TrySubmitGuestImage(ulong address, uint width, uint height, uint pitchInPixel)
|
||||
{
|
||||
EnsureStarted(width, height);
|
||||
return Invoke(handle => NativeVulkanApi.PresentGuestImage(handle, address, width, height, pitchInPixel)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TrySubmitOrderedGuestImageFlip(int videoOutHandle, int displayBufferIndex, ulong address,
|
||||
uint width, uint height, uint pitchInPixel) =>
|
||||
TrySubmitGuestImage(address, width, height, pitchInPixel);
|
||||
|
||||
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
|
||||
{
|
||||
EnsureStarted(1280, 720);
|
||||
Enqueue(handle => Check(handle, NativeVulkanApi.RegisterDisplayBuffer(handle, address, guestFormat)));
|
||||
}
|
||||
|
||||
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType)
|
||||
{
|
||||
EnsureStarted(1280, 720);
|
||||
return Invoke(handle => NativeVulkanApi.HasGuestImage(handle, address, format, numberType)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TrySubmitGuestImageBlit(ulong sourceAddress, uint sourceWidth, uint sourceHeight,
|
||||
uint sourceFormat, uint sourceNumberType, ulong destinationAddress, uint destinationWidth,
|
||||
uint destinationHeight, uint destinationFormat, uint destinationNumberType)
|
||||
{
|
||||
EnsureStarted(destinationWidth, destinationHeight);
|
||||
return Invoke(handle => NativeVulkanApi.BlitGuestImage(handle, sourceAddress, sourceWidth, sourceHeight,
|
||||
sourceFormat, destinationAddress, destinationWidth, destinationHeight, destinationFormat)) ==
|
||||
NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType,
|
||||
out Gen5PixelOutputKind outputKind)
|
||||
{
|
||||
var result = NativeVulkanApi.RenderTargetOutputKind(dataFormat, numberType, out var nativeKind);
|
||||
outputKind = (Gen5PixelOutputKind)nativeKind; return result == NativeGpuResult.Success;
|
||||
}
|
||||
|
||||
private void Run(uint width, uint height)
|
||||
{
|
||||
nint backend = 0;
|
||||
try
|
||||
{
|
||||
if (NativeVulkanApi.GetAbiVersion() != NativeVulkanApi.AbiVersion)
|
||||
throw new InvalidOperationException("Native Vulkan ABI version mismatch");
|
||||
var title = Marshal.StringToCoTaskMemUTF8("SharpEmu");
|
||||
try
|
||||
{
|
||||
var info = new NativeVulkanApi.CreateInfo
|
||||
{
|
||||
StructSize = (uint)sizeof(NativeVulkanApi.CreateInfo), AbiVersion = NativeVulkanApi.AbiVersion,
|
||||
Width = width, Height = height, TitleUtf8 = (byte*)title,
|
||||
EnableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1" ? 1u : 0u,
|
||||
};
|
||||
var result = NativeVulkanApi.Create(&info, out backend);
|
||||
if (result != NativeGpuResult.Success)
|
||||
throw new InvalidOperationException($"se_gpu_create failed with {result}: {NativeVulkanApi.GetError(0)}");
|
||||
}
|
||||
finally { Marshal.FreeCoTaskMem(title); }
|
||||
_ready.Set();
|
||||
NativeGpuInputSource.Instance.Attach();
|
||||
while (true)
|
||||
{
|
||||
if (_commands.TryTake(out var command, 8))
|
||||
{
|
||||
command(backend);
|
||||
for (var drained = 1; drained < 128 && _commands.TryTake(out command); ++drained)
|
||||
command(backend);
|
||||
}
|
||||
var result = NativeVulkanApi.Poll(backend, out var shouldClose);
|
||||
if (result != NativeGpuResult.Success || shouldClose != 0) break;
|
||||
var input = new NativeVulkanApi.Input { StructSize = (uint)sizeof(NativeVulkanApi.Input) };
|
||||
if (NativeVulkanApi.InputSnapshot(backend, &input) == NativeGpuResult.Success)
|
||||
NativeGpuInputSource.Instance.Update(&input);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_startError ??= exception;
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Native Vulkan backend failed: {exception}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ready.Set();
|
||||
if (backend != 0) NativeVulkanApi.Destroy(backend);
|
||||
}
|
||||
}
|
||||
|
||||
private void Enqueue(Action<nint> command)
|
||||
{
|
||||
if (!_commands.TryAdd(command)) Console.Error.WriteLine("[LOADER][WARN] Native GPU queue is full; dropping work");
|
||||
}
|
||||
|
||||
private NativeGpuResult Invoke(Func<nint, NativeGpuResult> operation)
|
||||
{
|
||||
var completion = new TaskCompletionSource<NativeGpuResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
Enqueue(handle =>
|
||||
{
|
||||
try { completion.SetResult(operation(handle)); }
|
||||
catch (Exception exception) { completion.SetException(exception); }
|
||||
});
|
||||
return completion.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static void Check(nint backend, NativeGpuResult result)
|
||||
{
|
||||
if (result is NativeGpuResult.Success or NativeGpuResult.NotReady) return;
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] Native GPU operation failed: {result}: {NativeVulkanApi.GetError(backend)}");
|
||||
}
|
||||
|
||||
private static VulkanCompiledGuestShader Spirv(IGuestCompiledShader shader) =>
|
||||
shader as VulkanCompiledGuestShader ?? throw new InvalidOperationException(
|
||||
$"Shader type {shader.GetType().Name} was not compiled by the native Vulkan backend");
|
||||
}
|
||||
@@ -5,6 +5,36 @@ namespace SharpEmu.ShaderCompiler.Vulkan;
|
||||
|
||||
public static class SpirvFixedShaders
|
||||
{
|
||||
/// <summary>Fragment half of the fixed fullscreen barycentric diagnostic draw.</summary>
|
||||
public static byte[] CreateBarycentricFragment()
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
var voidType = module.TypeVoid();
|
||||
var floatType = module.TypeFloat(32);
|
||||
var vec4Type = module.TypeVector(floatType, 4);
|
||||
var inputPointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
|
||||
var outputPointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||
var barycentric = module.AddGlobalVariable(inputPointer, SpirvStorageClass.Input);
|
||||
module.AddName(barycentric, "barycentric");
|
||||
module.AddDecoration(barycentric, SpirvDecoration.Location, 0);
|
||||
module.AddDecoration(barycentric, SpirvDecoration.NoPerspective);
|
||||
var output = module.AddGlobalVariable(outputPointer, SpirvStorageClass.Output);
|
||||
module.AddName(output, "outColor");
|
||||
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
var value = module.AddInstruction(SpirvOp.Load, vec4Type, barycentric);
|
||||
module.AddStatement(SpirvOp.Store, output, value);
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
module.AddEntryPoint(SpirvExecutionModel.Fragment, main, "main", [barycentric, output]);
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||
return module.Build();
|
||||
}
|
||||
|
||||
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
|
||||
@@ -95,6 +95,32 @@ public sealed class GuestMemoryAllocatorTests
|
||||
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlignedAllocationDoesNotRetainOverallocatedMappingsOutsideMacOS()
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const ulong desiredAddress = 0x00005000_0000_0123;
|
||||
const ulong alignment = 0x10000;
|
||||
const ulong alignedAddress = 0x00005000_0001_0000;
|
||||
const ulong allocationSize = 0x2000;
|
||||
using var host = new RelocatingHostMemory(alignedAddress);
|
||||
using var memory = new PhysicalVirtualMemory(host);
|
||||
|
||||
Assert.True(memory.TryAllocateAtOrAbove(desiredAddress, 0x1234, false, alignment, out var actualAddress));
|
||||
Assert.Equal(alignedAddress + alignment, actualAddress);
|
||||
Assert.Equal(
|
||||
[
|
||||
(alignedAddress, allocationSize),
|
||||
(alignedAddress + alignment, allocationSize),
|
||||
],
|
||||
host.AllocationCalls);
|
||||
Assert.Equal([alignedAddress + 0x1000], host.FreedAddresses);
|
||||
}
|
||||
|
||||
private sealed class FakeHostMemory : IHostMemory
|
||||
{
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||
@@ -197,6 +223,63 @@ public sealed class GuestMemoryAllocatorTests
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RelocatingHostMemory(ulong firstAddress) : IHostMemory, IDisposable
|
||||
{
|
||||
private bool _relocatedFirstAllocation;
|
||||
|
||||
public List<(ulong Address, ulong Size)> AllocationCalls { get; } = [];
|
||||
|
||||
public List<ulong> FreedAddresses { get; } = [];
|
||||
|
||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
||||
{
|
||||
AllocationCalls.Add((desiredAddress, size));
|
||||
if (!_relocatedFirstAllocation)
|
||||
{
|
||||
_relocatedFirstAllocation = true;
|
||||
return firstAddress + 0x1000;
|
||||
}
|
||||
|
||||
return desiredAddress;
|
||||
}
|
||||
|
||||
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
||||
|
||||
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
||||
|
||||
public bool Free(ulong address)
|
||||
{
|
||||
FreedAddresses.Add(address);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
||||
{
|
||||
rawOldProtection = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Query(ulong address, out HostRegionInfo info)
|
||||
{
|
||||
info = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void FlushInstructionCache(ulong address, ulong size)
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
|
||||
{
|
||||
public bool CommitSucceeds { get; set; } = true;
|
||||
|
||||
Reference in New Issue
Block a user