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 requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||||
|
|
||||||
// POSIX treats the requested address as a hint, and Rosetta may
|
// macOS needs alignment over-allocation; Linux uses exact-address search.
|
||||||
// relocate whole guest windows. Over-allocate once so the returned
|
if (OperatingSystem.IsMacOS())
|
||||||
// host range always contains an aligned guest-visible start.
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
{
|
||||||
var reserveSize = effectiveAlignment > PageSize
|
var reserveSize = effectiveAlignment > PageSize
|
||||||
? alignedSize + effectiveAlignment
|
? alignedSize + effectiveAlignment
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public sealed class GuiSettings
|
|||||||
|
|
||||||
public bool StrictDynlibResolution { get; set; }
|
public bool StrictDynlibResolution { get; set; }
|
||||||
|
|
||||||
|
/// <summary>GPU implementation selected for newly launched games.</summary>
|
||||||
|
public string RenderingBackend { get; set; } = "Legacy";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -43,6 +43,12 @@
|
|||||||
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||||
"Options.CpuEngine.Native": "Native",
|
"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.Label": "Strict dynlib resolution",
|
||||||
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
"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>
|
</ComboBox>
|
||||||
</Grid>
|
</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">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||||
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
<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
|
// The settings page edits _settings live, so a launch started while
|
||||||
// it is open already uses the new values.
|
// it is open already uses the new values.
|
||||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||||
|
RenderingBackendBox.SelectionChanged += (_, _) =>
|
||||||
|
_settings.RenderingBackend = SelectedRenderingBackend();
|
||||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.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");
|
CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
|
||||||
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
|
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");
|
StrictLabel.Text = loc.Get("Options.Strict.Label");
|
||||||
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
StrictDesc.Text = loc.Get("Options.Strict.Desc");
|
||||||
|
|
||||||
@@ -596,6 +603,10 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void ApplySettingsToControls()
|
private void ApplySettingsToControls()
|
||||||
{
|
{
|
||||||
|
RenderingBackendBox.SelectedIndex = string.Equals(
|
||||||
|
_settings.RenderingBackend,
|
||||||
|
"Native",
|
||||||
|
StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||||
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"trace" => 0,
|
"trace" => 0,
|
||||||
@@ -654,6 +665,9 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string SelectedRenderingBackend() =>
|
||||||
|
RenderingBackendBox.SelectedIndex == 1 ? "Native" : "Legacy";
|
||||||
|
|
||||||
private void UpdateLogFilePathText()
|
private void UpdateLogFilePathText()
|
||||||
{
|
{
|
||||||
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
|
||||||
@@ -1473,6 +1487,31 @@ public partial class MainWindow : Window
|
|||||||
_appliedEnvironmentVariables.Add(name);
|
_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();
|
var emulator = new EmulatorProcess();
|
||||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.Libs.Gpu.Vulkan;
|
using SharpEmu.Libs.Gpu.Vulkan;
|
||||||
|
using SharpEmu.Libs.Gpu.NativeVulkan;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Gpu;
|
namespace SharpEmu.Libs.Gpu;
|
||||||
|
|
||||||
@@ -12,7 +13,25 @@ namespace SharpEmu.Libs.Gpu;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class GuestGpu
|
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;
|
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
|
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)
|
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||||
{
|
{
|
||||||
var module = new SpirvModuleBuilder();
|
var module = new SpirvModuleBuilder();
|
||||||
|
|||||||
@@ -95,6 +95,32 @@ public sealed class GuestMemoryAllocatorTests
|
|||||||
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
|
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
|
private sealed class FakeHostMemory : IHostMemory
|
||||||
{
|
{
|
||||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
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
|
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
|
||||||
{
|
{
|
||||||
public bool CommitSucceeds { get; set; } = true;
|
public bool CommitSucceeds { get; set; } = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user