Rewrite Vulkan backend with native renderer

This commit is contained in:
Spooks
2026-07-15 13:27:47 -06:00
parent 864cbb0fa0
commit 5594d89cbd
21 changed files with 2518 additions and 1 deletions
+3
View File
@@ -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/&lt;titleId&gt;-&lt;timestamp&gt;.log, if <see cref="LogFilePath"/> is null.
/// </summary>
+5
View File
@@ -43,6 +43,11 @@
"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.",
"Options.RenderingBackend.Native": "Native Vulkan",
"Options.RenderingBackend.Legacy": "Silk.NET Vulkan (Legacy)",
"Options.Strict.Label": "Strict dynlib resolution",
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
+13
View File
@@ -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 (Legacy)" />
<ComboBoxItem x:Name="RenderingBackendNativeItem" Content="Native Vulkan" />
</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" />
+23
View File
@@ -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,15 @@ 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");
var emulator = new EmulatorProcess();
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
@@ -0,0 +1,85 @@
cmake_minimum_required(VERSION 3.25)
project(SharpEmuGpuVulkan VERSION 0.1.0 LANGUAGES CXX)
find_package(Vulkan REQUIRED)
find_package(SDL3 CONFIG QUIET)
if(TARGET SDL3::SDL3)
set(SE_GPU_SDL_TARGET SDL3::SDL3)
elseif(TARGET SDL3::SDL3-shared)
set(SE_GPU_SDL_TARGET SDL3::SDL3-shared)
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(SDL3 REQUIRED IMPORTED_TARGET sdl3>=3.2)
set(SE_GPU_SDL_TARGET PkgConfig::SDL3)
endif()
add_library(sharpemu_gpu_vulkan SHARED src/backend.cpp)
target_compile_features(sharpemu_gpu_vulkan PRIVATE cxx_std_20)
target_compile_definitions(sharpemu_gpu_vulkan PRIVATE SE_GPU_BUILD)
target_include_directories(sharpemu_gpu_vulkan PUBLIC include)
target_link_libraries(sharpemu_gpu_vulkan PRIVATE Vulkan::Vulkan ${SE_GPU_SDL_TARGET})
if(MSVC)
target_compile_options(sharpemu_gpu_vulkan PRIVATE /W4 /permissive- /EHsc)
else()
target_compile_options(sharpemu_gpu_vulkan PRIVATE -Wall -Wextra -Wpedantic -Wconversion -Werror)
endif()
set_target_properties(sharpemu_gpu_vulkan PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
# Visual Studio is a multi-config generator and otherwise inserts an extra
# Debug/Release directory. Keep the ABI library at the path consumed by the
# managed project on every generator.
foreach(SE_GPU_CONFIG Debug Release RelWithDebInfo MinSizeRel)
string(TOUPPER "${SE_GPU_CONFIG}" SE_GPU_CONFIG_UPPER)
set_target_properties(sharpemu_gpu_vulkan PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${SE_GPU_CONFIG_UPPER} "${CMAKE_BINARY_DIR}/bin"
LIBRARY_OUTPUT_DIRECTORY_${SE_GPU_CONFIG_UPPER} "${CMAKE_BINARY_DIR}/bin")
endforeach()
if(WIN32)
add_custom_command(TARGET sharpemu_gpu_vulkan POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_RUNTIME_DLLS:sharpemu_gpu_vulkan>
$<TARGET_FILE_DIR:sharpemu_gpu_vulkan>
COMMAND_EXPAND_LISTS)
endif()
include(CTest)
if(BUILD_TESTING)
add_executable(sharpemu_gpu_vulkan_exports_test tests/exports_test.cpp)
target_link_libraries(sharpemu_gpu_vulkan_exports_test PRIVATE sharpemu_gpu_vulkan)
set_target_properties(sharpemu_gpu_vulkan_exports_test PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
foreach(SE_GPU_CONFIG Debug Release RelWithDebInfo MinSizeRel)
string(TOUPPER "${SE_GPU_CONFIG}" SE_GPU_CONFIG_UPPER)
set_target_properties(sharpemu_gpu_vulkan_exports_test PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${SE_GPU_CONFIG_UPPER} "${CMAKE_BINARY_DIR}/bin")
endforeach()
add_test(NAME sharpemu_gpu_vulkan_exports_test COMMAND sharpemu_gpu_vulkan_exports_test)
if(TARGET Vulkan::glslc)
set(SMOKE_COMPUTE_SPV "${CMAKE_CURRENT_BINARY_DIR}/smoke.comp.spv")
set(SMOKE_VERTEX_SPV "${CMAKE_CURRENT_BINARY_DIR}/smoke.vert.spv")
set(SMOKE_FRAGMENT_SPV "${CMAKE_CURRENT_BINARY_DIR}/smoke.frag.spv")
add_custom_command(OUTPUT "${SMOKE_COMPUTE_SPV}"
COMMAND Vulkan::glslc "${CMAKE_CURRENT_SOURCE_DIR}/tests/smoke.comp" -o "${SMOKE_COMPUTE_SPV}"
DEPENDS tests/smoke.comp VERBATIM)
add_custom_command(OUTPUT "${SMOKE_VERTEX_SPV}"
COMMAND Vulkan::glslc "${CMAKE_CURRENT_SOURCE_DIR}/tests/smoke.vert" -o "${SMOKE_VERTEX_SPV}"
DEPENDS tests/smoke.vert VERBATIM)
add_custom_command(OUTPUT "${SMOKE_FRAGMENT_SPV}"
COMMAND Vulkan::glslc "${CMAKE_CURRENT_SOURCE_DIR}/tests/smoke.frag" -o "${SMOKE_FRAGMENT_SPV}"
DEPENDS tests/smoke.frag VERBATIM)
add_custom_target(sharpemu_gpu_vulkan_test_shaders
DEPENDS "${SMOKE_COMPUTE_SPV}" "${SMOKE_VERTEX_SPV}" "${SMOKE_FRAGMENT_SPV}")
add_executable(sharpemu_gpu_vulkan_abi_test tests/abi_test.cpp)
target_link_libraries(sharpemu_gpu_vulkan_abi_test PRIVATE sharpemu_gpu_vulkan)
add_dependencies(sharpemu_gpu_vulkan_abi_test sharpemu_gpu_vulkan_test_shaders)
add_test(NAME sharpemu_gpu_vulkan_abi_test
COMMAND sharpemu_gpu_vulkan_abi_test
"${SMOKE_COMPUTE_SPV}" "${SMOKE_VERTEX_SPV}" "${SMOKE_FRAGMENT_SPV}")
endif()
endif()
@@ -0,0 +1,105 @@
/* Copyright (C) 2026 SharpEmu Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <stddef.h>
#include <stdint.h>
#if defined(_WIN32)
# define SE_GPU_CALL __cdecl
# if defined(SE_GPU_BUILD)
# define SE_GPU_API __declspec(dllexport)
# else
# define SE_GPU_API __declspec(dllimport)
# endif
#else
# define SE_GPU_CALL
# define SE_GPU_API __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif
#define SE_GPU_ABI_VERSION 1u
typedef struct se_gpu_backend se_gpu_backend;
typedef enum se_gpu_result {
SE_GPU_OK = 0, SE_GPU_NOT_FOUND = 1, SE_GPU_NOT_READY = 2,
SE_GPU_INVALID_ARGUMENT = -1, SE_GPU_INCOMPATIBLE_ABI = -2,
SE_GPU_PLATFORM_ERROR = -3, SE_GPU_VULKAN_ERROR = -4,
SE_GPU_OUT_OF_MEMORY = -5, SE_GPU_INTERNAL_ERROR = -6
} se_gpu_result;
typedef struct se_gpu_bytes { const void* data; size_t size; } se_gpu_bytes;
typedef void (SE_GPU_CALL *se_gpu_log_fn)(int32_t level, const char* message, void* user);
typedef struct se_gpu_create_info {
uint32_t struct_size, abi_version, width, height, enable_validation;
const char* title_utf8;
se_gpu_log_fn log; void* log_user;
} se_gpu_create_info;
typedef struct se_gpu_sampler { uint32_t words[4]; } se_gpu_sampler;
typedef struct se_gpu_texture {
uint32_t struct_size; uint64_t address; uint32_t width, height, format, number_type;
se_gpu_bytes rgba_pixels; uint32_t is_fallback, is_storage, mip_levels, mip_level;
uint32_t pitch, tile_mode, dst_select; se_gpu_sampler sampler;
} se_gpu_texture;
typedef struct se_gpu_memory_buffer { uint64_t address; se_gpu_bytes data; } se_gpu_memory_buffer;
typedef struct se_gpu_vertex_buffer {
uint32_t struct_size, location, component_count, data_format, number_format;
uint64_t address; uint32_t stride, offset_bytes; se_gpu_bytes data;
} se_gpu_vertex_buffer;
typedef struct se_gpu_index_buffer { se_gpu_bytes data; uint32_t is_32_bit; } se_gpu_index_buffer;
typedef struct se_gpu_rect { int32_t x, y; uint32_t width, height; } se_gpu_rect;
typedef struct se_gpu_viewport { float x, y, width, height, min_depth, max_depth; } se_gpu_viewport;
typedef struct se_gpu_blend {
uint32_t enable, color_src, color_dst, color_func, alpha_src, alpha_dst, alpha_func;
uint32_t separate_alpha, write_mask;
} se_gpu_blend;
typedef struct se_gpu_render_target {
uint32_t struct_size; uint64_t address; uint32_t width, height, format, number_type, mip_levels;
} se_gpu_render_target;
typedef struct se_gpu_draw {
uint32_t struct_size, width, height; se_gpu_bytes vertex_spirv, pixel_spirv;
const se_gpu_texture* textures; uint32_t texture_count;
const se_gpu_memory_buffer* memory_buffers; uint32_t memory_buffer_count;
const se_gpu_vertex_buffer* vertex_buffers; uint32_t vertex_buffer_count;
const se_gpu_render_target* targets; uint32_t target_count;
const se_gpu_blend* blends; uint32_t blend_count;
const se_gpu_index_buffer* index_buffer; const se_gpu_rect* scissor;
const se_gpu_viewport* viewport; uint32_t attribute_count, vertex_count, instance_count;
uint32_t primitive_type, publish_targets;
} se_gpu_draw;
typedef struct se_gpu_compute {
uint32_t struct_size; uint64_t shader_address; se_gpu_bytes spirv;
const se_gpu_texture* textures; uint32_t texture_count;
const se_gpu_memory_buffer* memory_buffers; uint32_t memory_buffer_count;
uint32_t groups_x, groups_y, groups_z;
} se_gpu_compute;
typedef struct se_gpu_input {
uint32_t struct_size, keyboard_focused, virtual_keys[8], gamepad_connected, gamepad_buttons;
uint8_t left_x, left_y, right_x, right_y, left_trigger, right_trigger, reserved[2];
char gamepad_name_utf8[128];
} se_gpu_input;
SE_GPU_API uint32_t SE_GPU_CALL se_gpu_abi_version(void);
SE_GPU_API const char* SE_GPU_CALL se_gpu_last_error(const se_gpu_backend* backend);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_create(const se_gpu_create_info*, se_gpu_backend**);
SE_GPU_API void SE_GPU_CALL se_gpu_destroy(se_gpu_backend*);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_poll(se_gpu_backend*, uint32_t* should_close);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_input_snapshot(se_gpu_backend*, se_gpu_input*);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_present_bgra(
se_gpu_backend*, const void* pixels, size_t size, uint32_t width, uint32_t height, uint32_t pitch);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_submit_draw(se_gpu_backend*, const se_gpu_draw*);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_submit_compute(se_gpu_backend*, const se_gpu_compute*);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_register_display_buffer(se_gpu_backend*, uint64_t, uint32_t);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_present_guest_image(
se_gpu_backend*, uint64_t, uint32_t, uint32_t, uint32_t);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_has_guest_image(
se_gpu_backend*, uint64_t, uint32_t, uint32_t);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_blit_guest_image(
se_gpu_backend*, uint64_t, uint32_t, uint32_t, uint32_t,
uint64_t, uint32_t, uint32_t, uint32_t);
SE_GPU_API se_gpu_result SE_GPU_CALL se_gpu_render_target_output_kind(
uint32_t format, uint32_t number_type, uint32_t* output_kind);
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
#include "sharpemu_gpu_vulkan.h"
#include <algorithm>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <vector>
std::vector<char> read_file(const char* path) {
std::ifstream file(path, std::ios::binary);
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
}
int main(int argc, char** argv) {
if (se_gpu_abi_version() != SE_GPU_ABI_VERSION) return 1;
uint32_t output_kind{};
if (se_gpu_render_target_output_kind(10, 4, &output_kind) != SE_GPU_OK || output_kind != 1) return 5;
if (se_gpu_render_target_output_kind(0xffffffffu, 0, &output_kind) != SE_GPU_NOT_FOUND) return 6;
se_gpu_create_info info{}; info.struct_size = sizeof(info); info.abi_version = SE_GPU_ABI_VERSION;
info.width = 64; info.height = 64; info.title_utf8 = "SharpEmu native GPU test";
se_gpu_backend* backend{};
if (se_gpu_create(&info, &backend) != SE_GPU_OK) return 2;
if (argc != 4) { se_gpu_destroy(backend); return 8; }
std::vector<char> shader = read_file(argv[1]);
se_gpu_compute compute{}; compute.struct_size = sizeof(compute);
compute.spirv = {shader.data(), shader.size()}; compute.groups_x = 1; compute.groups_y = 1; compute.groups_z = 1;
if (shader.empty() || se_gpu_submit_compute(backend, &compute) != SE_GPU_OK) {
se_gpu_destroy(backend); return 9;
}
std::vector<char> vertex = read_file(argv[2]);
std::vector<char> fragment = read_file(argv[3]);
se_gpu_draw draw{}; draw.struct_size = sizeof(draw); draw.width = 64; draw.height = 64;
draw.vertex_spirv = {vertex.data(), vertex.size()}; draw.pixel_spirv = {fragment.data(), fragment.size()};
std::vector<uint32_t> padded_texture(8, 0xff804020u);
se_gpu_texture texture{}; texture.struct_size = sizeof(texture); texture.width = 2; texture.height = 2;
texture.format = 10; texture.pitch = 4; texture.dst_select = 0x324;
texture.rgba_pixels = {padded_texture.data(), padded_texture.size() * sizeof(uint32_t)};
draw.textures = &texture; draw.texture_count = 1;
draw.vertex_count = 3; draw.instance_count = 1; draw.primitive_type = 4;
if (vertex.empty() || fragment.empty() || se_gpu_submit_draw(backend, &draw) != SE_GPU_OK) {
se_gpu_destroy(backend); return 10;
}
std::vector<uint32_t> pixels(64 * 64);
for (uint32_t frame = 0; frame < 3; ++frame) {
std::fill(pixels.begin(), pixels.end(), 0xff000000u | frame * 0x00303030u);
if (se_gpu_present_bgra(backend, pixels.data(), pixels.size() * sizeof(uint32_t), 64, 64, 256) != SE_GPU_OK) {
se_gpu_destroy(backend); return 3;
}
uint32_t close{}; if (se_gpu_poll(backend, &close) != SE_GPU_OK || close) {
se_gpu_destroy(backend); return 4;
}
se_gpu_input input{}; input.struct_size = sizeof(input);
if (se_gpu_input_snapshot(backend, &input) != SE_GPU_OK) { se_gpu_destroy(backend); return 7; }
}
se_gpu_destroy(backend); return 0;
}
@@ -0,0 +1,14 @@
/* Copyright (C) 2026 SharpEmu Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "sharpemu_gpu_vulkan.h"
int main() {
if (se_gpu_abi_version() != SE_GPU_ABI_VERSION) return 1;
uint32_t output_kind{};
if (se_gpu_render_target_output_kind(10, 4, &output_kind) != SE_GPU_OK || output_kind != 1) return 2;
if (se_gpu_render_target_output_kind(10, 5, &output_kind) != SE_GPU_OK || output_kind != 2) return 3;
if (se_gpu_render_target_output_kind(0xffffffffu, 0, &output_kind) != SE_GPU_NOT_FOUND) return 4;
return 0;
}
@@ -0,0 +1,3 @@
#version 450
layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
void main() {}
@@ -0,0 +1,4 @@
#version 450
layout(location = 0) out vec4 color;
layout(set = 0, binding = 1) uniform sampler2D sourceTexture;
void main() { color = texture(sourceTexture, gl_FragCoord.xy / vec2(64.0)); }
@@ -0,0 +1,3 @@
#version 450
vec2 positions[3] = vec2[](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
void main() { gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); }
+8 -1
View File
@@ -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,13 @@ 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(static () =>
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND"),
"native",
StringComparison.OrdinalIgnoreCase)
? new NativeVulkanGuestGpuBackend()
: new VulkanGuestGpuBackend());
public static IGuestGpuBackend Current => Instance.Value;
}
@@ -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,172 @@
// 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";
[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,256 @@
// 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 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) 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)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompileVertexShader(state, evaluation, out var compiled, out error,
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex)) 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)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompilePixelShader(state, evaluation, outputs, out var compiled, out error,
globalBufferBase, totalGlobalBufferCount, imageBindingBase, scalarRegisterBufferIndex)) 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)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompileComputeShader(state, evaluation, localSizeX, localSizeY, localSizeZ,
out var compiled, out error)) return false;
shader = new VulkanCompiledGuestShader(compiled.Spirv); return true;
}
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)
{
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 SubmitStorageTranslatedDraw(IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount, uint width, uint height)
{
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 void SubmitComputeDispatch(ulong shaderAddress, IGuestCompiledShader computeShader,
IReadOnlyList<GuestDrawTexture> textures, IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint groupCountX, uint groupCountY, uint groupCountZ)
{
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)));
}
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 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, ulong destinationAddress, uint destinationWidth, uint destinationHeight,
uint destinationFormat)
{
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(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");
}
+38
View File
@@ -37,5 +37,43 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NativeGpuProjectDir>$(MSBuildThisFileDirectory)..\SharpEmu.Gpu.Vulkan.Native</NativeGpuProjectDir>
<NativeGpuBuildDir>$(RepoRoot)artifacts\native\gpu-vulkan</NativeGpuBuildDir>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<NativeGpuLibrary>$(NativeGpuBuildDir)\bin\sharpemu_gpu_vulkan.dll</NativeGpuLibrary>
<NativeGpuFileName>sharpemu_gpu_vulkan.dll</NativeGpuFileName>
<NativeGpuSdlLibrary>$(NativeGpuBuildDir)\bin\SDL3.dll</NativeGpuSdlLibrary>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
<NativeGpuLibrary>$(NativeGpuBuildDir)\bin\libsharpemu_gpu_vulkan.so</NativeGpuLibrary>
<NativeGpuFileName>libsharpemu_gpu_vulkan.so</NativeGpuFileName>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('OSX'))">
<NativeGpuLibrary>$(NativeGpuBuildDir)\bin\libsharpemu_gpu_vulkan.dylib</NativeGpuLibrary>
<NativeGpuFileName>libsharpemu_gpu_vulkan.dylib</NativeGpuFileName>
</PropertyGroup>
<ItemGroup Condition="'$(NativeGpuLibrary)' != ''">
<None Include="$(NativeGpuLibrary)"
Link="$(NativeGpuFileName)"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="PreserveNewest" />
<None Include="$(NativeGpuSdlLibrary)"
Condition="'$(NativeGpuSdlLibrary)' != ''"
Link="SDL3.dll"
CopyToOutputDirectory="PreserveNewest"
CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
<Target Name="BuildNativeGuestGpu" BeforeTargets="Build" Condition="'$(NativeGpuLibrary)' != ''">
<Exec Command="cmake -S &quot;$(NativeGpuProjectDir)&quot; -B &quot;$(NativeGpuBuildDir)&quot; -DCMAKE_BUILD_TYPE=$(Configuration) -DBUILD_TESTING=OFF" />
<Exec Command="cmake --build &quot;$(NativeGpuBuildDir)&quot; --config $(Configuration)" />
<Copy SourceFiles="$(NativeGpuLibrary)" DestinationFolder="$(TargetDir)" SkipUnchangedFiles="true" />
<Copy SourceFiles="$(NativeGpuSdlLibrary)"
DestinationFolder="$(TargetDir)"
SkipUnchangedFiles="true"
Condition="'$(NativeGpuSdlLibrary)' != ''" />
</Target>
</Project>
@@ -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();